billion-context 0.1.20 → 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 +64 -29
- package/README.zh-CN.md +50 -56
- package/dist/index.js +575 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -764,7 +764,7 @@ import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as rea
|
|
|
764
764
|
import { createHash as createHash2 } from "crypto";
|
|
765
765
|
import * as path3 from "path";
|
|
766
766
|
import { createInitialState } from "acp-kernel";
|
|
767
|
-
var PERSIST_VERSION =
|
|
767
|
+
var PERSIST_VERSION = 2;
|
|
768
768
|
function mergeState(parsed) {
|
|
769
769
|
const fresh = createInitialState();
|
|
770
770
|
return {
|
|
@@ -852,8 +852,9 @@ var SessionStore = class {
|
|
|
852
852
|
try {
|
|
853
853
|
const parsed = JSON.parse(await fs.readFile(full, "utf8"));
|
|
854
854
|
if (!isValidRecord(parsed)) continue;
|
|
855
|
-
const
|
|
856
|
-
const
|
|
855
|
+
const pm = parsed.meta ?? {};
|
|
856
|
+
const proto = pm.protocol ?? parsed.protocol;
|
|
857
|
+
const origin = pm.upstreamOrigin ?? parsed.upstreamOrigin;
|
|
857
858
|
const expectedNamespaced = path3.basename(relPathFor(parsed.id, proto, origin));
|
|
858
859
|
const expectedLegacy = legacyFileNameFor(parsed.id);
|
|
859
860
|
if (name !== expectedNamespaced && name !== expectedLegacy) {
|
|
@@ -907,7 +908,7 @@ var SessionStore = class {
|
|
|
907
908
|
async writeNow(session) {
|
|
908
909
|
if (!this.enabled) return;
|
|
909
910
|
const record = buildRecord(session);
|
|
910
|
-
const file = this.filePath(session.id, session.protocol, session.upstreamOrigin);
|
|
911
|
+
const file = this.filePath(session.id, session.meta.protocol, session.meta.upstreamOrigin);
|
|
911
912
|
try {
|
|
912
913
|
await fs.mkdir(path3.dirname(file), { recursive: true });
|
|
913
914
|
} catch (e) {
|
|
@@ -941,7 +942,7 @@ var SessionStore = class {
|
|
|
941
942
|
this.timers.delete(session.id);
|
|
942
943
|
}
|
|
943
944
|
const record = buildRecord(session);
|
|
944
|
-
const file = this.filePath(session.id, session.protocol, session.upstreamOrigin);
|
|
945
|
+
const file = this.filePath(session.id, session.meta.protocol, session.meta.upstreamOrigin);
|
|
945
946
|
try {
|
|
946
947
|
mkdirSync3(path3.dirname(file), { recursive: true });
|
|
947
948
|
} catch (e) {
|
|
@@ -995,13 +996,12 @@ function buildRecord(session) {
|
|
|
995
996
|
version: PERSIST_VERSION,
|
|
996
997
|
savedAt: Date.now(),
|
|
997
998
|
id: session.id,
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
requests: session.requests,
|
|
1002
|
-
tokensSaved: session.tokensSaved,
|
|
999
|
+
meta: { ...session.meta },
|
|
1000
|
+
stats: { ...session.stats },
|
|
1001
|
+
metadata: { ...session.metadata },
|
|
1003
1002
|
state: session.state,
|
|
1004
|
-
blockContents: Object.fromEntries(session.blockContents)
|
|
1003
|
+
blockContents: Object.fromEntries(session.blockContents),
|
|
1004
|
+
createdAt: session.createdAt
|
|
1005
1005
|
};
|
|
1006
1006
|
}
|
|
1007
1007
|
function buildSession(parsed) {
|
|
@@ -1009,15 +1009,29 @@ function buildSession(parsed) {
|
|
|
1009
1009
|
for (const [bid, content] of Object.entries(parsed.blockContents ?? {})) {
|
|
1010
1010
|
if (content && typeof content === "object") blockContents.set(bid, content);
|
|
1011
1011
|
}
|
|
1012
|
+
const meta = parsed.meta ?? {};
|
|
1013
|
+
const stats = parsed.stats ?? {};
|
|
1012
1014
|
return {
|
|
1013
1015
|
id: parsed.id,
|
|
1014
|
-
|
|
1015
|
-
|
|
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 ?? {},
|
|
1016
1032
|
state: mergeState(parsed.state),
|
|
1017
1033
|
createdAt: parsed.createdAt ?? Date.now(),
|
|
1018
1034
|
lastSeen: Date.now(),
|
|
1019
|
-
requests: parsed.requests ?? 0,
|
|
1020
|
-
tokensSaved: parsed.tokensSaved ?? 0,
|
|
1021
1035
|
blockContents,
|
|
1022
1036
|
inFlight: 0,
|
|
1023
1037
|
persisted: true
|
|
@@ -1082,8 +1096,9 @@ function getSession(id, meta) {
|
|
|
1082
1096
|
const existing = sessions.get(id);
|
|
1083
1097
|
if (existing) {
|
|
1084
1098
|
existing.lastSeen = Date.now();
|
|
1085
|
-
if (meta?.protocol && !existing.protocol) existing.protocol = meta.protocol;
|
|
1086
|
-
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;
|
|
1087
1102
|
return existing;
|
|
1088
1103
|
}
|
|
1089
1104
|
const store = getStore();
|
|
@@ -1097,13 +1112,12 @@ function getSession(id, meta) {
|
|
|
1097
1112
|
if (sessions.size >= MAX_SESSIONS) evictOldest();
|
|
1098
1113
|
const session = {
|
|
1099
1114
|
id,
|
|
1100
|
-
protocol: meta?.protocol,
|
|
1101
|
-
|
|
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: {},
|
|
1102
1118
|
state: createInitialState2(),
|
|
1103
1119
|
createdAt: Date.now(),
|
|
1104
1120
|
lastSeen: Date.now(),
|
|
1105
|
-
requests: 0,
|
|
1106
|
-
tokensSaved: 0,
|
|
1107
1121
|
blockContents: /* @__PURE__ */ new Map(),
|
|
1108
1122
|
inFlight: 0,
|
|
1109
1123
|
persisted: false
|
|
@@ -1457,8 +1471,31 @@ function routeEvent(ev, blocks, ctx, markConverted, markRealToolUse, getConverte
|
|
|
1457
1471
|
return emitEvent(ev);
|
|
1458
1472
|
}
|
|
1459
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
|
+
}
|
|
1460
1481
|
return getConverted() ? NOOP : emitEvent(ev);
|
|
1461
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
|
+
}
|
|
1462
1499
|
if (t === "message_stop") {
|
|
1463
1500
|
return getConverted() ? NOOP : emitEvent(ev);
|
|
1464
1501
|
}
|
|
@@ -1588,6 +1625,447 @@ function rewriteJsonResponse(body, ctx) {
|
|
|
1588
1625
|
return body;
|
|
1589
1626
|
}
|
|
1590
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
|
+
|
|
1591
2069
|
// src/orphan-gc.ts
|
|
1592
2070
|
var ORPHAN_THRESHOLD = 3;
|
|
1593
2071
|
var orphanStreaks = /* @__PURE__ */ new WeakMap();
|
|
@@ -1625,8 +2103,8 @@ import {
|
|
|
1625
2103
|
collectBlockContent as collectBlockContent2,
|
|
1626
2104
|
deactivateBlock
|
|
1627
2105
|
} from "acp-kernel";
|
|
1628
|
-
import { mkdirSync as
|
|
1629
|
-
import { dirname as
|
|
2106
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
2107
|
+
import { dirname as dirname4, join as join3 } from "path";
|
|
1630
2108
|
import { tmpdir } from "os";
|
|
1631
2109
|
function resolveDecompress(args, ctx) {
|
|
1632
2110
|
const rawBlockId = args.blockId;
|
|
@@ -1655,11 +2133,11 @@ function resolveDecompress(args, ctx) {
|
|
|
1655
2133
|
}
|
|
1656
2134
|
const header = `[Restored block ${blockId} \u2014 ${count} item(s)${full ? ", full" : ""}]`;
|
|
1657
2135
|
const safeBlockId = blockId.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
1658
|
-
const outPath = body.length > 1e4 ?
|
|
2136
|
+
const outPath = body.length > 1e4 ? join3(tmpdir(), `acp-decompress-${safeBlockId}-${Date.now()}.txt`) : null;
|
|
1659
2137
|
if (outPath) {
|
|
1660
2138
|
try {
|
|
1661
|
-
|
|
1662
|
-
|
|
2139
|
+
mkdirSync5(dirname4(outPath), { recursive: true });
|
|
2140
|
+
writeFileSync4(outPath, body, "utf8");
|
|
1663
2141
|
return `${header}
|
|
1664
2142
|
Content (${body.length} chars) written to: ${outPath}
|
|
1665
2143
|
Use the read tool to access it.`;
|
|
@@ -1942,6 +2420,10 @@ async function* compressLoopStream(initialUpstream, ctx, requestBody, requestOpt
|
|
|
1942
2420
|
if (typeof prompt === "number") {
|
|
1943
2421
|
const ch = typeof cached === "number" ? cached : 0;
|
|
1944
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;
|
|
1945
2427
|
}
|
|
1946
2428
|
}
|
|
1947
2429
|
if (!hasOnlyProxy) {
|
|
@@ -2342,6 +2824,12 @@ async function* compressLoopResponsesStream(initialUpstream, ctx, requestBody, r
|
|
|
2342
2824
|
const cached = inDet?.cached_tokens ?? prDet?.cached_tokens ?? "?";
|
|
2343
2825
|
const out = usage.output_tokens ?? "?";
|
|
2344
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
|
+
}
|
|
2345
2833
|
}
|
|
2346
2834
|
}
|
|
2347
2835
|
}
|
|
@@ -2667,9 +3155,10 @@ async function startServer(opts) {
|
|
|
2667
3155
|
}
|
|
2668
3156
|
});
|
|
2669
3157
|
server.listen(opts.port, opts.host, () => {
|
|
3158
|
+
const displayHost = opts.host === "0.0.0.0" ? "localhost" : opts.host;
|
|
2670
3159
|
log2(
|
|
2671
3160
|
"info",
|
|
2672
|
-
`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/`
|
|
2673
3162
|
);
|
|
2674
3163
|
});
|
|
2675
3164
|
server.on("error", (err) => {
|
|
@@ -2721,6 +3210,14 @@ async function handle(req, res, opts, core, config, log2) {
|
|
|
2721
3210
|
res.end(JSON.stringify({ ok: true, upstream: opts.upstream }));
|
|
2722
3211
|
return;
|
|
2723
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);
|
|
2724
3221
|
let bodyBuffer;
|
|
2725
3222
|
try {
|
|
2726
3223
|
bodyBuffer = await readBody(req);
|
|
@@ -2764,8 +3261,9 @@ async function handle(req, res, opts, core, config, log2) {
|
|
|
2764
3261
|
const sessionHeader = headerValue(req, opts.sessionHeader);
|
|
2765
3262
|
const conversation = protocol === "anthropic" ? conversationSignalAnthropic(parsed, sessionHeader) : protocol === "openai" ? conversationSignalOpenai(parsed, sessionHeader) : conversationSignalResponses(parsed, sessionHeader);
|
|
2766
3263
|
const sessionId = deriveSessionId(req.headers, protocol, upstreamOrigin, conversation);
|
|
2767
|
-
const session = getSession(sessionId, { protocol, upstreamOrigin });
|
|
2768
3264
|
const affinity = affinityToken(req.headers, conversation);
|
|
3265
|
+
const clientLabel = clientConversationHeader(req.headers);
|
|
3266
|
+
const session = getSession(sessionId, { protocol, upstreamOrigin, label: clientLabel ?? void 0 });
|
|
2769
3267
|
await withSessionLock(session, async () => {
|
|
2770
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);
|
|
2771
3269
|
acquireInFlight(session);
|
|
@@ -2811,7 +3309,7 @@ function diagNudge(turn, sessionId, tokenCount, limit) {
|
|
|
2811
3309
|
function prepareAnthropic(parsed, req, opts, core, config, log2, session) {
|
|
2812
3310
|
const sessionId = session.id;
|
|
2813
3311
|
const stream2 = parsed.stream === true;
|
|
2814
|
-
session.requests
|
|
3312
|
+
++session.stats.requests;
|
|
2815
3313
|
let processedMessages = [];
|
|
2816
3314
|
let rebuiltMessages = parsed.messages;
|
|
2817
3315
|
let systemOut = parsed.system;
|
|
@@ -2821,6 +3319,11 @@ function prepareAnthropic(parsed, req, opts, core, config, log2, session) {
|
|
|
2821
3319
|
const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
|
|
2822
3320
|
const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: "text-only" });
|
|
2823
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
|
+
}
|
|
2824
3327
|
log2("info", diagTagSummary(turn.messages, sessionId, "text-only"));
|
|
2825
3328
|
log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
|
|
2826
3329
|
processedMessages = turn.messages;
|
|
@@ -2850,7 +3353,7 @@ function prepareAnthropic(parsed, req, opts, core, config, log2, session) {
|
|
|
2850
3353
|
function prepareOpenai(parsed, req, opts, core, config, log2, session) {
|
|
2851
3354
|
const sessionId = session.id;
|
|
2852
3355
|
const stream2 = parsed.stream === true;
|
|
2853
|
-
session.requests
|
|
3356
|
+
++session.stats.requests;
|
|
2854
3357
|
let processedMessages = [];
|
|
2855
3358
|
let rebuiltMessages = parsed.messages;
|
|
2856
3359
|
let toolsOut = parsed.tools;
|
|
@@ -2862,6 +3365,11 @@ function prepareOpenai(parsed, req, opts, core, config, log2, session) {
|
|
|
2862
3365
|
const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
|
|
2863
3366
|
const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: "text-only" });
|
|
2864
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
|
+
}
|
|
2865
3373
|
log2("info", diagTagSummary(turn.messages, sessionId, "text-only"));
|
|
2866
3374
|
log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
|
|
2867
3375
|
processedMessages = turn.messages;
|
|
@@ -2893,7 +3401,7 @@ function prepareOpenai(parsed, req, opts, core, config, log2, session) {
|
|
|
2893
3401
|
function prepareResponses(parsed, req, opts, core, config, log2, session) {
|
|
2894
3402
|
const sessionId = session.id;
|
|
2895
3403
|
const stream2 = parsed.stream === true;
|
|
2896
|
-
session.requests
|
|
3404
|
+
++session.stats.requests;
|
|
2897
3405
|
let processedMessages = [];
|
|
2898
3406
|
let rebuiltInput = parsed.input;
|
|
2899
3407
|
let toolsOut = parsed.tools;
|
|
@@ -2906,6 +3414,11 @@ function prepareResponses(parsed, req, opts, core, config, log2, session) {
|
|
|
2906
3414
|
const tokenCount = estimateTokensFast3(msgs.map((m) => m.text ?? "").join("\n"));
|
|
2907
3415
|
const turn = core.processTurn({ messages: msgs, state: session.state, config, tokenCount, renderTags: process.env.ACP_RENDER_NONE ? "none" : "text-only" });
|
|
2908
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
|
+
}
|
|
2909
3422
|
log2("info", diagTagSummary(turn.messages, sessionId, "text-only"));
|
|
2910
3423
|
log2("info", diagNudge(turn, sessionId, tokenCount, config.modelContextLimit));
|
|
2911
3424
|
processedMessages = turn.messages;
|
|
@@ -3178,11 +3691,11 @@ async function pipeThrough(stream2, res) {
|
|
|
3178
3691
|
}
|
|
3179
3692
|
}
|
|
3180
3693
|
async function dumpStreamToFile(stream2, dir, name) {
|
|
3181
|
-
const { mkdirSync:
|
|
3182
|
-
const { join:
|
|
3694
|
+
const { mkdirSync: mkdirSync6, createWriteStream: createWriteStream2 } = await import("fs");
|
|
3695
|
+
const { join: join4 } = await import("path");
|
|
3183
3696
|
try {
|
|
3184
|
-
|
|
3185
|
-
const ws = createWriteStream2(
|
|
3697
|
+
mkdirSync6(dir, { recursive: true });
|
|
3698
|
+
const ws = createWriteStream2(join4(dir, name));
|
|
3186
3699
|
const reader = stream2.getReader();
|
|
3187
3700
|
try {
|
|
3188
3701
|
for (; ; ) {
|
|
@@ -3197,11 +3710,28 @@ async function dumpStreamToFile(stream2, dir, name) {
|
|
|
3197
3710
|
} catch {
|
|
3198
3711
|
}
|
|
3199
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
|
+
}
|
|
3200
3721
|
function sendStats(res) {
|
|
3201
3722
|
const sessions2 = listSessions().map((s) => ({
|
|
3202
3723
|
id: s.id,
|
|
3203
|
-
|
|
3204
|
-
|
|
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,
|
|
3205
3735
|
lastSeen: new Date(s.lastSeen).toISOString()
|
|
3206
3736
|
}));
|
|
3207
3737
|
res.writeHead(200, { "content-type": "application/json" });
|
|
@@ -3254,7 +3784,7 @@ function logMsg(opts, level, msg2) {
|
|
|
3254
3784
|
import { readFile, writeFile, mkdir, access, constants, rm } from "fs/promises";
|
|
3255
3785
|
import { execFile } from "child_process";
|
|
3256
3786
|
import path4 from "path";
|
|
3257
|
-
import { fileURLToPath } from "url";
|
|
3787
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3258
3788
|
var REGISTRY_BASE = "https://registry.npmjs.org";
|
|
3259
3789
|
var CHECK_INTERVAL_MS = 3 * 60 * 1e3;
|
|
3260
3790
|
var THROTTLE_FILE = path4.join(cacheDir(), ".update-check");
|
|
@@ -3293,7 +3823,7 @@ async function writeLastCheck(ts) {
|
|
|
3293
3823
|
}
|
|
3294
3824
|
}
|
|
3295
3825
|
async function findInstallDir(packageName) {
|
|
3296
|
-
let dir = path4.dirname(
|
|
3826
|
+
let dir = path4.dirname(fileURLToPath2(import.meta.url));
|
|
3297
3827
|
for (; ; ) {
|
|
3298
3828
|
try {
|
|
3299
3829
|
const pkg = JSON.parse(await readFile(path4.join(dir, "package.json"), "utf-8"));
|
|
@@ -3530,23 +4060,23 @@ function startAutoUpdate(opts) {
|
|
|
3530
4060
|
}
|
|
3531
4061
|
|
|
3532
4062
|
// src/cli.ts
|
|
3533
|
-
import { readFileSync as
|
|
3534
|
-
import { fileURLToPath as
|
|
4063
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
4064
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3535
4065
|
import path5 from "path";
|
|
3536
4066
|
var VERSION = (() => {
|
|
3537
4067
|
try {
|
|
3538
|
-
const here =
|
|
4068
|
+
const here = fileURLToPath3(import.meta.url);
|
|
3539
4069
|
const pkg = path5.join(path5.dirname(here), "..", "package.json");
|
|
3540
|
-
return JSON.parse(
|
|
4070
|
+
return JSON.parse(readFileSync4(pkg, "utf8")).version ?? "dev";
|
|
3541
4071
|
} catch {
|
|
3542
4072
|
return "dev";
|
|
3543
4073
|
}
|
|
3544
4074
|
})();
|
|
3545
4075
|
var PACKAGE_NAME = (() => {
|
|
3546
4076
|
try {
|
|
3547
|
-
const here =
|
|
4077
|
+
const here = fileURLToPath3(import.meta.url);
|
|
3548
4078
|
const pkg = path5.join(path5.dirname(here), "..", "package.json");
|
|
3549
|
-
return JSON.parse(
|
|
4079
|
+
return JSON.parse(readFileSync4(pkg, "utf8")).name ?? "billion-context";
|
|
3550
4080
|
} catch {
|
|
3551
4081
|
return "billion-context";
|
|
3552
4082
|
}
|