billion-context 0.1.19 → 0.1.21
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 +198 -65
- package/README.zh-CN.md +147 -63
- package/dist/index.js +598 -50
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/config.ts
|
|
4
4
|
import { defaultConfig } from "acp-kernel";
|
|
5
|
-
import { readFileSync } from "fs";
|
|
5
|
+
import { readFileSync, existsSync, mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
6
|
+
import { dirname } from "path";
|
|
6
7
|
|
|
7
8
|
// src/paths.ts
|
|
8
9
|
import { homedir } from "os";
|
|
@@ -194,6 +195,22 @@ function loadConfigFile() {
|
|
|
194
195
|
}
|
|
195
196
|
return {};
|
|
196
197
|
}
|
|
198
|
+
var TEMPLATE_CONFIG = `{
|
|
199
|
+
"providers": {
|
|
200
|
+
}
|
|
201
|
+
}`;
|
|
202
|
+
function ensureConfigTemplate() {
|
|
203
|
+
const p = configFile();
|
|
204
|
+
if (existsSync(p)) return false;
|
|
205
|
+
try {
|
|
206
|
+
mkdirSync2(dirname(p), { recursive: true });
|
|
207
|
+
writeFileSync(p, TEMPLATE_CONFIG + "\n", "utf8");
|
|
208
|
+
log("info", `[acp-config] created empty config at ${p} \u2014 add your providers (see README Quickstart), then restart`);
|
|
209
|
+
return true;
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
197
214
|
function parseRouteEntry(v) {
|
|
198
215
|
if (typeof v === "string" && v.length > 0) {
|
|
199
216
|
return { url: v.replace(/\/$/, "") };
|
|
@@ -743,11 +760,11 @@ import { createInitialState as createInitialState2 } from "acp-kernel";
|
|
|
743
760
|
|
|
744
761
|
// src/persist.ts
|
|
745
762
|
import { promises as fs } from "fs";
|
|
746
|
-
import { existsSync, mkdirSync as
|
|
763
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
747
764
|
import { createHash as createHash2 } from "crypto";
|
|
748
765
|
import * as path3 from "path";
|
|
749
766
|
import { createInitialState } from "acp-kernel";
|
|
750
|
-
var PERSIST_VERSION =
|
|
767
|
+
var PERSIST_VERSION = 2;
|
|
751
768
|
function mergeState(parsed) {
|
|
752
769
|
const fresh = createInitialState();
|
|
753
770
|
return {
|
|
@@ -835,8 +852,9 @@ var SessionStore = class {
|
|
|
835
852
|
try {
|
|
836
853
|
const parsed = JSON.parse(await fs.readFile(full, "utf8"));
|
|
837
854
|
if (!isValidRecord(parsed)) continue;
|
|
838
|
-
const
|
|
839
|
-
const
|
|
855
|
+
const pm = parsed.meta ?? {};
|
|
856
|
+
const proto = pm.protocol ?? parsed.protocol;
|
|
857
|
+
const origin = pm.upstreamOrigin ?? parsed.upstreamOrigin;
|
|
840
858
|
const expectedNamespaced = path3.basename(relPathFor(parsed.id, proto, origin));
|
|
841
859
|
const expectedLegacy = legacyFileNameFor(parsed.id);
|
|
842
860
|
if (name !== expectedNamespaced && name !== expectedLegacy) {
|
|
@@ -859,7 +877,7 @@ var SessionStore = class {
|
|
|
859
877
|
const candidates = [this.filePath(id, meta?.protocol, meta?.upstreamOrigin)];
|
|
860
878
|
if (meta?.protocol) candidates.push(this.filePath(id));
|
|
861
879
|
for (const file of candidates) {
|
|
862
|
-
if (!
|
|
880
|
+
if (!existsSync2(file)) continue;
|
|
863
881
|
try {
|
|
864
882
|
const parsed = JSON.parse(readFileSync2(file, "utf8"));
|
|
865
883
|
if (!isValidRecord(parsed) || parsed.id !== id) continue;
|
|
@@ -890,7 +908,7 @@ var SessionStore = class {
|
|
|
890
908
|
async writeNow(session) {
|
|
891
909
|
if (!this.enabled) return;
|
|
892
910
|
const record = buildRecord(session);
|
|
893
|
-
const file = this.filePath(session.id, session.protocol, session.upstreamOrigin);
|
|
911
|
+
const file = this.filePath(session.id, session.meta.protocol, session.meta.upstreamOrigin);
|
|
894
912
|
try {
|
|
895
913
|
await fs.mkdir(path3.dirname(file), { recursive: true });
|
|
896
914
|
} catch (e) {
|
|
@@ -924,15 +942,15 @@ var SessionStore = class {
|
|
|
924
942
|
this.timers.delete(session.id);
|
|
925
943
|
}
|
|
926
944
|
const record = buildRecord(session);
|
|
927
|
-
const file = this.filePath(session.id, session.protocol, session.upstreamOrigin);
|
|
945
|
+
const file = this.filePath(session.id, session.meta.protocol, session.meta.upstreamOrigin);
|
|
928
946
|
try {
|
|
929
|
-
|
|
947
|
+
mkdirSync3(path3.dirname(file), { recursive: true });
|
|
930
948
|
} catch (e) {
|
|
931
949
|
this.log("warn", `[persist] could not create session dir ${this.dir}: ${msg(e)}`);
|
|
932
950
|
}
|
|
933
951
|
const tmp = this.tempPath(session.id);
|
|
934
952
|
try {
|
|
935
|
-
|
|
953
|
+
writeFileSync2(tmp, JSON.stringify(record), "utf8");
|
|
936
954
|
renameSync2(tmp, file);
|
|
937
955
|
return true;
|
|
938
956
|
} catch (e) {
|
|
@@ -978,13 +996,12 @@ function buildRecord(session) {
|
|
|
978
996
|
version: PERSIST_VERSION,
|
|
979
997
|
savedAt: Date.now(),
|
|
980
998
|
id: session.id,
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
requests: session.requests,
|
|
985
|
-
tokensSaved: session.tokensSaved,
|
|
999
|
+
meta: { ...session.meta },
|
|
1000
|
+
stats: { ...session.stats },
|
|
1001
|
+
metadata: { ...session.metadata },
|
|
986
1002
|
state: session.state,
|
|
987
|
-
blockContents: Object.fromEntries(session.blockContents)
|
|
1003
|
+
blockContents: Object.fromEntries(session.blockContents),
|
|
1004
|
+
createdAt: session.createdAt
|
|
988
1005
|
};
|
|
989
1006
|
}
|
|
990
1007
|
function buildSession(parsed) {
|
|
@@ -992,15 +1009,29 @@ function buildSession(parsed) {
|
|
|
992
1009
|
for (const [bid, content] of Object.entries(parsed.blockContents ?? {})) {
|
|
993
1010
|
if (content && typeof content === "object") blockContents.set(bid, content);
|
|
994
1011
|
}
|
|
1012
|
+
const meta = parsed.meta ?? {};
|
|
1013
|
+
const stats = parsed.stats ?? {};
|
|
995
1014
|
return {
|
|
996
1015
|
id: parsed.id,
|
|
997
|
-
|
|
998
|
-
|
|
1016
|
+
meta: {
|
|
1017
|
+
protocol: meta.protocol ?? parsed.protocol,
|
|
1018
|
+
upstreamOrigin: meta.upstreamOrigin ?? parsed.upstreamOrigin,
|
|
1019
|
+
label: meta.label ?? parsed.label,
|
|
1020
|
+
title: meta.title
|
|
1021
|
+
},
|
|
1022
|
+
stats: {
|
|
1023
|
+
requests: stats.requests ?? parsed.requests ?? 0,
|
|
1024
|
+
tokensSaved: stats.tokensSaved ?? parsed.tokensSaved ?? 0,
|
|
1025
|
+
inputTokens: stats.inputTokens ?? parsed.inputTokens ?? 0,
|
|
1026
|
+
cachedTokens: stats.cachedTokens ?? parsed.cachedTokens ?? 0,
|
|
1027
|
+
outputTokens: stats.outputTokens ?? parsed.outputTokens ?? 0,
|
|
1028
|
+
cacheSamples: stats.cacheSamples ?? parsed.cacheSamples ?? 0,
|
|
1029
|
+
contextTokens: stats.contextTokens ?? parsed.contextTokens ?? 0
|
|
1030
|
+
},
|
|
1031
|
+
metadata: parsed.metadata ?? {},
|
|
999
1032
|
state: mergeState(parsed.state),
|
|
1000
1033
|
createdAt: parsed.createdAt ?? Date.now(),
|
|
1001
1034
|
lastSeen: Date.now(),
|
|
1002
|
-
requests: parsed.requests ?? 0,
|
|
1003
|
-
tokensSaved: parsed.tokensSaved ?? 0,
|
|
1004
1035
|
blockContents,
|
|
1005
1036
|
inFlight: 0,
|
|
1006
1037
|
persisted: true
|
|
@@ -1065,8 +1096,9 @@ function getSession(id, meta) {
|
|
|
1065
1096
|
const existing = sessions.get(id);
|
|
1066
1097
|
if (existing) {
|
|
1067
1098
|
existing.lastSeen = Date.now();
|
|
1068
|
-
if (meta?.protocol && !existing.protocol) existing.protocol = meta.protocol;
|
|
1069
|
-
if (meta?.upstreamOrigin && !existing.upstreamOrigin) existing.upstreamOrigin = meta.upstreamOrigin;
|
|
1099
|
+
if (meta?.protocol && !existing.meta.protocol) existing.meta.protocol = meta.protocol;
|
|
1100
|
+
if (meta?.upstreamOrigin && !existing.meta.upstreamOrigin) existing.meta.upstreamOrigin = meta.upstreamOrigin;
|
|
1101
|
+
if (meta?.label && !existing.meta.label) existing.meta.label = meta.label;
|
|
1070
1102
|
return existing;
|
|
1071
1103
|
}
|
|
1072
1104
|
const store = getStore();
|
|
@@ -1080,13 +1112,12 @@ function getSession(id, meta) {
|
|
|
1080
1112
|
if (sessions.size >= MAX_SESSIONS) evictOldest();
|
|
1081
1113
|
const session = {
|
|
1082
1114
|
id,
|
|
1083
|
-
protocol: meta?.protocol,
|
|
1084
|
-
|
|
1115
|
+
meta: { protocol: meta?.protocol, upstreamOrigin: meta?.upstreamOrigin, label: meta?.label },
|
|
1116
|
+
stats: { requests: 0, tokensSaved: 0, inputTokens: 0, cachedTokens: 0, outputTokens: 0, cacheSamples: 0, contextTokens: 0 },
|
|
1117
|
+
metadata: {},
|
|
1085
1118
|
state: createInitialState2(),
|
|
1086
1119
|
createdAt: Date.now(),
|
|
1087
1120
|
lastSeen: Date.now(),
|
|
1088
|
-
requests: 0,
|
|
1089
|
-
tokensSaved: 0,
|
|
1090
1121
|
blockContents: /* @__PURE__ */ new Map(),
|
|
1091
1122
|
inFlight: 0,
|
|
1092
1123
|
persisted: false
|
|
@@ -1440,8 +1471,31 @@ function routeEvent(ev, blocks, ctx, markConverted, markRealToolUse, getConverte
|
|
|
1440
1471
|
return emitEvent(ev);
|
|
1441
1472
|
}
|
|
1442
1473
|
if (t === "message_delta") {
|
|
1474
|
+
if (!getConverted()) {
|
|
1475
|
+
const u = d.usage;
|
|
1476
|
+
if (u) {
|
|
1477
|
+
const out = u.output_tokens;
|
|
1478
|
+
if (typeof out === "number") ctx.session.stats.outputTokens += out;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1443
1481
|
return getConverted() ? NOOP : emitEvent(ev);
|
|
1444
1482
|
}
|
|
1483
|
+
if (t === "message_start" && !getConverted()) {
|
|
1484
|
+
const u = d.message?.usage;
|
|
1485
|
+
if (u) {
|
|
1486
|
+
const inp = u.input_tokens;
|
|
1487
|
+
const cc = u.cache_creation_input_tokens;
|
|
1488
|
+
const cr = u.cache_read_input_tokens;
|
|
1489
|
+
if (typeof inp === "number") ctx.session.stats.inputTokens += inp;
|
|
1490
|
+
if (typeof cr === "number") {
|
|
1491
|
+
ctx.session.stats.cachedTokens += cr;
|
|
1492
|
+
ctx.session.stats.cacheSamples += 1;
|
|
1493
|
+
} else if (typeof inp === "number") {
|
|
1494
|
+
ctx.session.stats.cacheSamples += 1;
|
|
1495
|
+
}
|
|
1496
|
+
if (typeof cc === "number") ctx.session.stats.inputTokens += cc;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1445
1499
|
if (t === "message_stop") {
|
|
1446
1500
|
return getConverted() ? NOOP : emitEvent(ev);
|
|
1447
1501
|
}
|
|
@@ -1571,6 +1625,447 @@ function rewriteJsonResponse(body, ctx) {
|
|
|
1571
1625
|
return body;
|
|
1572
1626
|
}
|
|
1573
1627
|
|
|
1628
|
+
// src/web.ts
|
|
1629
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
1630
|
+
import { dirname as dirname3, join as join2 } from "path";
|
|
1631
|
+
import { fileURLToPath } from "url";
|
|
1632
|
+
function getVersion() {
|
|
1633
|
+
try {
|
|
1634
|
+
const here = fileURLToPath(import.meta.url);
|
|
1635
|
+
const pkg = join2(dirname3(here), "..", "package.json");
|
|
1636
|
+
return JSON.parse(readFileSync3(pkg, "utf8")).version ?? "dev";
|
|
1637
|
+
} catch {
|
|
1638
|
+
return "dev";
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
function readProviders() {
|
|
1642
|
+
const parsed = safeReadJson(configFile());
|
|
1643
|
+
const routes = {};
|
|
1644
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1645
|
+
const providers = parsed.providers;
|
|
1646
|
+
if (providers) {
|
|
1647
|
+
for (const [k, v] of Object.entries(providers)) {
|
|
1648
|
+
const route = parseRouteEntry(v);
|
|
1649
|
+
if (route) routes[k] = route;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return routes;
|
|
1654
|
+
}
|
|
1655
|
+
async function handleConfigGet(res) {
|
|
1656
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1657
|
+
res.end(JSON.stringify({ path: configFile(), providers: readProviders() }, null, 2));
|
|
1658
|
+
}
|
|
1659
|
+
async function handleConfigPut(req, res) {
|
|
1660
|
+
const raw = await readJsonBody(req);
|
|
1661
|
+
if (!raw || typeof raw !== "object") {
|
|
1662
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
1663
|
+
res.end(JSON.stringify({ error: "expected JSON body" }));
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
const body = raw;
|
|
1667
|
+
if (!body.providers || typeof body.providers !== "object" || Array.isArray(body.providers)) {
|
|
1668
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
1669
|
+
res.end(JSON.stringify({ error: 'expected JSON: { "providers": { ... } }' }));
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
const routes = {};
|
|
1673
|
+
for (const [name, val] of Object.entries(body.providers)) {
|
|
1674
|
+
if (!name || typeof name !== "string") {
|
|
1675
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
1676
|
+
res.end(JSON.stringify({ error: `invalid provider name: ${JSON.stringify(name)}` }));
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
const route = parseRouteEntry(val);
|
|
1680
|
+
if (!route) {
|
|
1681
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
1682
|
+
res.end(JSON.stringify({ error: `invalid provider "${name}": expected "url" or { url, models }` }));
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
routes[name] = route;
|
|
1686
|
+
}
|
|
1687
|
+
const existing = safeReadJson(configFile()) ?? {};
|
|
1688
|
+
existing.providers = body.providers;
|
|
1689
|
+
try {
|
|
1690
|
+
mkdirSync4(dirname3(configFile()), { recursive: true });
|
|
1691
|
+
writeFileSync3(configFile(), JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
1692
|
+
} catch (e) {
|
|
1693
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
1694
|
+
res.end(JSON.stringify({ error: `failed to write: ${String(e)}` }));
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
log("info", `[acp-web] providers updated via web UI (${Object.keys(routes).length} providers) \u2014 restart to apply`);
|
|
1698
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1699
|
+
res.end(JSON.stringify({ ok: true, count: Object.keys(routes).length, note: "restart bili to apply" }));
|
|
1700
|
+
}
|
|
1701
|
+
function readJsonBody(req) {
|
|
1702
|
+
return new Promise((resolve) => {
|
|
1703
|
+
const chunks = [];
|
|
1704
|
+
let size = 0;
|
|
1705
|
+
req.on("data", (c) => {
|
|
1706
|
+
size += c.length;
|
|
1707
|
+
if (size > 256 * 1024) {
|
|
1708
|
+
req.destroy();
|
|
1709
|
+
resolve(void 0);
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
chunks.push(c);
|
|
1713
|
+
});
|
|
1714
|
+
req.on("end", () => {
|
|
1715
|
+
try {
|
|
1716
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
1717
|
+
} catch {
|
|
1718
|
+
resolve(void 0);
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
req.on("error", () => resolve(void 0));
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
function renderUI(origin) {
|
|
1725
|
+
return HTML_UI.replace(/__ORIGIN__/g, origin).replace(/__VERSION__/g, getVersion());
|
|
1726
|
+
}
|
|
1727
|
+
var HTML_UI = `<!DOCTYPE html>
|
|
1728
|
+
<html lang="en">
|
|
1729
|
+
<head>
|
|
1730
|
+
<meta charset="utf-8">
|
|
1731
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1732
|
+
<title>billion-context</title>
|
|
1733
|
+
<style>
|
|
1734
|
+
:root {
|
|
1735
|
+
--bg: #1a1b26; --bg2: #24283b; --bg3: #2f334d;
|
|
1736
|
+
--fg: #c0caf5; --dim: #565f89; --accent: #7aa2f7; --accent2: #bb9af7;
|
|
1737
|
+
--ok: #9ece6a; --warn: #e0af68; --err: #f7768e;
|
|
1738
|
+
--border: #3b4261; --radius: 8px;
|
|
1739
|
+
}
|
|
1740
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1741
|
+
body {
|
|
1742
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
1743
|
+
background: var(--bg); color: var(--fg); line-height: 1.5; min-height: 100vh;
|
|
1744
|
+
}
|
|
1745
|
+
.mono { font-family: "SF Mono", "Cascadia Code", Consolas, monospace; }
|
|
1746
|
+
header {
|
|
1747
|
+
display: flex; align-items: center; gap: 16px;
|
|
1748
|
+
padding: 16px 24px; border-bottom: 1px solid var(--border);
|
|
1749
|
+
}
|
|
1750
|
+
header a { color: inherit; text-decoration: none; }
|
|
1751
|
+
header .logo { font-size: 18px; font-weight: 600; cursor: pointer; }
|
|
1752
|
+
header .logo:hover { opacity: 0.85; }
|
|
1753
|
+
header .logo span { color: var(--accent); }
|
|
1754
|
+
header .meta { font-size: 12px; color: var(--dim); }
|
|
1755
|
+
header .meta b { color: var(--fg); }
|
|
1756
|
+
header .gh {
|
|
1757
|
+
margin-left: auto; font-size: 13px; color: var(--dim); text-decoration: none;
|
|
1758
|
+
display: flex; align-items: center; gap: 6px; padding: 6px 12px;
|
|
1759
|
+
border: 1px solid var(--border); border-radius: var(--radius); transition: color .15s, border-color .15s;
|
|
1760
|
+
}
|
|
1761
|
+
header .gh:hover { color: var(--accent); border-color: var(--accent); }
|
|
1762
|
+
nav { display: flex; gap: 4px; padding: 0 24px; border-bottom: 1px solid var(--border); }
|
|
1763
|
+
nav button {
|
|
1764
|
+
background: none; border: none; color: var(--dim); cursor: pointer;
|
|
1765
|
+
padding: 12px 16px; font-size: 14px; border-bottom: 2px solid transparent;
|
|
1766
|
+
font-family: inherit; transition: color .15s;
|
|
1767
|
+
}
|
|
1768
|
+
nav button:hover { color: var(--fg); }
|
|
1769
|
+
nav button.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
1770
|
+
main { max-width: 800px; margin: 0 auto; padding: 24px; }
|
|
1771
|
+
.tab { display: none; }
|
|
1772
|
+
.tab.active { display: block; }
|
|
1773
|
+
|
|
1774
|
+
.card {
|
|
1775
|
+
background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius);
|
|
1776
|
+
padding: 16px; margin-bottom: 12px;
|
|
1777
|
+
}
|
|
1778
|
+
.card-head {
|
|
1779
|
+
display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;
|
|
1780
|
+
}
|
|
1781
|
+
.card-head .name { font-weight: 600; color: var(--accent); }
|
|
1782
|
+
.card-head .name input { font-weight: 600; color: var(--accent); }
|
|
1783
|
+
input, select {
|
|
1784
|
+
background: var(--bg3); border: 1px solid var(--border); border-radius: 4px;
|
|
1785
|
+
color: var(--fg); padding: 6px 10px; font-size: 13px; font-family: inherit; width: 100%;
|
|
1786
|
+
}
|
|
1787
|
+
input:focus, select:focus { outline: none; border-color: var(--accent); }
|
|
1788
|
+
.row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
|
|
1789
|
+
.row label { font-size: 12px; color: var(--dim); min-width: 80px; }
|
|
1790
|
+
.row input { flex: 1; }
|
|
1791
|
+
.sub-card {
|
|
1792
|
+
background: var(--bg3); border-radius: 4px; padding: 10px 12px; margin: 8px 0 8px 20px;
|
|
1793
|
+
border-left: 2px solid var(--accent2);
|
|
1794
|
+
}
|
|
1795
|
+
.model-head { display: flex; justify-content: space-between; align-items: center; }
|
|
1796
|
+
.model-head .mname { color: var(--accent2); font-size: 13px; font-weight: 500; }
|
|
1797
|
+
.model-row { display: flex; gap: 8px; align-items: center; margin-top: 6px; }
|
|
1798
|
+
.model-row label { font-size: 11px; color: var(--dim); min-width: 60px; }
|
|
1799
|
+
.model-row input { flex: 1; font-family: "SF Mono", monospace; font-size: 12px; }
|
|
1800
|
+
.btn {
|
|
1801
|
+
background: var(--bg3); border: 1px solid var(--border); border-radius: 4px;
|
|
1802
|
+
color: var(--fg); padding: 8px 14px; font-size: 13px; cursor: pointer; font-family: inherit;
|
|
1803
|
+
transition: background .15s, border-color .15s;
|
|
1804
|
+
}
|
|
1805
|
+
.btn:hover { background: var(--border); }
|
|
1806
|
+
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--bg); font-weight: 500; }
|
|
1807
|
+
.btn.primary:hover { background: var(--accent2); border-color: var(--accent2); }
|
|
1808
|
+
.btn.danger { color: var(--err); }
|
|
1809
|
+
.btn.danger:hover { background: rgba(247,118,142,.1); }
|
|
1810
|
+
.btn.small { padding: 4px 8px; font-size: 12px; }
|
|
1811
|
+
.add-bar { display: flex; gap: 8px; margin: 12px 0; }
|
|
1812
|
+
.actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
|
1813
|
+
|
|
1814
|
+
.snippet {
|
|
1815
|
+
background: var(--bg); border: 1px solid var(--border); border-radius: 4px;
|
|
1816
|
+
padding: 12px; margin-bottom: 12px; position: relative;
|
|
1817
|
+
}
|
|
1818
|
+
.snippet .label { font-size: 12px; color: var(--dim); margin-bottom: 4px; }
|
|
1819
|
+
.snippet .label b { color: var(--fg); }
|
|
1820
|
+
.snippet code { display: block; font-size: 13px; color: var(--ok); white-space: pre-wrap; word-break: break-all; }
|
|
1821
|
+
.snippet .copy { position: absolute; top: 8px; right: 8px; }
|
|
1822
|
+
|
|
1823
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
1824
|
+
th { text-align: left; padding: 8px 12px; color: var(--dim); font-weight: 500; border-bottom: 1px solid var(--border); }
|
|
1825
|
+
td { padding: 8px 12px; border-bottom: 1px solid var(--border); }
|
|
1826
|
+
td.mono { color: var(--accent); font-size: 12px; }
|
|
1827
|
+
.empty { text-align: center; padding: 32px; color: var(--dim); font-size: 14px; }
|
|
1828
|
+
.toast {
|
|
1829
|
+
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
|
|
1830
|
+
background: var(--ok); color: var(--bg); padding: 10px 20px; border-radius: var(--radius);
|
|
1831
|
+
font-size: 14px; font-weight: 500; opacity: 0; transition: opacity .3s; pointer-events: none;
|
|
1832
|
+
}
|
|
1833
|
+
.toast.show { opacity: 1; }
|
|
1834
|
+
.toast.err { background: var(--err); color: var(--bg); }
|
|
1835
|
+
.notice {
|
|
1836
|
+
background: rgba(224,175,104,.1); border: 1px solid var(--warn); border-radius: var(--radius);
|
|
1837
|
+
color: var(--warn); padding: 10px 14px; margin-bottom: 16px; font-size: 13px;
|
|
1838
|
+
}
|
|
1839
|
+
select { cursor: pointer; }
|
|
1840
|
+
</style>
|
|
1841
|
+
</head>
|
|
1842
|
+
<body>
|
|
1843
|
+
<header>
|
|
1844
|
+
<a class="logo" href="https://github.com/ranxianglei/billion-context" target="_blank" rel="noopener" title="GitHub repo">billion<span>-context</span></a>
|
|
1845
|
+
<div class="meta">v<b>__VERSION__</b> · <b>__ORIGIN__</b></div>
|
|
1846
|
+
<a class="gh" href="https://github.com/ranxianglei/billion-context" target="_blank" rel="noopener">
|
|
1847
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
1848
|
+
GitHub
|
|
1849
|
+
</a>
|
|
1850
|
+
</header>
|
|
1851
|
+
<nav>
|
|
1852
|
+
<button class="active" onclick="showTab('providers')">Providers</button>
|
|
1853
|
+
<button onclick="showTab('setup')">Client Setup</button>
|
|
1854
|
+
<button onclick="showTab('sessions')">Sessions</button>
|
|
1855
|
+
</nav>
|
|
1856
|
+
<main>
|
|
1857
|
+
<!-- Providers tab -->
|
|
1858
|
+
<div id="tab-providers" class="tab active">
|
|
1859
|
+
<div id="restart-notice" class="notice" style="display:none"></div>
|
|
1860
|
+
<div id="providers-list"></div>
|
|
1861
|
+
<div class="add-bar">
|
|
1862
|
+
<button class="btn" onclick="addProvider()">+ Add provider</button>
|
|
1863
|
+
</div>
|
|
1864
|
+
<div class="actions">
|
|
1865
|
+
<button class="btn primary" onclick="saveProviders()">Save</button>
|
|
1866
|
+
</div>
|
|
1867
|
+
</div>
|
|
1868
|
+
|
|
1869
|
+
<!-- Client Setup tab -->
|
|
1870
|
+
<div id="tab-setup" class="tab">
|
|
1871
|
+
<div class="row" style="margin-bottom:16px">
|
|
1872
|
+
<label>Provider</label>
|
|
1873
|
+
<select id="setup-provider" onchange="renderSetup()"></select>
|
|
1874
|
+
</div>
|
|
1875
|
+
<div id="setup-snippets"></div>
|
|
1876
|
+
</div>
|
|
1877
|
+
|
|
1878
|
+
<!-- Sessions tab -->
|
|
1879
|
+
<div id="tab-sessions" class="tab">
|
|
1880
|
+
<div class="row" style="justify-content:space-between;margin-bottom:12px">
|
|
1881
|
+
<span style="font-size:13px;color:var(--dim)">Auto-refreshes every 5s</span>
|
|
1882
|
+
<span id="sess-total" style="font-size:13px;color:var(--dim)"></span>
|
|
1883
|
+
</div>
|
|
1884
|
+
<div id="sessions-table"></div>
|
|
1885
|
+
</div>
|
|
1886
|
+
</main>
|
|
1887
|
+
<div id="toast" class="toast"></div>
|
|
1888
|
+
|
|
1889
|
+
<script>
|
|
1890
|
+
var ORIGIN = "__ORIGIN__";
|
|
1891
|
+
var providers = [];
|
|
1892
|
+
var savedProviders = null;
|
|
1893
|
+
|
|
1894
|
+
// \u2500\u2500 helpers \u2500\u2500
|
|
1895
|
+
function el(id) { return document.getElementById(id); }
|
|
1896
|
+
function esc(s) { return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); }
|
|
1897
|
+
function fmtTok(n) { if (n >= 1000000) return (n/1000000).toFixed(1)+"M"; if (n >= 1000) return (n/1000).toFixed(1)+"K"; return String(n); }
|
|
1898
|
+
function toast(msg, isErr) {
|
|
1899
|
+
var t = el("toast"); t.textContent = msg; t.className = "toast show" + (isErr ? " err" : "");
|
|
1900
|
+
setTimeout(function(){ t.className = "toast" + (isErr ? " err" : ""); }, 2500);
|
|
1901
|
+
}
|
|
1902
|
+
function showTab(name) {
|
|
1903
|
+
document.querySelectorAll(".tab").forEach(function(t){ t.classList.remove("active"); });
|
|
1904
|
+
document.querySelectorAll("nav button").forEach(function(b){ b.classList.remove("active"); });
|
|
1905
|
+
el("tab-"+name).classList.add("active");
|
|
1906
|
+
event.target.classList.add("active");
|
|
1907
|
+
if (name === "sessions") refreshSessions();
|
|
1908
|
+
if (name === "setup") renderSetup();
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// \u2500\u2500 load \u2500\u2500
|
|
1912
|
+
async function load() {
|
|
1913
|
+
try {
|
|
1914
|
+
var r = await fetch("/__acp/config");
|
|
1915
|
+
var d = await r.json();
|
|
1916
|
+
providers = entries(d.providers);
|
|
1917
|
+
el("setup-provider").innerHTML = "";
|
|
1918
|
+
savedProviders = JSON.stringify(providers);
|
|
1919
|
+
renderProviders();
|
|
1920
|
+
} catch(e) {
|
|
1921
|
+
el("providers-list").innerHTML = '<div class="empty">Failed to load config: ' + esc(e) + "</div>";
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
function entries(obj) {
|
|
1925
|
+
if (!obj || typeof obj !== "object") return [];
|
|
1926
|
+
return Object.keys(obj).map(function(name){
|
|
1927
|
+
var v = obj[name];
|
|
1928
|
+
var url, models = [];
|
|
1929
|
+
if (typeof v === "string") { url = v; }
|
|
1930
|
+
else { url = v.url || ""; models = entries_models(v.models); }
|
|
1931
|
+
return { name: name, url: url, models: models };
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
function entries_models(obj) {
|
|
1935
|
+
if (!obj || typeof obj !== "object") return [];
|
|
1936
|
+
return Object.keys(obj).map(function(name){
|
|
1937
|
+
return { name: name, context: obj[name].context||0, output: obj[name].output||0 };
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// \u2500\u2500 providers editor \u2500\u2500
|
|
1942
|
+
function renderProviders() {
|
|
1943
|
+
var html = "";
|
|
1944
|
+
if (providers.length === 0) html = '<div class="empty">No providers yet. Click "Add provider" below.</div>';
|
|
1945
|
+
providers.forEach(function(p, i) {
|
|
1946
|
+
html += '<div class="card">';
|
|
1947
|
+
html += '<div class="card-head"><div class="name"><input value="'+esc(p.name)+'" onchange="providers['+i+'].name=this.value" style="background:transparent;border:none;padding:0;width:auto"></div>';
|
|
1948
|
+
html += '<button class="btn danger small" onclick="removeProvider('+i+')">Remove</button></div>';
|
|
1949
|
+
html += '<div class="row"><label>URL</label><input class="mono" value="'+esc(p.url)+'" onchange="providers['+i+'].url=this.value"></div>';
|
|
1950
|
+
p.models.forEach(function(m, j) {
|
|
1951
|
+
html += '<div class="sub-card">';
|
|
1952
|
+
html += '<div class="model-head"><span class="mname mono">'+esc(m.name)+'</span>';
|
|
1953
|
+
html += '<button class="btn danger small" onclick="removeModel('+i+','+j+')">Remove</button></div>';
|
|
1954
|
+
html += '<div class="model-row"><label>context</label><input type="number" value="'+m.context+'" onchange="providers['+i+'].models['+j+'].context=parseInt(this.value)||0"></div>';
|
|
1955
|
+
html += '<div class="model-row"><label>output</label><input type="number" value="'+m.output+'" onchange="providers['+i+'].models['+j+'].output=parseInt(this.value)||0"></div>';
|
|
1956
|
+
html += '<div class="model-row"><label>name</label><input value="'+esc(m.name)+'" onchange="providers['+i+'].models['+j+'].name=this.value"></div>';
|
|
1957
|
+
html += '</div>';
|
|
1958
|
+
});
|
|
1959
|
+
html += '<button class="btn small" onclick="addModel('+i+')">+ Add model</button>';
|
|
1960
|
+
html += '</div>';
|
|
1961
|
+
});
|
|
1962
|
+
el("providers-list").innerHTML = html;
|
|
1963
|
+
checkDirty();
|
|
1964
|
+
}
|
|
1965
|
+
function addProvider() {
|
|
1966
|
+
providers.push({ name: "new-provider", url: "https://", models: [] });
|
|
1967
|
+
renderProviders();
|
|
1968
|
+
}
|
|
1969
|
+
function removeProvider(i) {
|
|
1970
|
+
providers.splice(i, 1);
|
|
1971
|
+
renderProviders();
|
|
1972
|
+
}
|
|
1973
|
+
function addModel(i) {
|
|
1974
|
+
providers[i].models.push({ name: "model-name", context: 200000, output: 8192 });
|
|
1975
|
+
renderProviders();
|
|
1976
|
+
}
|
|
1977
|
+
function removeModel(i, j) {
|
|
1978
|
+
providers[i].models.splice(j, 1);
|
|
1979
|
+
renderProviders();
|
|
1980
|
+
}
|
|
1981
|
+
function checkDirty() {
|
|
1982
|
+
var dirty = savedProviders !== null && JSON.stringify(providers) !== savedProviders;
|
|
1983
|
+
var n = el("restart-notice");
|
|
1984
|
+
if (dirty) { n.style.display = "block"; n.textContent = "Unsaved changes \u2014 click Save to write to the config file, then restart bili to apply."; }
|
|
1985
|
+
else { n.style.display = "none"; }
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// \u2500\u2500 save \u2500\u2500
|
|
1989
|
+
async function saveProviders() {
|
|
1990
|
+
var obj = {};
|
|
1991
|
+
providers.forEach(function(p) {
|
|
1992
|
+
if (!p.name) return;
|
|
1993
|
+
if (p.models.length === 0) { obj[p.name] = p.url; }
|
|
1994
|
+
else {
|
|
1995
|
+
var models = {};
|
|
1996
|
+
p.models.forEach(function(m){ if(m.name) models[m.name] = { context: m.context, output: m.output }; });
|
|
1997
|
+
obj[p.name] = { url: p.url, models: models };
|
|
1998
|
+
}
|
|
1999
|
+
});
|
|
2000
|
+
try {
|
|
2001
|
+
var r = await fetch("/__acp/config", { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ providers: obj }) });
|
|
2002
|
+
var d = await r.json();
|
|
2003
|
+
if (r.ok) { savedProviders = JSON.stringify(providers); checkDirty(); toast("Saved " + d.count + " providers \u2014 restart bili to apply"); }
|
|
2004
|
+
else { toast("Error: " + (d.error || "unknown"), true); }
|
|
2005
|
+
} catch(e) { toast("Save failed: " + e, true); }
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// \u2500\u2500 client setup \u2500\u2500
|
|
2009
|
+
function renderSetup() {
|
|
2010
|
+
var sel = el("setup-provider");
|
|
2011
|
+
if (sel.options.length === 0 && providers.length > 0) {
|
|
2012
|
+
providers.forEach(function(p){ sel.options.add(new Option(p.name, p.name)); });
|
|
2013
|
+
}
|
|
2014
|
+
var name = sel.value || (providers[0] && providers[0].name) || "";
|
|
2015
|
+
var p = providers.find(function(x){ return x.name === name; });
|
|
2016
|
+
var box = el("setup-snippets");
|
|
2017
|
+
if (!p) { box.innerHTML = '<div class="empty">Add a provider first (Providers tab).</div>'; return; }
|
|
2018
|
+
var base = ORIGIN + "/" + p.name;
|
|
2019
|
+
// Pi: the original upstream URL minus host \u2192 keep the tail
|
|
2020
|
+
var tail = p.url.replace(/^https?:\\/\\/[^/]+/, "");
|
|
2021
|
+
var full = tail ? base + tail : base;
|
|
2022
|
+
var h = "";
|
|
2023
|
+
h += snippet("Pi (~/.pi/agent/models.json)", '"baseUrl": "' + full + '"');
|
|
2024
|
+
h += snippet("OpenCode (opencode.json)", '"baseURL": "' + full + '"');
|
|
2025
|
+
h += snippet("Codex (config.toml)", 'base_url = "' + full + '"');
|
|
2026
|
+
h += snippet("Full path (any client)", full);
|
|
2027
|
+
box.innerHTML = h;
|
|
2028
|
+
}
|
|
2029
|
+
function snippet(label, code) {
|
|
2030
|
+
var c = code.replace(/"/g, """);
|
|
2031
|
+
return '<div class="snippet"><div class="label"><b>' + label + '</b></div><code>' + esc(code) + '</code><button class="btn small copy" onclick="copyText(this,\\''+c+'\\')">Copy</button></div>';
|
|
2032
|
+
}
|
|
2033
|
+
function copyText(btn, text) {
|
|
2034
|
+
var t = text.replace(/"/g, '"');
|
|
2035
|
+
navigator.clipboard.writeText(t).then(function(){ toast("Copied"); });
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// \u2500\u2500 sessions \u2500\u2500
|
|
2039
|
+
async function refreshSessions() {
|
|
2040
|
+
try {
|
|
2041
|
+
var r = await fetch("/__acp/stats");
|
|
2042
|
+
var d = await r.json();
|
|
2043
|
+
var ss = d.sessions || [];
|
|
2044
|
+
el("sess-total").textContent = ss.length + " session" + (ss.length !== 1 ? "s" : "");
|
|
2045
|
+
if (ss.length === 0) { el("sessions-table").innerHTML = '<div class="empty">No sessions yet. Send a request through the proxy.</div>'; return; }
|
|
2046
|
+
var h = '<table><tr><th>ID</th><th>Requests</th><th>Tokens saved</th><th>Last seen</th></tr>';
|
|
2047
|
+
var h = '<table><tr><th>Title</th><th>Protocol</th><th>Label</th><th>Requests</th><th>Context</th><th>Cache hit</th><th>Input</th><th>Output</th><th>Last seen</th></tr>';
|
|
2048
|
+
ss.forEach(function(s) {
|
|
2049
|
+
var title = s.title ? esc(s.title) : "<span class='dim'>\u2014</span>";
|
|
2050
|
+
var proto = s.protocol ? esc(s.protocol) : "<span class='dim'>?</span>";
|
|
2051
|
+
var label = s.label ? "<span class=\\"mono\\">"+esc(s.label.slice(0,24))+"</span>" : "<span class='dim'>\u2014</span>";
|
|
2052
|
+
var ctx = s.contextTokens ? fmtTok(s.contextTokens) : "0";
|
|
2053
|
+
var ch = (s.cacheHitPct !== null && s.cacheHitPct !== undefined) ? s.cacheHitPct + "%" : "<span class='dim'>\u2014</span>";
|
|
2054
|
+
var inp = s.inputTokens ? fmtTok(s.inputTokens) : "0";
|
|
2055
|
+
var out = s.outputTokens ? fmtTok(s.outputTokens) : "0";
|
|
2056
|
+
h += "<tr><td>"+title+"</td><td>"+proto+"</td><td>"+label+"</td><td>"+s.requests+"</td><td>"+ctx+"</td><td>"+ch+"</td><td>"+inp+"</td><td>"+out+"</td><td>"+esc(s.lastSeen)+"</td></tr>";
|
|
2057
|
+
});
|
|
2058
|
+
h += "</table>";
|
|
2059
|
+
el("sessions-table").innerHTML = h;
|
|
2060
|
+
} catch(e) { el("sessions-table").innerHTML = '<div class="empty">Failed to load: ' + esc(e) + "</div>"; }
|
|
2061
|
+
}
|
|
2062
|
+
setInterval(function(){ if (el("tab-sessions").classList.contains("active")) refreshSessions(); }, 5000);
|
|
2063
|
+
|
|
2064
|
+
load();
|
|
2065
|
+
</script>
|
|
2066
|
+
</body>
|
|
2067
|
+
</html>`;
|
|
2068
|
+
|
|
1574
2069
|
// src/orphan-gc.ts
|
|
1575
2070
|
var ORPHAN_THRESHOLD = 3;
|
|
1576
2071
|
var orphanStreaks = /* @__PURE__ */ new WeakMap();
|
|
@@ -1608,8 +2103,8 @@ import {
|
|
|
1608
2103
|
collectBlockContent as collectBlockContent2,
|
|
1609
2104
|
deactivateBlock
|
|
1610
2105
|
} from "acp-kernel";
|
|
1611
|
-
import { mkdirSync as
|
|
1612
|
-
import { dirname as
|
|
2106
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
2107
|
+
import { dirname as dirname4, join as join3 } from "path";
|
|
1613
2108
|
import { tmpdir } from "os";
|
|
1614
2109
|
function resolveDecompress(args, ctx) {
|
|
1615
2110
|
const rawBlockId = args.blockId;
|
|
@@ -1638,11 +2133,11 @@ function resolveDecompress(args, ctx) {
|
|
|
1638
2133
|
}
|
|
1639
2134
|
const header = `[Restored block ${blockId} \u2014 ${count} item(s)${full ? ", full" : ""}]`;
|
|
1640
2135
|
const safeBlockId = blockId.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
1641
|
-
const outPath = body.length > 1e4 ?
|
|
2136
|
+
const outPath = body.length > 1e4 ? join3(tmpdir(), `acp-decompress-${safeBlockId}-${Date.now()}.txt`) : null;
|
|
1642
2137
|
if (outPath) {
|
|
1643
2138
|
try {
|
|
1644
|
-
|
|
1645
|
-
|
|
2139
|
+
mkdirSync5(dirname4(outPath), { recursive: true });
|
|
2140
|
+
writeFileSync4(outPath, body, "utf8");
|
|
1646
2141
|
return `${header}
|
|
1647
2142
|
Content (${body.length} chars) written to: ${outPath}
|
|
1648
2143
|
Use the read tool to access it.`;
|
|
@@ -1925,6 +2420,10 @@ async function* compressLoopStream(initialUpstream, ctx, requestBody, requestOpt
|
|
|
1925
2420
|
if (typeof prompt === "number") {
|
|
1926
2421
|
const ch = typeof cached === "number" ? cached : 0;
|
|
1927
2422
|
log("info", `[acp-usage] round ${loopCount} input=${prompt} cached=${typeof cached === "number" ? cached : "?"} output=${out ?? "?"}${ch > 0 ? ` (cache hit ${Math.round(ch / prompt * 100)}%)` : ""}`);
|
|
2423
|
+
ctx.session.stats.inputTokens += prompt;
|
|
2424
|
+
if (typeof cached === "number") ctx.session.stats.cachedTokens += cached;
|
|
2425
|
+
if (typeof out === "number") ctx.session.stats.outputTokens += out;
|
|
2426
|
+
ctx.session.stats.cacheSamples += 1;
|
|
1928
2427
|
}
|
|
1929
2428
|
}
|
|
1930
2429
|
if (!hasOnlyProxy) {
|
|
@@ -2325,6 +2824,12 @@ async function* compressLoopResponsesStream(initialUpstream, ctx, requestBody, r
|
|
|
2325
2824
|
const cached = inDet?.cached_tokens ?? prDet?.cached_tokens ?? "?";
|
|
2326
2825
|
const out = usage.output_tokens ?? "?";
|
|
2327
2826
|
log("info", `[acp-usage] round ${loopCount} input=${prompt} cached=${cached} output=${out}${cached !== "?" && cached !== 0 && prompt !== "?" ? ` (cache hit ${Math.round(Number(cached) / Number(prompt) * 100)}%)` : ""}`);
|
|
2827
|
+
if (typeof prompt === "number") {
|
|
2828
|
+
ctx.session.stats.inputTokens += prompt;
|
|
2829
|
+
if (typeof cached === "number") ctx.session.stats.cachedTokens += cached;
|
|
2830
|
+
if (typeof out === "number") ctx.session.stats.outputTokens += out;
|
|
2831
|
+
ctx.session.stats.cacheSamples += 1;
|
|
2832
|
+
}
|
|
2328
2833
|
}
|
|
2329
2834
|
}
|
|
2330
2835
|
}
|
|
@@ -2650,9 +3155,10 @@ async function startServer(opts) {
|
|
|
2650
3155
|
}
|
|
2651
3156
|
});
|
|
2652
3157
|
server.listen(opts.port, opts.host, () => {
|
|
3158
|
+
const displayHost = opts.host === "0.0.0.0" ? "localhost" : opts.host;
|
|
2653
3159
|
log2(
|
|
2654
3160
|
"info",
|
|
2655
|
-
`acp-proxy listening on http://${
|
|
3161
|
+
`acp-proxy listening on http://${displayHost}:${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}`) + ` \u2014 web UI: http://${displayHost}:${opts.port}/__acp/`
|
|
2656
3162
|
);
|
|
2657
3163
|
});
|
|
2658
3164
|
server.on("error", (err) => {
|
|
@@ -2704,6 +3210,14 @@ async function handle(req, res, opts, core, config, log2) {
|
|
|
2704
3210
|
res.end(JSON.stringify({ ok: true, upstream: opts.upstream }));
|
|
2705
3211
|
return;
|
|
2706
3212
|
}
|
|
3213
|
+
if (req.method === "GET" && req.url === "/__acp/") {
|
|
3214
|
+
const origin = `http://${opts.host === "0.0.0.0" ? "localhost" : opts.host}:${opts.port}`;
|
|
3215
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
3216
|
+
res.end(renderUI(origin));
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
3219
|
+
if (req.method === "GET" && req.url === "/__acp/config") return handleConfigGet(res);
|
|
3220
|
+
if (req.method === "PUT" && req.url === "/__acp/config") return handleConfigPut(req, res);
|
|
2707
3221
|
let bodyBuffer;
|
|
2708
3222
|
try {
|
|
2709
3223
|
bodyBuffer = await readBody(req);
|
|
@@ -2747,8 +3261,9 @@ async function handle(req, res, opts, core, config, log2) {
|
|
|
2747
3261
|
const sessionHeader = headerValue(req, opts.sessionHeader);
|
|
2748
3262
|
const conversation = protocol === "anthropic" ? conversationSignalAnthropic(parsed, sessionHeader) : protocol === "openai" ? conversationSignalOpenai(parsed, sessionHeader) : conversationSignalResponses(parsed, sessionHeader);
|
|
2749
3263
|
const sessionId = deriveSessionId(req.headers, protocol, upstreamOrigin, conversation);
|
|
2750
|
-
const session = getSession(sessionId, { protocol, upstreamOrigin });
|
|
2751
3264
|
const affinity = affinityToken(req.headers, conversation);
|
|
3265
|
+
const clientLabel = clientConversationHeader(req.headers);
|
|
3266
|
+
const session = getSession(sessionId, { protocol, upstreamOrigin, label: clientLabel ?? void 0 });
|
|
2752
3267
|
await withSessionLock(session, async () => {
|
|
2753
3268
|
prepared = protocol === "anthropic" ? prepareAnthropic(parsed, req, opts, core, reqConfig, log2, session) : protocol === "openai" ? prepareOpenai(parsed, req, opts, core, reqConfig, log2, session) : prepareResponses(parsed, req, opts, core, reqConfig, log2, session);
|
|
2754
3269
|
acquireInFlight(session);
|
|
@@ -2794,7 +3309,7 @@ function diagNudge(turn, sessionId, tokenCount, limit) {
|
|
|
2794
3309
|
function prepareAnthropic(parsed, req, opts, core, config, log2, session) {
|
|
2795
3310
|
const sessionId = session.id;
|
|
2796
3311
|
const stream2 = parsed.stream === true;
|
|
2797
|
-
session.requests
|
|
3312
|
+
++session.stats.requests;
|
|
2798
3313
|
let processedMessages = [];
|
|
2799
3314
|
let rebuiltMessages = parsed.messages;
|
|
2800
3315
|
let systemOut = parsed.system;
|
|
@@ -2804,6 +3319,11 @@ function prepareAnthropic(parsed, req, opts, core, config, log2, session) {
|
|
|
2804
3319
|
const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
|
|
2805
3320
|
const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: "text-only" });
|
|
2806
3321
|
session.state = turn.state;
|
|
3322
|
+
session.stats.contextTokens = tokenCount;
|
|
3323
|
+
if (!session.meta.title) {
|
|
3324
|
+
const t = deriveTitle(msgs);
|
|
3325
|
+
if (t) session.meta.title = t;
|
|
3326
|
+
}
|
|
2807
3327
|
log2("info", diagTagSummary(turn.messages, sessionId, "text-only"));
|
|
2808
3328
|
log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
|
|
2809
3329
|
processedMessages = turn.messages;
|
|
@@ -2833,7 +3353,7 @@ function prepareAnthropic(parsed, req, opts, core, config, log2, session) {
|
|
|
2833
3353
|
function prepareOpenai(parsed, req, opts, core, config, log2, session) {
|
|
2834
3354
|
const sessionId = session.id;
|
|
2835
3355
|
const stream2 = parsed.stream === true;
|
|
2836
|
-
session.requests
|
|
3356
|
+
++session.stats.requests;
|
|
2837
3357
|
let processedMessages = [];
|
|
2838
3358
|
let rebuiltMessages = parsed.messages;
|
|
2839
3359
|
let toolsOut = parsed.tools;
|
|
@@ -2845,6 +3365,11 @@ function prepareOpenai(parsed, req, opts, core, config, log2, session) {
|
|
|
2845
3365
|
const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
|
|
2846
3366
|
const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: "text-only" });
|
|
2847
3367
|
session.state = turn.state;
|
|
3368
|
+
session.stats.contextTokens = tokenCount;
|
|
3369
|
+
if (!session.meta.title) {
|
|
3370
|
+
const t = deriveTitle(msgs);
|
|
3371
|
+
if (t) session.meta.title = t;
|
|
3372
|
+
}
|
|
2848
3373
|
log2("info", diagTagSummary(turn.messages, sessionId, "text-only"));
|
|
2849
3374
|
log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
|
|
2850
3375
|
processedMessages = turn.messages;
|
|
@@ -2876,7 +3401,7 @@ function prepareOpenai(parsed, req, opts, core, config, log2, session) {
|
|
|
2876
3401
|
function prepareResponses(parsed, req, opts, core, config, log2, session) {
|
|
2877
3402
|
const sessionId = session.id;
|
|
2878
3403
|
const stream2 = parsed.stream === true;
|
|
2879
|
-
session.requests
|
|
3404
|
+
++session.stats.requests;
|
|
2880
3405
|
let processedMessages = [];
|
|
2881
3406
|
let rebuiltInput = parsed.input;
|
|
2882
3407
|
let toolsOut = parsed.tools;
|
|
@@ -2889,6 +3414,11 @@ function prepareResponses(parsed, req, opts, core, config, log2, session) {
|
|
|
2889
3414
|
const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
|
|
2890
3415
|
const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: process.env.ACP_RENDER_NONE ? "none" : "text-only" });
|
|
2891
3416
|
session.state = turn.state;
|
|
3417
|
+
session.stats.contextTokens = tokenCount;
|
|
3418
|
+
if (!session.meta.title) {
|
|
3419
|
+
const t = deriveTitle(msgs);
|
|
3420
|
+
if (t) session.meta.title = t;
|
|
3421
|
+
}
|
|
2892
3422
|
log2("info", diagTagSummary(turn.messages, sessionId, "text-only"));
|
|
2893
3423
|
log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
|
|
2894
3424
|
processedMessages = turn.messages;
|
|
@@ -3161,11 +3691,11 @@ async function pipeThrough(stream2, res) {
|
|
|
3161
3691
|
}
|
|
3162
3692
|
}
|
|
3163
3693
|
async function dumpStreamToFile(stream2, dir, name) {
|
|
3164
|
-
const { mkdirSync:
|
|
3165
|
-
const { join:
|
|
3694
|
+
const { mkdirSync: mkdirSync6, createWriteStream: createWriteStream2 } = await import("fs");
|
|
3695
|
+
const { join: join4 } = await import("path");
|
|
3166
3696
|
try {
|
|
3167
|
-
|
|
3168
|
-
const ws = createWriteStream2(
|
|
3697
|
+
mkdirSync6(dir, { recursive: true });
|
|
3698
|
+
const ws = createWriteStream2(join4(dir, name));
|
|
3169
3699
|
const reader = stream2.getReader();
|
|
3170
3700
|
try {
|
|
3171
3701
|
for (; ; ) {
|
|
@@ -3180,11 +3710,28 @@ async function dumpStreamToFile(stream2, dir, name) {
|
|
|
3180
3710
|
} catch {
|
|
3181
3711
|
}
|
|
3182
3712
|
}
|
|
3713
|
+
function deriveTitle(messages) {
|
|
3714
|
+
for (const m of messages) {
|
|
3715
|
+
if (m.role !== "user" || m.contentType !== "text") continue;
|
|
3716
|
+
const clean = (m.text ?? "").replace(/\s+/g, " ").trim();
|
|
3717
|
+
if (clean) return clean.length > 60 ? clean.slice(0, 57) + "\u2026" : clean;
|
|
3718
|
+
}
|
|
3719
|
+
return void 0;
|
|
3720
|
+
}
|
|
3183
3721
|
function sendStats(res) {
|
|
3184
3722
|
const sessions2 = listSessions().map((s) => ({
|
|
3185
3723
|
id: s.id,
|
|
3186
|
-
|
|
3187
|
-
|
|
3724
|
+
protocol: s.meta.protocol,
|
|
3725
|
+
upstream: s.meta.upstreamOrigin,
|
|
3726
|
+
label: s.meta.label,
|
|
3727
|
+
title: s.meta.title,
|
|
3728
|
+
requests: s.stats.requests,
|
|
3729
|
+
contextTokens: s.stats.contextTokens,
|
|
3730
|
+
inputTokens: s.stats.inputTokens,
|
|
3731
|
+
cachedTokens: s.stats.cachedTokens,
|
|
3732
|
+
outputTokens: s.stats.outputTokens,
|
|
3733
|
+
cacheSamples: s.stats.cacheSamples,
|
|
3734
|
+
cacheHitPct: s.stats.cacheSamples > 0 ? Math.round(s.stats.cachedTokens / s.stats.inputTokens * 100) : null,
|
|
3188
3735
|
lastSeen: new Date(s.lastSeen).toISOString()
|
|
3189
3736
|
}));
|
|
3190
3737
|
res.writeHead(200, { "content-type": "application/json" });
|
|
@@ -3237,7 +3784,7 @@ function logMsg(opts, level, msg2) {
|
|
|
3237
3784
|
import { readFile, writeFile, mkdir, access, constants, rm } from "fs/promises";
|
|
3238
3785
|
import { execFile } from "child_process";
|
|
3239
3786
|
import path4 from "path";
|
|
3240
|
-
import { fileURLToPath } from "url";
|
|
3787
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3241
3788
|
var REGISTRY_BASE = "https://registry.npmjs.org";
|
|
3242
3789
|
var CHECK_INTERVAL_MS = 3 * 60 * 1e3;
|
|
3243
3790
|
var THROTTLE_FILE = path4.join(cacheDir(), ".update-check");
|
|
@@ -3276,7 +3823,7 @@ async function writeLastCheck(ts) {
|
|
|
3276
3823
|
}
|
|
3277
3824
|
}
|
|
3278
3825
|
async function findInstallDir(packageName) {
|
|
3279
|
-
let dir = path4.dirname(
|
|
3826
|
+
let dir = path4.dirname(fileURLToPath2(import.meta.url));
|
|
3280
3827
|
for (; ; ) {
|
|
3281
3828
|
try {
|
|
3282
3829
|
const pkg = JSON.parse(await readFile(path4.join(dir, "package.json"), "utf-8"));
|
|
@@ -3513,23 +4060,23 @@ function startAutoUpdate(opts) {
|
|
|
3513
4060
|
}
|
|
3514
4061
|
|
|
3515
4062
|
// src/cli.ts
|
|
3516
|
-
import { readFileSync as
|
|
3517
|
-
import { fileURLToPath as
|
|
4063
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
4064
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3518
4065
|
import path5 from "path";
|
|
3519
4066
|
var VERSION = (() => {
|
|
3520
4067
|
try {
|
|
3521
|
-
const here =
|
|
4068
|
+
const here = fileURLToPath3(import.meta.url);
|
|
3522
4069
|
const pkg = path5.join(path5.dirname(here), "..", "package.json");
|
|
3523
|
-
return JSON.parse(
|
|
4070
|
+
return JSON.parse(readFileSync4(pkg, "utf8")).version ?? "dev";
|
|
3524
4071
|
} catch {
|
|
3525
4072
|
return "dev";
|
|
3526
4073
|
}
|
|
3527
4074
|
})();
|
|
3528
4075
|
var PACKAGE_NAME = (() => {
|
|
3529
4076
|
try {
|
|
3530
|
-
const here =
|
|
4077
|
+
const here = fileURLToPath3(import.meta.url);
|
|
3531
4078
|
const pkg = path5.join(path5.dirname(here), "..", "package.json");
|
|
3532
|
-
return JSON.parse(
|
|
4079
|
+
return JSON.parse(readFileSync4(pkg, "utf8")).name ?? "billion-context";
|
|
3533
4080
|
} catch {
|
|
3534
4081
|
return "billion-context";
|
|
3535
4082
|
}
|
|
@@ -3641,6 +4188,7 @@ async function main() {
|
|
|
3641
4188
|
for (const [k, v] of Object.entries(overrides)) {
|
|
3642
4189
|
if (v !== void 0) process.env[k] = v;
|
|
3643
4190
|
}
|
|
4191
|
+
ensureConfigTemplate();
|
|
3644
4192
|
const opts = loadOptions();
|
|
3645
4193
|
await startServer(opts);
|
|
3646
4194
|
if (opts.autoUpdate) {
|