@dianshuv/copilot-api 0.6.2 → 0.7.0
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 +2 -1
- package/dist/main.mjs +800 -688
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -4,14 +4,14 @@ import consola from "consola";
|
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path, { dirname, join } from "node:path";
|
|
7
|
+
import { getProxyForUrl } from "proxy-from-env";
|
|
8
|
+
import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
|
|
7
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
8
10
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
9
11
|
import clipboard from "clipboardy";
|
|
10
12
|
import { serve } from "srvx";
|
|
11
13
|
import invariant from "tiny-invariant";
|
|
12
14
|
import { PostHog } from "posthog-node";
|
|
13
|
-
import { getProxyForUrl } from "proxy-from-env";
|
|
14
|
-
import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
|
|
15
15
|
import { execSync } from "node:child_process";
|
|
16
16
|
import process$1 from "node:process";
|
|
17
17
|
import pc from "picocolors";
|
|
@@ -32,12 +32,90 @@ async function ensurePaths() {
|
|
|
32
32
|
await ensureFile(PATHS.GITHUB_TOKEN_PATH);
|
|
33
33
|
}
|
|
34
34
|
async function ensureFile(filePath) {
|
|
35
|
+
const isWindows = process.platform === "win32";
|
|
35
36
|
try {
|
|
36
37
|
await fs.access(filePath, fs.constants.W_OK);
|
|
37
|
-
if (
|
|
38
|
+
if (!isWindows) {
|
|
39
|
+
if (((await fs.stat(filePath)).mode & 511) !== 384) await fs.chmod(filePath, 384);
|
|
40
|
+
}
|
|
38
41
|
} catch {
|
|
39
42
|
await fs.writeFile(filePath, "");
|
|
40
|
-
await fs.chmod(filePath, 384);
|
|
43
|
+
if (!isWindows) await fs.chmod(filePath, 384);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/lib/proxy.ts
|
|
49
|
+
/**
|
|
50
|
+
* Custom dispatcher that routes requests through proxies based on environment variables.
|
|
51
|
+
* Extends Agent to properly inherit the Dispatcher interface.
|
|
52
|
+
*/
|
|
53
|
+
var ProxyDispatcher = class extends Agent {
|
|
54
|
+
proxies = /* @__PURE__ */ new Map();
|
|
55
|
+
dispatch(options, handler) {
|
|
56
|
+
try {
|
|
57
|
+
const origin = this.getOriginUrl(options.origin);
|
|
58
|
+
const proxyUrl = this.getProxyUrl(origin);
|
|
59
|
+
if (!proxyUrl) {
|
|
60
|
+
consola.debug(`HTTP proxy bypass: ${origin.hostname}`);
|
|
61
|
+
return super.dispatch(options, handler);
|
|
62
|
+
}
|
|
63
|
+
const agent = this.getOrCreateProxyAgent(proxyUrl);
|
|
64
|
+
consola.debug(`HTTP proxy route: ${origin.hostname} via ${this.formatProxyLabel(proxyUrl)}`);
|
|
65
|
+
return agent.dispatch(options, handler);
|
|
66
|
+
} catch {
|
|
67
|
+
return super.dispatch(options, handler);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
getOriginUrl(origin) {
|
|
71
|
+
return typeof origin === "string" ? new URL(origin) : origin;
|
|
72
|
+
}
|
|
73
|
+
getProxyUrl(origin) {
|
|
74
|
+
const raw = getProxyForUrl(origin.toString());
|
|
75
|
+
return raw && raw.length > 0 ? raw : void 0;
|
|
76
|
+
}
|
|
77
|
+
getOrCreateProxyAgent(proxyUrl) {
|
|
78
|
+
let agent = this.proxies.get(proxyUrl);
|
|
79
|
+
if (!agent) {
|
|
80
|
+
agent = new ProxyAgent(proxyUrl);
|
|
81
|
+
this.proxies.set(proxyUrl, agent);
|
|
82
|
+
}
|
|
83
|
+
return agent;
|
|
84
|
+
}
|
|
85
|
+
formatProxyLabel(proxyUrl) {
|
|
86
|
+
try {
|
|
87
|
+
const u = new URL(proxyUrl);
|
|
88
|
+
return `${u.protocol}//${u.host}`;
|
|
89
|
+
} catch {
|
|
90
|
+
return proxyUrl;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async close() {
|
|
94
|
+
await super.close();
|
|
95
|
+
await Promise.all([...this.proxies.values()].map((p) => p.close()));
|
|
96
|
+
this.proxies.clear();
|
|
97
|
+
}
|
|
98
|
+
destroy(errOrCallback, callback) {
|
|
99
|
+
for (const agent of this.proxies.values()) if (typeof errOrCallback === "function") agent.destroy(errOrCallback);
|
|
100
|
+
else if (callback) agent.destroy(errOrCallback ?? null, callback);
|
|
101
|
+
else agent.destroy(errOrCallback ?? null).catch(() => {});
|
|
102
|
+
this.proxies.clear();
|
|
103
|
+
if (typeof errOrCallback === "function") {
|
|
104
|
+
super.destroy(errOrCallback);
|
|
105
|
+
return;
|
|
106
|
+
} else if (callback) {
|
|
107
|
+
super.destroy(errOrCallback ?? null, callback);
|
|
108
|
+
return;
|
|
109
|
+
} else return super.destroy(errOrCallback ?? null);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
function initProxyFromEnv() {
|
|
113
|
+
if (typeof Bun !== "undefined") return;
|
|
114
|
+
try {
|
|
115
|
+
setGlobalDispatcher(new ProxyDispatcher());
|
|
116
|
+
consola.debug("HTTP proxy configured from environment (per-URL)");
|
|
117
|
+
} catch (err) {
|
|
118
|
+
consola.debug("Proxy setup skipped:", err);
|
|
41
119
|
}
|
|
42
120
|
}
|
|
43
121
|
|
|
@@ -51,7 +129,10 @@ const state = {
|
|
|
51
129
|
autoTruncate: true,
|
|
52
130
|
compressToolResults: false,
|
|
53
131
|
redirectAnthropic: false,
|
|
54
|
-
|
|
132
|
+
stripServerTools: false,
|
|
133
|
+
contextEditingMode: "off",
|
|
134
|
+
normalizeResponsesCallIds: true,
|
|
135
|
+
historyMinEntries: 50,
|
|
55
136
|
staleRequestMaxAge: 600,
|
|
56
137
|
timezoneOffset: 8,
|
|
57
138
|
shutdownGracefulWait: 60,
|
|
@@ -109,10 +190,6 @@ const GITHUB_APP_SCOPES = ["read:user"].join(" ");
|
|
|
109
190
|
|
|
110
191
|
//#endregion
|
|
111
192
|
//#region src/lib/auto-truncate-common.ts
|
|
112
|
-
/**
|
|
113
|
-
* Common types and configuration for auto-truncate modules.
|
|
114
|
-
* Shared between OpenAI and Anthropic format handlers.
|
|
115
|
-
*/
|
|
116
193
|
const DEFAULT_AUTO_TRUNCATE_CONFIG = {
|
|
117
194
|
safetyMarginPercent: 2,
|
|
118
195
|
maxRequestBodyBytes: Infinity,
|
|
@@ -153,6 +230,99 @@ function onTokenLimitExceeded(modelId, reportedLimit) {
|
|
|
153
230
|
function getEffectiveTokenLimit(modelId) {
|
|
154
231
|
return dynamicTokenLimits.get(modelId) ?? null;
|
|
155
232
|
}
|
|
233
|
+
const LARGE_TOOL_RESULT_THRESHOLD = 1e4;
|
|
234
|
+
const COMPRESSED_SUMMARY_LENGTH = 500;
|
|
235
|
+
function getMessageBytes(msg) {
|
|
236
|
+
return JSON.stringify(msg).length;
|
|
237
|
+
}
|
|
238
|
+
function compressToolResultContent(content) {
|
|
239
|
+
if (content.length <= LARGE_TOOL_RESULT_THRESHOLD) return content;
|
|
240
|
+
const halfLen = Math.floor(COMPRESSED_SUMMARY_LENGTH / 2);
|
|
241
|
+
const start = content.slice(0, halfLen);
|
|
242
|
+
const end = content.slice(-halfLen);
|
|
243
|
+
return `${start}\n\n[... ${(content.length - COMPRESSED_SUMMARY_LENGTH).toLocaleString()} characters omitted for brevity ...]\n\n${end}`;
|
|
244
|
+
}
|
|
245
|
+
function calculateLimits(model, config, defaultContextWindow) {
|
|
246
|
+
const rawTokenLimit = getEffectiveTokenLimit(model.id) ?? model.capabilities?.limits?.max_context_window_tokens ?? model.capabilities?.limits?.max_prompt_tokens ?? defaultContextWindow;
|
|
247
|
+
return {
|
|
248
|
+
tokenLimit: Math.floor(rawTokenLimit * (1 - config.safetyMarginPercent / 100)),
|
|
249
|
+
byteLimit: getEffectiveByteLimitBytes()
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function ensureStartsWithUser(messages, logTag) {
|
|
253
|
+
let startIndex = 0;
|
|
254
|
+
while (startIndex < messages.length && messages[startIndex].role !== "user") startIndex++;
|
|
255
|
+
if (startIndex > 0) consola.debug(`[AutoTruncate:${logTag}] Skipped ${startIndex} leading non-user messages`);
|
|
256
|
+
return messages.slice(startIndex);
|
|
257
|
+
}
|
|
258
|
+
function findOptimalPreserveIndex(params) {
|
|
259
|
+
const { messages, systemBytes, systemTokens, payloadOverhead, tokenLimit, byteLimit, estimateTokens } = params;
|
|
260
|
+
if (messages.length === 0) return 0;
|
|
261
|
+
const markerBytes = 200;
|
|
262
|
+
const availableTokens = tokenLimit - systemTokens - 50;
|
|
263
|
+
const availableBytes = byteLimit - payloadOverhead - systemBytes - markerBytes;
|
|
264
|
+
if (availableTokens <= 0 || availableBytes <= 0) return messages.length;
|
|
265
|
+
const n = messages.length;
|
|
266
|
+
const cumTokens = Array.from({ length: n + 1 }, () => 0);
|
|
267
|
+
const cumBytes = Array.from({ length: n + 1 }, () => 0);
|
|
268
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
269
|
+
const msg = messages[i];
|
|
270
|
+
cumTokens[i] = cumTokens[i + 1] + estimateTokens(msg);
|
|
271
|
+
cumBytes[i] = cumBytes[i + 1] + getMessageBytes(msg) + 1;
|
|
272
|
+
}
|
|
273
|
+
let left = 0;
|
|
274
|
+
let right = n;
|
|
275
|
+
while (left < right) {
|
|
276
|
+
const mid = left + right >>> 1;
|
|
277
|
+
if (cumTokens[mid] <= availableTokens && cumBytes[mid] <= availableBytes) right = mid;
|
|
278
|
+
else left = mid + 1;
|
|
279
|
+
}
|
|
280
|
+
return left;
|
|
281
|
+
}
|
|
282
|
+
function generateRemovedMessagesSummary(removedMessages, getToolCallNames) {
|
|
283
|
+
const toolCalls = [];
|
|
284
|
+
let userMessageCount = 0;
|
|
285
|
+
let assistantMessageCount = 0;
|
|
286
|
+
for (const msg of removedMessages) {
|
|
287
|
+
if (msg.role === "user") userMessageCount++;
|
|
288
|
+
else if (msg.role === "assistant") assistantMessageCount++;
|
|
289
|
+
for (const name of getToolCallNames(msg)) toolCalls.push(name);
|
|
290
|
+
}
|
|
291
|
+
const parts = [];
|
|
292
|
+
if (userMessageCount > 0 || assistantMessageCount > 0) {
|
|
293
|
+
const breakdown = [];
|
|
294
|
+
if (userMessageCount > 0) breakdown.push(`${userMessageCount} user`);
|
|
295
|
+
if (assistantMessageCount > 0) breakdown.push(`${assistantMessageCount} assistant`);
|
|
296
|
+
parts.push(`Messages: ${breakdown.join(", ")}`);
|
|
297
|
+
}
|
|
298
|
+
if (toolCalls.length > 0) {
|
|
299
|
+
const uniqueTools = [...new Set(toolCalls)];
|
|
300
|
+
const displayTools = uniqueTools.length > 5 ? [...uniqueTools.slice(0, 5), `+${uniqueTools.length - 5} more`] : uniqueTools;
|
|
301
|
+
parts.push(`Tools used: ${displayTools.join(", ")}`);
|
|
302
|
+
}
|
|
303
|
+
return parts.join(". ");
|
|
304
|
+
}
|
|
305
|
+
function findCompressThreshold(messages, tokenLimit, byteLimit, preservePercent, estimateTokens) {
|
|
306
|
+
const n = messages.length;
|
|
307
|
+
const cumTokens = Array.from({ length: n + 1 }, () => 0);
|
|
308
|
+
const cumBytes = Array.from({ length: n + 1 }, () => 0);
|
|
309
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
310
|
+
const msg = messages[i];
|
|
311
|
+
cumTokens[i] = cumTokens[i + 1] + estimateTokens(msg);
|
|
312
|
+
cumBytes[i] = cumBytes[i + 1] + getMessageBytes(msg) + 1;
|
|
313
|
+
}
|
|
314
|
+
const preserveTokenLimit = Math.floor(tokenLimit * preservePercent);
|
|
315
|
+
const preserveByteLimit = Math.floor(byteLimit * preservePercent);
|
|
316
|
+
let thresholdIndex = n;
|
|
317
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
318
|
+
if (cumTokens[i] > preserveTokenLimit || cumBytes[i] > preserveByteLimit) {
|
|
319
|
+
thresholdIndex = i + 1;
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
thresholdIndex = i;
|
|
323
|
+
}
|
|
324
|
+
return thresholdIndex;
|
|
325
|
+
}
|
|
156
326
|
|
|
157
327
|
//#endregion
|
|
158
328
|
//#region src/lib/error.ts
|
|
@@ -350,6 +520,9 @@ const sleep = (ms) => new Promise((resolve) => {
|
|
|
350
520
|
setTimeout(resolve, ms);
|
|
351
521
|
});
|
|
352
522
|
const isNullish = (value) => value === null || value === void 0;
|
|
523
|
+
function findModelById(modelId) {
|
|
524
|
+
return state.models?.data.find((m) => m.id === modelId);
|
|
525
|
+
}
|
|
353
526
|
async function cacheModels() {
|
|
354
527
|
state.models = await getModels();
|
|
355
528
|
}
|
|
@@ -482,6 +655,7 @@ async function runAuth(options) {
|
|
|
482
655
|
consola.info("Verbose logging enabled");
|
|
483
656
|
}
|
|
484
657
|
state.showToken = options.showToken;
|
|
658
|
+
initProxyFromEnv();
|
|
485
659
|
await ensurePaths();
|
|
486
660
|
await setupGitHubToken({ force: true });
|
|
487
661
|
consola.success("GitHub token written to", PATHS.GITHUB_TOKEN_PATH);
|
|
@@ -528,6 +702,7 @@ const checkUsage = defineCommand({
|
|
|
528
702
|
description: "Show current GitHub Copilot usage/quota information"
|
|
529
703
|
},
|
|
530
704
|
async run() {
|
|
705
|
+
initProxyFromEnv();
|
|
531
706
|
await ensurePaths();
|
|
532
707
|
await setupGitHubToken();
|
|
533
708
|
try {
|
|
@@ -585,6 +760,7 @@ async function checkTokenExists() {
|
|
|
585
760
|
}
|
|
586
761
|
async function getAccountInfo() {
|
|
587
762
|
try {
|
|
763
|
+
initProxyFromEnv();
|
|
588
764
|
await ensurePaths();
|
|
589
765
|
await setupGitHubToken();
|
|
590
766
|
if (!state.githubToken) return null;
|
|
@@ -673,6 +849,7 @@ const debugModels = defineCommand({
|
|
|
673
849
|
},
|
|
674
850
|
async run({ args }) {
|
|
675
851
|
state.accountType = args["account-type"];
|
|
852
|
+
initProxyFromEnv();
|
|
676
853
|
await ensurePaths();
|
|
677
854
|
if (args["github-token"]) {
|
|
678
855
|
state.githubToken = args["github-token"];
|
|
@@ -1036,7 +1213,7 @@ const patchClaude = defineCommand({
|
|
|
1036
1213
|
|
|
1037
1214
|
//#endregion
|
|
1038
1215
|
//#region package.json
|
|
1039
|
-
var version = "0.
|
|
1216
|
+
var version = "0.7.0";
|
|
1040
1217
|
|
|
1041
1218
|
//#endregion
|
|
1042
1219
|
//#region src/lib/adaptive-rate-limiter.ts
|
|
@@ -1644,12 +1821,14 @@ const historyState = {
|
|
|
1644
1821
|
maxEntries: 1e3,
|
|
1645
1822
|
sessionTimeoutMs: 1800 * 1e3
|
|
1646
1823
|
};
|
|
1824
|
+
const entryIndex = /* @__PURE__ */ new Map();
|
|
1647
1825
|
function initHistory(enabled, maxEntries) {
|
|
1648
1826
|
historyState.enabled = enabled;
|
|
1649
1827
|
historyState.maxEntries = maxEntries;
|
|
1650
1828
|
historyState.entries = [];
|
|
1651
1829
|
historyState.sessions = /* @__PURE__ */ new Map();
|
|
1652
1830
|
historyState.currentSessionId = enabled ? generateId$1() : "";
|
|
1831
|
+
entryIndex.clear();
|
|
1653
1832
|
}
|
|
1654
1833
|
function isHistoryEnabled() {
|
|
1655
1834
|
return historyState.enabled;
|
|
@@ -1698,6 +1877,7 @@ function recordRequest(endpoint, request) {
|
|
|
1698
1877
|
}
|
|
1699
1878
|
};
|
|
1700
1879
|
historyState.entries.push(entry);
|
|
1880
|
+
entryIndex.set(entry.id, entry);
|
|
1701
1881
|
session.requestCount++;
|
|
1702
1882
|
if (!session.models.includes(request.model)) session.models.push(request.model);
|
|
1703
1883
|
if (request.tools && request.tools.length > 0) {
|
|
@@ -1707,6 +1887,7 @@ function recordRequest(endpoint, request) {
|
|
|
1707
1887
|
while (historyState.maxEntries > 0 && historyState.entries.length > historyState.maxEntries) {
|
|
1708
1888
|
const removed = historyState.entries.shift();
|
|
1709
1889
|
if (removed) {
|
|
1890
|
+
entryIndex.delete(removed.id);
|
|
1710
1891
|
if (historyState.entries.filter((e) => e.sessionId === removed.sessionId).length === 0) historyState.sessions.delete(removed.sessionId);
|
|
1711
1892
|
}
|
|
1712
1893
|
}
|
|
@@ -1721,7 +1902,7 @@ function recordRequest(endpoint, request) {
|
|
|
1721
1902
|
}
|
|
1722
1903
|
function recordResponse(id, response, durationMs) {
|
|
1723
1904
|
if (!historyState.enabled || !id) return;
|
|
1724
|
-
const entry =
|
|
1905
|
+
const entry = entryIndex.get(id);
|
|
1725
1906
|
if (entry) {
|
|
1726
1907
|
entry.response = response;
|
|
1727
1908
|
entry.durationMs = durationMs;
|
|
@@ -1800,7 +1981,7 @@ function getHistory(options = {}) {
|
|
|
1800
1981
|
};
|
|
1801
1982
|
}
|
|
1802
1983
|
function getEntry(id) {
|
|
1803
|
-
return
|
|
1984
|
+
return entryIndex.get(id);
|
|
1804
1985
|
}
|
|
1805
1986
|
function getSessions() {
|
|
1806
1987
|
const sessions = Array.from(historyState.sessions.values()).sort((a, b) => b.lastActivity - a.lastActivity);
|
|
@@ -1819,11 +2000,14 @@ function clearHistory() {
|
|
|
1819
2000
|
historyState.entries = [];
|
|
1820
2001
|
historyState.sessions = /* @__PURE__ */ new Map();
|
|
1821
2002
|
historyState.currentSessionId = generateId$1();
|
|
2003
|
+
entryIndex.clear();
|
|
1822
2004
|
notifyHistoryCleared();
|
|
1823
2005
|
}
|
|
1824
2006
|
function deleteSession(sessionId) {
|
|
1825
2007
|
if (!historyState.sessions.has(sessionId)) return false;
|
|
2008
|
+
const removedEntries = historyState.entries.filter((e) => e.sessionId === sessionId);
|
|
1826
2009
|
historyState.entries = historyState.entries.filter((e) => e.sessionId !== sessionId);
|
|
2010
|
+
for (const e of removedEntries) entryIndex.delete(e.id);
|
|
1827
2011
|
historyState.sessions.delete(sessionId);
|
|
1828
2012
|
if (historyState.currentSessionId === sessionId) historyState.currentSessionId = generateId$1();
|
|
1829
2013
|
notifySessionDeleted(sessionId);
|
|
@@ -1908,6 +2092,27 @@ function getTokenStats() {
|
|
|
1908
2092
|
timeline
|
|
1909
2093
|
};
|
|
1910
2094
|
}
|
|
2095
|
+
function getHistoryEntryCount() {
|
|
2096
|
+
return historyState.entries.length;
|
|
2097
|
+
}
|
|
2098
|
+
function getHistoryMaxEntries() {
|
|
2099
|
+
return historyState.maxEntries;
|
|
2100
|
+
}
|
|
2101
|
+
function setHistoryMaxEntries(max) {
|
|
2102
|
+
historyState.maxEntries = max;
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Evict the oldest `count` entries from the history store.
|
|
2106
|
+
* Returns the actual number of entries evicted.
|
|
2107
|
+
*/
|
|
2108
|
+
function evictOldestEntries(count) {
|
|
2109
|
+
if (count <= 0) return 0;
|
|
2110
|
+
const actual = Math.min(count, historyState.entries.length);
|
|
2111
|
+
const removed = historyState.entries.splice(0, actual);
|
|
2112
|
+
for (const e of removed) entryIndex.delete(e.id);
|
|
2113
|
+
for (const entry of removed) if (!historyState.entries.some((e) => e.sessionId === entry.sessionId)) historyState.sessions.delete(entry.sessionId);
|
|
2114
|
+
return actual;
|
|
2115
|
+
}
|
|
1911
2116
|
function exportHistory(format = "json") {
|
|
1912
2117
|
if (format === "json") return JSON.stringify({
|
|
1913
2118
|
sessions: Array.from(historyState.sessions.values()),
|
|
@@ -1948,6 +2153,91 @@ function exportHistory(format = "json") {
|
|
|
1948
2153
|
return [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
|
|
1949
2154
|
}
|
|
1950
2155
|
|
|
2156
|
+
//#endregion
|
|
2157
|
+
//#region src/lib/history-memory-pressure.ts
|
|
2158
|
+
/**
|
|
2159
|
+
* Memory pressure monitor — proactively evicts old history entries
|
|
2160
|
+
* when heap usage approaches the V8 heap limit, preventing OOM crashes.
|
|
2161
|
+
*
|
|
2162
|
+
* Graduated response:
|
|
2163
|
+
* 75–80% Warning: log only, no eviction
|
|
2164
|
+
* 80–90% High: evict entries, reduce maxEntries by 25%
|
|
2165
|
+
* 90%+ Critical: aggressive eviction, reduce maxEntries by 50%
|
|
2166
|
+
*/
|
|
2167
|
+
const CHECK_INTERVAL_MS = 3e4;
|
|
2168
|
+
const WARN_THRESHOLD = .75;
|
|
2169
|
+
const EVICT_THRESHOLD = .8;
|
|
2170
|
+
const CRITICAL_THRESHOLD = .9;
|
|
2171
|
+
const WARN_LOG_COOLDOWN_MS = 3e5;
|
|
2172
|
+
let resolvedHeapLimit = null;
|
|
2173
|
+
let timer = null;
|
|
2174
|
+
let lastWarningTime = 0;
|
|
2175
|
+
let totalEvictedCount = 0;
|
|
2176
|
+
async function resolveHeapLimit() {
|
|
2177
|
+
if (resolvedHeapLimit !== null) return resolvedHeapLimit;
|
|
2178
|
+
let limit;
|
|
2179
|
+
try {
|
|
2180
|
+
limit = (await import("node:v8")).getHeapStatistics().heap_size_limit;
|
|
2181
|
+
} catch {
|
|
2182
|
+
limit = 512 * 1024 * 1024;
|
|
2183
|
+
}
|
|
2184
|
+
resolvedHeapLimit = limit;
|
|
2185
|
+
return limit;
|
|
2186
|
+
}
|
|
2187
|
+
function formatMB(bytes) {
|
|
2188
|
+
return `${Math.round(bytes / 1024 / 1024)}MB`;
|
|
2189
|
+
}
|
|
2190
|
+
function formatPct(ratio) {
|
|
2191
|
+
return `${Math.round(ratio * 100)}%`;
|
|
2192
|
+
}
|
|
2193
|
+
async function checkMemoryPressure() {
|
|
2194
|
+
const heapLimit = await resolveHeapLimit();
|
|
2195
|
+
const { heapUsed } = process.memoryUsage();
|
|
2196
|
+
const ratio = heapUsed / heapLimit;
|
|
2197
|
+
if (ratio < WARN_THRESHOLD) return;
|
|
2198
|
+
const currentEntries = getHistoryEntryCount();
|
|
2199
|
+
if (currentEntries <= state.historyMinEntries) {
|
|
2200
|
+
if (ratio >= EVICT_THRESHOLD && Date.now() - lastWarningTime > WARN_LOG_COOLDOWN_MS) {
|
|
2201
|
+
lastWarningTime = Date.now();
|
|
2202
|
+
consola.warn(`[memory] Heap ${formatMB(heapUsed)}/${formatMB(heapLimit)} (${formatPct(ratio)}) — only ${currentEntries} history entries remain. Consider increasing --max-old-space-size`);
|
|
2203
|
+
}
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
if (ratio < EVICT_THRESHOLD) {
|
|
2207
|
+
if (Date.now() - lastWarningTime > WARN_LOG_COOLDOWN_MS) {
|
|
2208
|
+
lastWarningTime = Date.now();
|
|
2209
|
+
consola.warn(`[memory] Heap ${formatMB(heapUsed)}/${formatMB(heapLimit)} (${formatPct(ratio)}) — approaching limit, ${currentEntries} history entries in memory`);
|
|
2210
|
+
}
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
lastWarningTime = Date.now();
|
|
2214
|
+
const currentMax = getHistoryMaxEntries();
|
|
2215
|
+
const newMaxEntries = ratio >= CRITICAL_THRESHOLD ? Math.max(state.historyMinEntries, Math.floor(currentMax * .5)) : Math.max(state.historyMinEntries, Math.floor(currentMax * .75));
|
|
2216
|
+
const evictCount = Math.max(0, currentEntries - newMaxEntries);
|
|
2217
|
+
if (evictCount <= 0) return;
|
|
2218
|
+
const evicted = evictOldestEntries(evictCount);
|
|
2219
|
+
totalEvictedCount += evicted;
|
|
2220
|
+
if (newMaxEntries < currentMax) setHistoryMaxEntries(newMaxEntries);
|
|
2221
|
+
const afterHeapUsed = process.memoryUsage().heapUsed;
|
|
2222
|
+
consola.warn(`[memory] Evicted ${evicted} history entries due to memory pressure (heap: ${formatMB(heapUsed)} → ${formatMB(afterHeapUsed)}/${formatMB(heapLimit)}, entries: ${currentEntries} → ${currentEntries - evicted}, max: ${newMaxEntries})`);
|
|
2223
|
+
globalThis.gc?.();
|
|
2224
|
+
}
|
|
2225
|
+
function startMemoryPressureMonitor() {
|
|
2226
|
+
if (timer) return;
|
|
2227
|
+
timer = setInterval(() => {
|
|
2228
|
+
checkMemoryPressure().catch((error) => {
|
|
2229
|
+
consola.error("[memory] Error in memory pressure check:", error);
|
|
2230
|
+
});
|
|
2231
|
+
}, CHECK_INTERVAL_MS);
|
|
2232
|
+
if ("unref" in timer) timer.unref();
|
|
2233
|
+
}
|
|
2234
|
+
function stopMemoryPressureMonitor() {
|
|
2235
|
+
if (timer) {
|
|
2236
|
+
clearInterval(timer);
|
|
2237
|
+
timer = null;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
|
|
1951
2241
|
//#endregion
|
|
1952
2242
|
//#region src/lib/posthog.ts
|
|
1953
2243
|
let client = null;
|
|
@@ -1998,99 +2288,24 @@ async function shutdownPostHog() {
|
|
|
1998
2288
|
}
|
|
1999
2289
|
|
|
2000
2290
|
//#endregion
|
|
2001
|
-
//#region src/lib/
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
*/
|
|
2006
|
-
var ProxyDispatcher = class extends Agent {
|
|
2007
|
-
proxies = /* @__PURE__ */ new Map();
|
|
2008
|
-
dispatch(options, handler) {
|
|
2291
|
+
//#region src/lib/shell.ts
|
|
2292
|
+
function getShell() {
|
|
2293
|
+
const { platform, ppid, env } = process$1;
|
|
2294
|
+
if (platform === "win32") {
|
|
2009
2295
|
try {
|
|
2010
|
-
|
|
2011
|
-
const proxyUrl = this.getProxyUrl(origin);
|
|
2012
|
-
if (!proxyUrl) {
|
|
2013
|
-
consola.debug(`HTTP proxy bypass: ${origin.hostname}`);
|
|
2014
|
-
return super.dispatch(options, handler);
|
|
2015
|
-
}
|
|
2016
|
-
const agent = this.getOrCreateProxyAgent(proxyUrl);
|
|
2017
|
-
consola.debug(`HTTP proxy route: ${origin.hostname} via ${this.formatProxyLabel(proxyUrl)}`);
|
|
2018
|
-
return agent.dispatch(options, handler);
|
|
2296
|
+
if (execSync(`wmic process get ParentProcessId,Name | findstr "${ppid}"`, { stdio: "pipe" }).toString().toLowerCase().includes("powershell.exe")) return "powershell";
|
|
2019
2297
|
} catch {
|
|
2020
|
-
return
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
getOriginUrl(origin) {
|
|
2024
|
-
return typeof origin === "string" ? new URL(origin) : origin;
|
|
2025
|
-
}
|
|
2026
|
-
getProxyUrl(origin) {
|
|
2027
|
-
const raw = getProxyForUrl(origin.toString());
|
|
2028
|
-
return raw && raw.length > 0 ? raw : void 0;
|
|
2029
|
-
}
|
|
2030
|
-
getOrCreateProxyAgent(proxyUrl) {
|
|
2031
|
-
let agent = this.proxies.get(proxyUrl);
|
|
2032
|
-
if (!agent) {
|
|
2033
|
-
agent = new ProxyAgent(proxyUrl);
|
|
2034
|
-
this.proxies.set(proxyUrl, agent);
|
|
2298
|
+
return "cmd";
|
|
2035
2299
|
}
|
|
2036
|
-
return
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
return
|
|
2042
|
-
|
|
2043
|
-
return proxyUrl;
|
|
2300
|
+
return "cmd";
|
|
2301
|
+
} else {
|
|
2302
|
+
const shellPath = env.SHELL;
|
|
2303
|
+
if (shellPath) {
|
|
2304
|
+
if (shellPath.endsWith("zsh")) return "zsh";
|
|
2305
|
+
if (shellPath.endsWith("fish")) return "fish";
|
|
2306
|
+
if (shellPath.endsWith("bash")) return "bash";
|
|
2044
2307
|
}
|
|
2045
|
-
|
|
2046
|
-
async close() {
|
|
2047
|
-
await super.close();
|
|
2048
|
-
await Promise.all([...this.proxies.values()].map((p) => p.close()));
|
|
2049
|
-
this.proxies.clear();
|
|
2050
|
-
}
|
|
2051
|
-
destroy(errOrCallback, callback) {
|
|
2052
|
-
for (const agent of this.proxies.values()) if (typeof errOrCallback === "function") agent.destroy(errOrCallback);
|
|
2053
|
-
else if (callback) agent.destroy(errOrCallback ?? null, callback);
|
|
2054
|
-
else agent.destroy(errOrCallback ?? null).catch(() => {});
|
|
2055
|
-
this.proxies.clear();
|
|
2056
|
-
if (typeof errOrCallback === "function") {
|
|
2057
|
-
super.destroy(errOrCallback);
|
|
2058
|
-
return;
|
|
2059
|
-
} else if (callback) {
|
|
2060
|
-
super.destroy(errOrCallback ?? null, callback);
|
|
2061
|
-
return;
|
|
2062
|
-
} else return super.destroy(errOrCallback ?? null);
|
|
2063
|
-
}
|
|
2064
|
-
};
|
|
2065
|
-
function initProxyFromEnv() {
|
|
2066
|
-
if (typeof Bun !== "undefined") return;
|
|
2067
|
-
try {
|
|
2068
|
-
setGlobalDispatcher(new ProxyDispatcher());
|
|
2069
|
-
consola.debug("HTTP proxy configured from environment (per-URL)");
|
|
2070
|
-
} catch (err) {
|
|
2071
|
-
consola.debug("Proxy setup skipped:", err);
|
|
2072
|
-
}
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
//#endregion
|
|
2076
|
-
//#region src/lib/shell.ts
|
|
2077
|
-
function getShell() {
|
|
2078
|
-
const { platform, ppid, env } = process$1;
|
|
2079
|
-
if (platform === "win32") {
|
|
2080
|
-
try {
|
|
2081
|
-
if (execSync(`wmic process get ParentProcessId,Name | findstr "${ppid}"`, { stdio: "pipe" }).toString().toLowerCase().includes("powershell.exe")) return "powershell";
|
|
2082
|
-
} catch {
|
|
2083
|
-
return "cmd";
|
|
2084
|
-
}
|
|
2085
|
-
return "cmd";
|
|
2086
|
-
} else {
|
|
2087
|
-
const shellPath = env.SHELL;
|
|
2088
|
-
if (shellPath) {
|
|
2089
|
-
if (shellPath.endsWith("zsh")) return "zsh";
|
|
2090
|
-
if (shellPath.endsWith("fish")) return "fish";
|
|
2091
|
-
if (shellPath.endsWith("bash")) return "bash";
|
|
2092
|
-
}
|
|
2093
|
-
return "sh";
|
|
2308
|
+
return "sh";
|
|
2094
2309
|
}
|
|
2095
2310
|
}
|
|
2096
2311
|
/**
|
|
@@ -2184,6 +2399,7 @@ async function gracefulShutdown(signal, deps) {
|
|
|
2184
2399
|
try {
|
|
2185
2400
|
deps?.contextManager?.stopReaper();
|
|
2186
2401
|
} catch {}
|
|
2402
|
+
stopMemoryPressureMonitor();
|
|
2187
2403
|
stopRefresh();
|
|
2188
2404
|
const wsClients = getWsCount();
|
|
2189
2405
|
if (wsClients > 0) {
|
|
@@ -2681,6 +2897,147 @@ const awaitApproval = async () => {
|
|
|
2681
2897
|
if (!await consola.prompt(`Accept incoming request?`, { type: "confirm" })) throw new HTTPError("Request rejected", 403, JSON.stringify({ message: "Request rejected" }));
|
|
2682
2898
|
};
|
|
2683
2899
|
|
|
2900
|
+
//#endregion
|
|
2901
|
+
//#region src/lib/message-sanitizer.ts
|
|
2902
|
+
const startPattern = /^\s*<system-reminder>[\s\S]*?<\/system-reminder>\n*/;
|
|
2903
|
+
const endPatternWithNewline = /\n+<system-reminder>[\s\S]*?<\/system-reminder>\s*$/;
|
|
2904
|
+
const endPatternOnly = /^\s*<system-reminder>[\s\S]*?<\/system-reminder>\s*$/;
|
|
2905
|
+
function removeSystemReminderTags(text) {
|
|
2906
|
+
let result = text;
|
|
2907
|
+
let prev;
|
|
2908
|
+
do {
|
|
2909
|
+
prev = result;
|
|
2910
|
+
result = result.replace(startPattern, "");
|
|
2911
|
+
} while (result !== prev);
|
|
2912
|
+
do {
|
|
2913
|
+
prev = result;
|
|
2914
|
+
result = result.replace(endPatternWithNewline, "");
|
|
2915
|
+
} while (result !== prev);
|
|
2916
|
+
result = result.replace(endPatternOnly, "");
|
|
2917
|
+
return result;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
//#endregion
|
|
2921
|
+
//#region src/lib/repetition-detector.ts
|
|
2922
|
+
/**
|
|
2923
|
+
* Stream repetition detector.
|
|
2924
|
+
*
|
|
2925
|
+
* Uses the KMP failure function (prefix function) to detect repeated patterns
|
|
2926
|
+
* in streaming text output. When a model gets stuck in a repetitive loop,
|
|
2927
|
+
* it wastes tokens producing the same content over and over. This detector
|
|
2928
|
+
* identifies such loops early so the caller can take action (log warning,
|
|
2929
|
+
* abort stream, etc.).
|
|
2930
|
+
*
|
|
2931
|
+
* The algorithm works by maintaining a sliding buffer of recent text and
|
|
2932
|
+
* computing the longest proper prefix that is also a suffix — if this
|
|
2933
|
+
* length exceeds `(text.length - period) >= minRepetitions * period`,
|
|
2934
|
+
* it means a pattern of length `period` has repeated enough times.
|
|
2935
|
+
*/
|
|
2936
|
+
const DEFAULT_CONFIG = {
|
|
2937
|
+
minPatternLength: 10,
|
|
2938
|
+
minRepetitions: 3,
|
|
2939
|
+
maxBufferSize: 5e3
|
|
2940
|
+
};
|
|
2941
|
+
var RepetitionDetector = class {
|
|
2942
|
+
buffer = "";
|
|
2943
|
+
config;
|
|
2944
|
+
detected = false;
|
|
2945
|
+
constructor(config) {
|
|
2946
|
+
this.config = {
|
|
2947
|
+
...DEFAULT_CONFIG,
|
|
2948
|
+
...config
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
/**
|
|
2952
|
+
* Feed a text chunk into the detector.
|
|
2953
|
+
* Returns `true` if repetition has been detected (now or previously).
|
|
2954
|
+
* Once detected, subsequent calls return `true` without further analysis.
|
|
2955
|
+
*/
|
|
2956
|
+
feed(text) {
|
|
2957
|
+
if (this.detected) return true;
|
|
2958
|
+
if (!text) return false;
|
|
2959
|
+
this.buffer += text;
|
|
2960
|
+
if (this.buffer.length > this.config.maxBufferSize) this.buffer = this.buffer.slice(-this.config.maxBufferSize);
|
|
2961
|
+
const minRequired = this.config.minPatternLength * this.config.minRepetitions;
|
|
2962
|
+
if (this.buffer.length < minRequired) return false;
|
|
2963
|
+
this.detected = detectRepetition(this.buffer, this.config.minPatternLength, this.config.minRepetitions);
|
|
2964
|
+
return this.detected;
|
|
2965
|
+
}
|
|
2966
|
+
/** Reset detector state for a new stream */
|
|
2967
|
+
reset() {
|
|
2968
|
+
this.buffer = "";
|
|
2969
|
+
this.detected = false;
|
|
2970
|
+
}
|
|
2971
|
+
/** Whether repetition has been detected */
|
|
2972
|
+
get isDetected() {
|
|
2973
|
+
return this.detected;
|
|
2974
|
+
}
|
|
2975
|
+
};
|
|
2976
|
+
/**
|
|
2977
|
+
* Detect if the tail of `text` contains a repeating pattern.
|
|
2978
|
+
*
|
|
2979
|
+
* Uses the KMP prefix function: for a string S, the prefix function π[i]
|
|
2980
|
+
* gives the length of the longest proper prefix of S[0..i] that is also
|
|
2981
|
+
* a suffix. If π[n-1] ≥ (n - period) where period = n - π[n-1], then
|
|
2982
|
+
* the string is composed of a repeating unit of length `period`.
|
|
2983
|
+
*
|
|
2984
|
+
* We check the suffix of the buffer (last `checkLength` chars) to detect
|
|
2985
|
+
* if a pattern of at least `minPatternLength` chars repeats at least
|
|
2986
|
+
* `minRepetitions` times.
|
|
2987
|
+
*/
|
|
2988
|
+
function detectRepetition(text, minPatternLength, minRepetitions) {
|
|
2989
|
+
const minWindow = minPatternLength * minRepetitions;
|
|
2990
|
+
const maxWindow = Math.min(text.length, 2e3);
|
|
2991
|
+
const windowSizes = [
|
|
2992
|
+
minWindow,
|
|
2993
|
+
Math.floor(maxWindow * .5),
|
|
2994
|
+
maxWindow
|
|
2995
|
+
].filter((w) => w >= minWindow && w <= text.length);
|
|
2996
|
+
for (const windowSize of windowSizes) {
|
|
2997
|
+
const window = text.slice(-windowSize);
|
|
2998
|
+
const period = findRepeatingPeriod(window);
|
|
2999
|
+
if (period >= minPatternLength) {
|
|
3000
|
+
if (Math.floor(window.length / period) >= minRepetitions) return true;
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
return false;
|
|
3004
|
+
}
|
|
3005
|
+
/**
|
|
3006
|
+
* Find the shortest repeating period in a string using KMP prefix function.
|
|
3007
|
+
* Returns the period length, or the string length if no repetition found.
|
|
3008
|
+
*/
|
|
3009
|
+
function findRepeatingPeriod(s) {
|
|
3010
|
+
const n = s.length;
|
|
3011
|
+
if (n === 0) return 0;
|
|
3012
|
+
const pi = new Int32Array(n);
|
|
3013
|
+
for (let i = 1; i < n; i++) {
|
|
3014
|
+
let j = pi[i - 1] ?? 0;
|
|
3015
|
+
while (j > 0 && s[i] !== s[j]) j = pi[j - 1] ?? 0;
|
|
3016
|
+
if (s[i] === s[j]) j++;
|
|
3017
|
+
pi[i] = j;
|
|
3018
|
+
}
|
|
3019
|
+
const period = n - pi[n - 1];
|
|
3020
|
+
if (period < n && n % period === 0) return period;
|
|
3021
|
+
if (period < n && pi[n - 1] >= period) return period;
|
|
3022
|
+
return n;
|
|
3023
|
+
}
|
|
3024
|
+
/**
|
|
3025
|
+
* Create a repetition detector callback for use in stream processing.
|
|
3026
|
+
* Returns a function that accepts text deltas and logs a warning on first detection.
|
|
3027
|
+
*/
|
|
3028
|
+
function createStreamRepetitionChecker(label, config) {
|
|
3029
|
+
const detector = new RepetitionDetector(config);
|
|
3030
|
+
let warned = false;
|
|
3031
|
+
return (textDelta) => {
|
|
3032
|
+
const isRepetitive = detector.feed(textDelta);
|
|
3033
|
+
if (isRepetitive && !warned) {
|
|
3034
|
+
warned = true;
|
|
3035
|
+
consola.warn(`[RepetitionDetector] ${label}: Repetitive output detected in stream`);
|
|
3036
|
+
}
|
|
3037
|
+
return isRepetitive;
|
|
3038
|
+
};
|
|
3039
|
+
}
|
|
3040
|
+
|
|
2684
3041
|
//#endregion
|
|
2685
3042
|
//#region src/lib/tokenizer.ts
|
|
2686
3043
|
const ENCODING_MAP = {
|
|
@@ -2893,6 +3250,32 @@ const getTokenCount = async (payload, model) => {
|
|
|
2893
3250
|
};
|
|
2894
3251
|
};
|
|
2895
3252
|
|
|
3253
|
+
//#endregion
|
|
3254
|
+
//#region src/services/copilot/create-chat-completions.ts
|
|
3255
|
+
const createChatCompletions = async (payload, options) => {
|
|
3256
|
+
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
3257
|
+
const enableVision = payload.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
|
|
3258
|
+
const isAgentCall = payload.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
|
|
3259
|
+
const headers = {
|
|
3260
|
+
...copilotHeaders(state, {
|
|
3261
|
+
vision: enableVision,
|
|
3262
|
+
intent: isAgentCall ? "conversation-agent" : "conversation-panel"
|
|
3263
|
+
}),
|
|
3264
|
+
"X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
|
|
3265
|
+
};
|
|
3266
|
+
const response = await fetch(`${copilotBaseUrl(state)}/chat/completions`, {
|
|
3267
|
+
method: "POST",
|
|
3268
|
+
headers,
|
|
3269
|
+
body: JSON.stringify(payload)
|
|
3270
|
+
});
|
|
3271
|
+
if (!response.ok) {
|
|
3272
|
+
consola.error("Failed to create chat completions", response);
|
|
3273
|
+
throw await HTTPError.fromResponse("Failed to create chat completions", response, payload.model);
|
|
3274
|
+
}
|
|
3275
|
+
if (payload.stream) return events(response);
|
|
3276
|
+
return await response.json();
|
|
3277
|
+
};
|
|
3278
|
+
|
|
2896
3279
|
//#endregion
|
|
2897
3280
|
//#region src/lib/auto-truncate-openai.ts
|
|
2898
3281
|
/**
|
|
@@ -2907,13 +3290,6 @@ const getTokenCount = async (payload, model) => {
|
|
|
2907
3290
|
* - Dynamic byte limit adjustment on 413 errors
|
|
2908
3291
|
* - Optional smart compression of old tool_result content
|
|
2909
3292
|
*/
|
|
2910
|
-
function calculateLimits$1(model, config) {
|
|
2911
|
-
const rawTokenLimit = getEffectiveTokenLimit(model.id) ?? model.capabilities?.limits?.max_context_window_tokens ?? model.capabilities?.limits?.max_prompt_tokens ?? 128e3;
|
|
2912
|
-
return {
|
|
2913
|
-
tokenLimit: Math.floor(rawTokenLimit * (1 - config.safetyMarginPercent / 100)),
|
|
2914
|
-
byteLimit: getEffectiveByteLimitBytes()
|
|
2915
|
-
};
|
|
2916
|
-
}
|
|
2917
3293
|
/** Estimate tokens for a single message (fast approximation) */
|
|
2918
3294
|
function estimateMessageTokens$1(msg) {
|
|
2919
3295
|
let charCount = 0;
|
|
@@ -2925,10 +3301,6 @@ function estimateMessageTokens$1(msg) {
|
|
|
2925
3301
|
if (msg.tool_calls) charCount += JSON.stringify(msg.tool_calls).length;
|
|
2926
3302
|
return Math.ceil(charCount / 4) + 10;
|
|
2927
3303
|
}
|
|
2928
|
-
/** Get byte size of a message */
|
|
2929
|
-
function getMessageBytes$1(msg) {
|
|
2930
|
-
return JSON.stringify(msg).length;
|
|
2931
|
-
}
|
|
2932
3304
|
/** Extract system/developer messages from the beginning */
|
|
2933
3305
|
function extractSystemMessages(messages) {
|
|
2934
3306
|
let splitIndex = 0;
|
|
@@ -3000,28 +3372,6 @@ function filterOrphanedToolUse$1(messages) {
|
|
|
3000
3372
|
if (removedCount > 0) consola.debug(`[AutoTruncate:OpenAI] Filtered ${removedCount} orphaned tool_use`);
|
|
3001
3373
|
return result;
|
|
3002
3374
|
}
|
|
3003
|
-
/** Ensure messages start with a user message */
|
|
3004
|
-
function ensureStartsWithUser$1(messages) {
|
|
3005
|
-
let startIndex = 0;
|
|
3006
|
-
while (startIndex < messages.length && messages[startIndex].role !== "user") startIndex++;
|
|
3007
|
-
if (startIndex > 0) consola.debug(`[AutoTruncate:OpenAI] Skipped ${startIndex} leading non-user messages`);
|
|
3008
|
-
return messages.slice(startIndex);
|
|
3009
|
-
}
|
|
3010
|
-
/** Threshold for large tool message content (bytes) */
|
|
3011
|
-
const LARGE_TOOL_RESULT_THRESHOLD$1 = 1e4;
|
|
3012
|
-
/** Maximum length for compressed tool_result summary */
|
|
3013
|
-
const COMPRESSED_SUMMARY_LENGTH$1 = 500;
|
|
3014
|
-
/**
|
|
3015
|
-
* Compress a large tool message content to a summary.
|
|
3016
|
-
* Keeps the first and last portions with a note about truncation.
|
|
3017
|
-
*/
|
|
3018
|
-
function compressToolResultContent$1(content) {
|
|
3019
|
-
if (content.length <= LARGE_TOOL_RESULT_THRESHOLD$1) return content;
|
|
3020
|
-
const halfLen = Math.floor(COMPRESSED_SUMMARY_LENGTH$1 / 2);
|
|
3021
|
-
const start = content.slice(0, halfLen);
|
|
3022
|
-
const end = content.slice(-halfLen);
|
|
3023
|
-
return `${start}\n\n[... ${(content.length - COMPRESSED_SUMMARY_LENGTH$1).toLocaleString()} characters omitted for brevity ...]\n\n${end}`;
|
|
3024
|
-
}
|
|
3025
3375
|
/**
|
|
3026
3376
|
* Smart compression strategy for OpenAI format:
|
|
3027
3377
|
* 1. Calculate tokens/bytes from the end until reaching preservePercent of limit
|
|
@@ -3031,37 +3381,20 @@ function compressToolResultContent$1(content) {
|
|
|
3031
3381
|
* @param preservePercent - Percentage of context to preserve uncompressed (0.0-1.0)
|
|
3032
3382
|
*/
|
|
3033
3383
|
function smartCompressToolResults$1(messages, tokenLimit, byteLimit, preservePercent) {
|
|
3034
|
-
const
|
|
3035
|
-
|
|
3036
|
-
const cumBytes = Array.from({ length: n + 1 }, () => 0);
|
|
3037
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
3038
|
-
const msg = messages[i];
|
|
3039
|
-
cumTokens[i] = cumTokens[i + 1] + estimateMessageTokens$1(msg);
|
|
3040
|
-
cumBytes[i] = cumBytes[i + 1] + getMessageBytes$1(msg) + 1;
|
|
3041
|
-
}
|
|
3042
|
-
const preserveTokenLimit = Math.floor(tokenLimit * preservePercent);
|
|
3043
|
-
const preserveByteLimit = Math.floor(byteLimit * preservePercent);
|
|
3044
|
-
let thresholdIndex = n;
|
|
3045
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
3046
|
-
if (cumTokens[i] > preserveTokenLimit || cumBytes[i] > preserveByteLimit) {
|
|
3047
|
-
thresholdIndex = i + 1;
|
|
3048
|
-
break;
|
|
3049
|
-
}
|
|
3050
|
-
thresholdIndex = i;
|
|
3051
|
-
}
|
|
3052
|
-
if (thresholdIndex >= n) return {
|
|
3384
|
+
const thresholdIndex = findCompressThreshold(messages, tokenLimit, byteLimit, preservePercent, estimateMessageTokens$1);
|
|
3385
|
+
if (thresholdIndex >= messages.length) return {
|
|
3053
3386
|
messages,
|
|
3054
3387
|
compressedCount: 0,
|
|
3055
|
-
compressThresholdIndex:
|
|
3388
|
+
compressThresholdIndex: messages.length
|
|
3056
3389
|
};
|
|
3057
3390
|
const result = [];
|
|
3058
3391
|
let compressedCount = 0;
|
|
3059
3392
|
for (const [i, msg] of messages.entries()) {
|
|
3060
|
-
if (i < thresholdIndex && msg.role === "tool" && typeof msg.content === "string" && msg.content.length > LARGE_TOOL_RESULT_THRESHOLD
|
|
3393
|
+
if (i < thresholdIndex && msg.role === "tool" && typeof msg.content === "string" && msg.content.length > LARGE_TOOL_RESULT_THRESHOLD) {
|
|
3061
3394
|
compressedCount++;
|
|
3062
3395
|
result.push({
|
|
3063
3396
|
...msg,
|
|
3064
|
-
content: compressToolResultContent
|
|
3397
|
+
content: compressToolResultContent(msg.content)
|
|
3065
3398
|
});
|
|
3066
3399
|
continue;
|
|
3067
3400
|
}
|
|
@@ -3074,42 +3407,13 @@ function smartCompressToolResults$1(messages, tokenLimit, byteLimit, preservePer
|
|
|
3074
3407
|
};
|
|
3075
3408
|
}
|
|
3076
3409
|
/**
|
|
3077
|
-
* Find the optimal index from which to preserve messages.
|
|
3078
|
-
* Uses binary search with pre-calculated cumulative sums.
|
|
3079
|
-
* Returns the smallest index where the preserved portion fits within limits.
|
|
3080
|
-
*/
|
|
3081
|
-
function findOptimalPreserveIndex$1(params) {
|
|
3082
|
-
const { messages, systemBytes, systemTokens, payloadOverhead, tokenLimit, byteLimit } = params;
|
|
3083
|
-
if (messages.length === 0) return 0;
|
|
3084
|
-
const markerBytes = 200;
|
|
3085
|
-
const availableTokens = tokenLimit - systemTokens - 50;
|
|
3086
|
-
const availableBytes = byteLimit - payloadOverhead - systemBytes - markerBytes;
|
|
3087
|
-
if (availableTokens <= 0 || availableBytes <= 0) return messages.length;
|
|
3088
|
-
const n = messages.length;
|
|
3089
|
-
const cumTokens = Array.from({ length: n + 1 }, () => 0);
|
|
3090
|
-
const cumBytes = Array.from({ length: n + 1 }, () => 0);
|
|
3091
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
3092
|
-
const msg = messages[i];
|
|
3093
|
-
cumTokens[i] = cumTokens[i + 1] + estimateMessageTokens$1(msg);
|
|
3094
|
-
cumBytes[i] = cumBytes[i + 1] + getMessageBytes$1(msg) + 1;
|
|
3095
|
-
}
|
|
3096
|
-
let left = 0;
|
|
3097
|
-
let right = n;
|
|
3098
|
-
while (left < right) {
|
|
3099
|
-
const mid = left + right >>> 1;
|
|
3100
|
-
if (cumTokens[mid] <= availableTokens && cumBytes[mid] <= availableBytes) right = mid;
|
|
3101
|
-
else left = mid + 1;
|
|
3102
|
-
}
|
|
3103
|
-
return left;
|
|
3104
|
-
}
|
|
3105
|
-
/**
|
|
3106
3410
|
* Check if payload needs compaction based on model limits or byte size.
|
|
3107
3411
|
*/
|
|
3108
3412
|
async function checkNeedsCompactionOpenAI(payload, model, config = {}) {
|
|
3109
|
-
const { tokenLimit, byteLimit } = calculateLimits
|
|
3413
|
+
const { tokenLimit, byteLimit } = calculateLimits(model, {
|
|
3110
3414
|
...DEFAULT_AUTO_TRUNCATE_CONFIG,
|
|
3111
3415
|
...config
|
|
3112
|
-
});
|
|
3416
|
+
}, 128e3);
|
|
3113
3417
|
const currentTokens = (await getTokenCount(payload, model)).input;
|
|
3114
3418
|
const currentBytes = JSON.stringify(payload).length;
|
|
3115
3419
|
const exceedsTokens = currentTokens > tokenLimit;
|
|
@@ -3128,35 +3432,6 @@ async function checkNeedsCompactionOpenAI(payload, model, config = {}) {
|
|
|
3128
3432
|
};
|
|
3129
3433
|
}
|
|
3130
3434
|
/**
|
|
3131
|
-
* Generate a summary of removed messages for context.
|
|
3132
|
-
* Extracts key information like tool calls and topics.
|
|
3133
|
-
*/
|
|
3134
|
-
function generateRemovedMessagesSummary$1(removedMessages) {
|
|
3135
|
-
const toolCalls = [];
|
|
3136
|
-
let userMessageCount = 0;
|
|
3137
|
-
let assistantMessageCount = 0;
|
|
3138
|
-
for (const msg of removedMessages) {
|
|
3139
|
-
if (msg.role === "user") userMessageCount++;
|
|
3140
|
-
else if (msg.role === "assistant") assistantMessageCount++;
|
|
3141
|
-
if (msg.tool_calls) {
|
|
3142
|
-
for (const tc of msg.tool_calls) if (tc.function.name) toolCalls.push(tc.function.name);
|
|
3143
|
-
}
|
|
3144
|
-
}
|
|
3145
|
-
const parts = [];
|
|
3146
|
-
if (userMessageCount > 0 || assistantMessageCount > 0) {
|
|
3147
|
-
const breakdown = [];
|
|
3148
|
-
if (userMessageCount > 0) breakdown.push(`${userMessageCount} user`);
|
|
3149
|
-
if (assistantMessageCount > 0) breakdown.push(`${assistantMessageCount} assistant`);
|
|
3150
|
-
parts.push(`Messages: ${breakdown.join(", ")}`);
|
|
3151
|
-
}
|
|
3152
|
-
if (toolCalls.length > 0) {
|
|
3153
|
-
const uniqueTools = [...new Set(toolCalls)];
|
|
3154
|
-
const displayTools = uniqueTools.length > 5 ? [...uniqueTools.slice(0, 5), `+${uniqueTools.length - 5} more`] : uniqueTools;
|
|
3155
|
-
parts.push(`Tools used: ${displayTools.join(", ")}`);
|
|
3156
|
-
}
|
|
3157
|
-
return parts.join(". ");
|
|
3158
|
-
}
|
|
3159
|
-
/**
|
|
3160
3435
|
* Add a compression notice to the system message.
|
|
3161
3436
|
* Informs the model that some tool content has been compressed.
|
|
3162
3437
|
*/
|
|
@@ -3210,7 +3485,7 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
|
|
|
3210
3485
|
...DEFAULT_AUTO_TRUNCATE_CONFIG,
|
|
3211
3486
|
...config
|
|
3212
3487
|
};
|
|
3213
|
-
const { tokenLimit, byteLimit } = calculateLimits
|
|
3488
|
+
const { tokenLimit, byteLimit } = calculateLimits(model, cfg, 128e3);
|
|
3214
3489
|
const originalBytes = JSON.stringify(payload).length;
|
|
3215
3490
|
const originalTokens = (await getTokenCount(payload, model)).input;
|
|
3216
3491
|
if (originalTokens <= tokenLimit && originalBytes <= byteLimit) return {
|
|
@@ -3255,16 +3530,17 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
|
|
|
3255
3530
|
...payload,
|
|
3256
3531
|
messages: workingMessages
|
|
3257
3532
|
}).length - messagesJson.length;
|
|
3258
|
-
const systemBytes = systemMessages.reduce((sum, m) => sum + getMessageBytes
|
|
3533
|
+
const systemBytes = systemMessages.reduce((sum, m) => sum + getMessageBytes(m) + 1, 0);
|
|
3259
3534
|
const systemTokens = systemMessages.reduce((sum, m) => sum + estimateMessageTokens$1(m), 0);
|
|
3260
3535
|
consola.debug(`[AutoTruncate:OpenAI] overhead=${Math.round(payloadOverhead / 1024)}KB, system=${systemMessages.length} msgs (${Math.round(systemBytes / 1024)}KB)`);
|
|
3261
|
-
const preserveIndex = findOptimalPreserveIndex
|
|
3536
|
+
const preserveIndex = findOptimalPreserveIndex({
|
|
3262
3537
|
messages: conversationMessages,
|
|
3263
3538
|
systemBytes,
|
|
3264
3539
|
systemTokens,
|
|
3265
3540
|
payloadOverhead,
|
|
3266
3541
|
tokenLimit,
|
|
3267
|
-
byteLimit
|
|
3542
|
+
byteLimit,
|
|
3543
|
+
estimateTokens: estimateMessageTokens$1
|
|
3268
3544
|
});
|
|
3269
3545
|
if (preserveIndex === 0) {
|
|
3270
3546
|
consola.warn("[AutoTruncate:OpenAI] Cannot truncate, system messages too large");
|
|
@@ -3289,7 +3565,7 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
|
|
|
3289
3565
|
let preserved = conversationMessages.slice(preserveIndex);
|
|
3290
3566
|
preserved = filterOrphanedToolResults$1(preserved);
|
|
3291
3567
|
preserved = filterOrphanedToolUse$1(preserved);
|
|
3292
|
-
preserved = ensureStartsWithUser
|
|
3568
|
+
preserved = ensureStartsWithUser(preserved, "OpenAI");
|
|
3293
3569
|
preserved = filterOrphanedToolResults$1(preserved);
|
|
3294
3570
|
preserved = filterOrphanedToolUse$1(preserved);
|
|
3295
3571
|
if (preserved.length === 0) {
|
|
@@ -3304,7 +3580,7 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
|
|
|
3304
3580
|
}
|
|
3305
3581
|
const removedMessages = conversationMessages.slice(0, preserveIndex);
|
|
3306
3582
|
const removedCount = conversationMessages.length - preserved.length;
|
|
3307
|
-
const summary = generateRemovedMessagesSummary
|
|
3583
|
+
const summary = generateRemovedMessagesSummary(removedMessages, (msg) => msg.tool_calls?.map((tc) => tc.function.name).filter(Boolean) ?? []);
|
|
3308
3584
|
let newSystemMessages = systemMessages;
|
|
3309
3585
|
let newMessages = preserved;
|
|
3310
3586
|
if (systemMessages.length > 0) {
|
|
@@ -3326,196 +3602,20 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
|
|
|
3326
3602
|
let reason = "tokens";
|
|
3327
3603
|
if (exceedsTokens && exceedsBytes) reason = "tokens+size";
|
|
3328
3604
|
else if (exceedsBytes) reason = "size";
|
|
3329
|
-
const actions = [];
|
|
3330
|
-
if (removedCount > 0) actions.push(`removed ${removedCount} msgs`);
|
|
3331
|
-
if (compressedCount > 0) actions.push(`compressed ${compressedCount} tool_results`);
|
|
3332
|
-
const actionInfo = actions.length > 0 ? ` (${actions.join(", ")})` : "";
|
|
3333
|
-
consola.info(`[AutoTruncate:OpenAI] ${reason}: ${originalTokens}→${newTokenCount.input} tokens, ${Math.round(originalBytes / 1024)}→${Math.round(newBytes / 1024)}KB${actionInfo}`);
|
|
3334
|
-
if (newBytes > byteLimit) consola.warn(`[AutoTruncate:OpenAI] Result still over byte limit (${Math.round(newBytes / 1024)}KB > ${Math.round(byteLimit / 1024)}KB)`);
|
|
3335
|
-
return {
|
|
3336
|
-
payload: newPayload,
|
|
3337
|
-
wasCompacted: true,
|
|
3338
|
-
originalTokens,
|
|
3339
|
-
compactedTokens: newTokenCount.input,
|
|
3340
|
-
removedMessageCount: removedCount
|
|
3341
|
-
};
|
|
3342
|
-
}
|
|
3343
|
-
/**
|
|
3344
|
-
* Create a marker to prepend to responses indicating auto-truncation occurred.
|
|
3345
|
-
*/
|
|
3346
|
-
function createTruncationResponseMarkerOpenAI(result) {
|
|
3347
|
-
if (!result.wasCompacted) return "";
|
|
3348
|
-
const reduction = result.originalTokens - result.compactedTokens;
|
|
3349
|
-
const percentage = Math.round(reduction / result.originalTokens * 100);
|
|
3350
|
-
return `\n\n---\n[Auto-truncated: ${result.removedMessageCount} messages removed, ${result.originalTokens} → ${result.compactedTokens} tokens (${percentage}% reduction)]`;
|
|
3351
|
-
}
|
|
3352
|
-
|
|
3353
|
-
//#endregion
|
|
3354
|
-
//#region src/lib/message-sanitizer.ts
|
|
3355
|
-
const startPattern = /^\s*<system-reminder>[\s\S]*?<\/system-reminder>\n*/;
|
|
3356
|
-
const endPatternWithNewline = /\n+<system-reminder>[\s\S]*?<\/system-reminder>\s*$/;
|
|
3357
|
-
const endPatternOnly = /^\s*<system-reminder>[\s\S]*?<\/system-reminder>\s*$/;
|
|
3358
|
-
function removeSystemReminderTags(text) {
|
|
3359
|
-
let result = text;
|
|
3360
|
-
let prev;
|
|
3361
|
-
do {
|
|
3362
|
-
prev = result;
|
|
3363
|
-
result = result.replace(startPattern, "");
|
|
3364
|
-
} while (result !== prev);
|
|
3365
|
-
do {
|
|
3366
|
-
prev = result;
|
|
3367
|
-
result = result.replace(endPatternWithNewline, "");
|
|
3368
|
-
} while (result !== prev);
|
|
3369
|
-
result = result.replace(endPatternOnly, "");
|
|
3370
|
-
return result;
|
|
3371
|
-
}
|
|
3372
|
-
|
|
3373
|
-
//#endregion
|
|
3374
|
-
//#region src/lib/repetition-detector.ts
|
|
3375
|
-
/**
|
|
3376
|
-
* Stream repetition detector.
|
|
3377
|
-
*
|
|
3378
|
-
* Uses the KMP failure function (prefix function) to detect repeated patterns
|
|
3379
|
-
* in streaming text output. When a model gets stuck in a repetitive loop,
|
|
3380
|
-
* it wastes tokens producing the same content over and over. This detector
|
|
3381
|
-
* identifies such loops early so the caller can take action (log warning,
|
|
3382
|
-
* abort stream, etc.).
|
|
3383
|
-
*
|
|
3384
|
-
* The algorithm works by maintaining a sliding buffer of recent text and
|
|
3385
|
-
* computing the longest proper prefix that is also a suffix — if this
|
|
3386
|
-
* length exceeds `(text.length - period) >= minRepetitions * period`,
|
|
3387
|
-
* it means a pattern of length `period` has repeated enough times.
|
|
3388
|
-
*/
|
|
3389
|
-
const DEFAULT_CONFIG = {
|
|
3390
|
-
minPatternLength: 10,
|
|
3391
|
-
minRepetitions: 3,
|
|
3392
|
-
maxBufferSize: 5e3
|
|
3393
|
-
};
|
|
3394
|
-
var RepetitionDetector = class {
|
|
3395
|
-
buffer = "";
|
|
3396
|
-
config;
|
|
3397
|
-
detected = false;
|
|
3398
|
-
constructor(config) {
|
|
3399
|
-
this.config = {
|
|
3400
|
-
...DEFAULT_CONFIG,
|
|
3401
|
-
...config
|
|
3402
|
-
};
|
|
3403
|
-
}
|
|
3404
|
-
/**
|
|
3405
|
-
* Feed a text chunk into the detector.
|
|
3406
|
-
* Returns `true` if repetition has been detected (now or previously).
|
|
3407
|
-
* Once detected, subsequent calls return `true` without further analysis.
|
|
3408
|
-
*/
|
|
3409
|
-
feed(text) {
|
|
3410
|
-
if (this.detected) return true;
|
|
3411
|
-
if (!text) return false;
|
|
3412
|
-
this.buffer += text;
|
|
3413
|
-
if (this.buffer.length > this.config.maxBufferSize) this.buffer = this.buffer.slice(-this.config.maxBufferSize);
|
|
3414
|
-
const minRequired = this.config.minPatternLength * this.config.minRepetitions;
|
|
3415
|
-
if (this.buffer.length < minRequired) return false;
|
|
3416
|
-
this.detected = detectRepetition(this.buffer, this.config.minPatternLength, this.config.minRepetitions);
|
|
3417
|
-
return this.detected;
|
|
3418
|
-
}
|
|
3419
|
-
/** Reset detector state for a new stream */
|
|
3420
|
-
reset() {
|
|
3421
|
-
this.buffer = "";
|
|
3422
|
-
this.detected = false;
|
|
3423
|
-
}
|
|
3424
|
-
/** Whether repetition has been detected */
|
|
3425
|
-
get isDetected() {
|
|
3426
|
-
return this.detected;
|
|
3427
|
-
}
|
|
3428
|
-
};
|
|
3429
|
-
/**
|
|
3430
|
-
* Detect if the tail of `text` contains a repeating pattern.
|
|
3431
|
-
*
|
|
3432
|
-
* Uses the KMP prefix function: for a string S, the prefix function π[i]
|
|
3433
|
-
* gives the length of the longest proper prefix of S[0..i] that is also
|
|
3434
|
-
* a suffix. If π[n-1] ≥ (n - period) where period = n - π[n-1], then
|
|
3435
|
-
* the string is composed of a repeating unit of length `period`.
|
|
3436
|
-
*
|
|
3437
|
-
* We check the suffix of the buffer (last `checkLength` chars) to detect
|
|
3438
|
-
* if a pattern of at least `minPatternLength` chars repeats at least
|
|
3439
|
-
* `minRepetitions` times.
|
|
3440
|
-
*/
|
|
3441
|
-
function detectRepetition(text, minPatternLength, minRepetitions) {
|
|
3442
|
-
const minWindow = minPatternLength * minRepetitions;
|
|
3443
|
-
const maxWindow = Math.min(text.length, 2e3);
|
|
3444
|
-
const windowSizes = [
|
|
3445
|
-
minWindow,
|
|
3446
|
-
Math.floor(maxWindow * .5),
|
|
3447
|
-
maxWindow
|
|
3448
|
-
].filter((w) => w >= minWindow && w <= text.length);
|
|
3449
|
-
for (const windowSize of windowSizes) {
|
|
3450
|
-
const window = text.slice(-windowSize);
|
|
3451
|
-
const period = findRepeatingPeriod(window);
|
|
3452
|
-
if (period >= minPatternLength) {
|
|
3453
|
-
if (Math.floor(window.length / period) >= minRepetitions) return true;
|
|
3454
|
-
}
|
|
3455
|
-
}
|
|
3456
|
-
return false;
|
|
3457
|
-
}
|
|
3458
|
-
/**
|
|
3459
|
-
* Find the shortest repeating period in a string using KMP prefix function.
|
|
3460
|
-
* Returns the period length, or the string length if no repetition found.
|
|
3461
|
-
*/
|
|
3462
|
-
function findRepeatingPeriod(s) {
|
|
3463
|
-
const n = s.length;
|
|
3464
|
-
if (n === 0) return 0;
|
|
3465
|
-
const pi = new Int32Array(n);
|
|
3466
|
-
for (let i = 1; i < n; i++) {
|
|
3467
|
-
let j = pi[i - 1] ?? 0;
|
|
3468
|
-
while (j > 0 && s[i] !== s[j]) j = pi[j - 1] ?? 0;
|
|
3469
|
-
if (s[i] === s[j]) j++;
|
|
3470
|
-
pi[i] = j;
|
|
3471
|
-
}
|
|
3472
|
-
const period = n - pi[n - 1];
|
|
3473
|
-
if (period < n && n % period === 0) return period;
|
|
3474
|
-
if (period < n && pi[n - 1] >= period) return period;
|
|
3475
|
-
return n;
|
|
3476
|
-
}
|
|
3477
|
-
/**
|
|
3478
|
-
* Create a repetition detector callback for use in stream processing.
|
|
3479
|
-
* Returns a function that accepts text deltas and logs a warning on first detection.
|
|
3480
|
-
*/
|
|
3481
|
-
function createStreamRepetitionChecker(label, config) {
|
|
3482
|
-
const detector = new RepetitionDetector(config);
|
|
3483
|
-
let warned = false;
|
|
3484
|
-
return (textDelta) => {
|
|
3485
|
-
const isRepetitive = detector.feed(textDelta);
|
|
3486
|
-
if (isRepetitive && !warned) {
|
|
3487
|
-
warned = true;
|
|
3488
|
-
consola.warn(`[RepetitionDetector] ${label}: Repetitive output detected in stream`);
|
|
3489
|
-
}
|
|
3490
|
-
return isRepetitive;
|
|
3491
|
-
};
|
|
3492
|
-
}
|
|
3493
|
-
|
|
3494
|
-
//#endregion
|
|
3495
|
-
//#region src/services/copilot/create-chat-completions.ts
|
|
3496
|
-
const createChatCompletions = async (payload, options) => {
|
|
3497
|
-
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
3498
|
-
const enableVision = payload.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
|
|
3499
|
-
const isAgentCall = payload.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
|
|
3500
|
-
const headers = {
|
|
3501
|
-
...copilotHeaders(state, {
|
|
3502
|
-
vision: enableVision,
|
|
3503
|
-
intent: isAgentCall ? "conversation-agent" : "conversation-panel"
|
|
3504
|
-
}),
|
|
3505
|
-
"X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
|
|
3605
|
+
const actions = [];
|
|
3606
|
+
if (removedCount > 0) actions.push(`removed ${removedCount} msgs`);
|
|
3607
|
+
if (compressedCount > 0) actions.push(`compressed ${compressedCount} tool_results`);
|
|
3608
|
+
const actionInfo = actions.length > 0 ? ` (${actions.join(", ")})` : "";
|
|
3609
|
+
consola.info(`[AutoTruncate:OpenAI] ${reason}: ${originalTokens}→${newTokenCount.input} tokens, ${Math.round(originalBytes / 1024)}→${Math.round(newBytes / 1024)}KB${actionInfo}`);
|
|
3610
|
+
if (newBytes > byteLimit) consola.warn(`[AutoTruncate:OpenAI] Result still over byte limit (${Math.round(newBytes / 1024)}KB > ${Math.round(byteLimit / 1024)}KB)`);
|
|
3611
|
+
return {
|
|
3612
|
+
payload: newPayload,
|
|
3613
|
+
wasCompacted: true,
|
|
3614
|
+
originalTokens,
|
|
3615
|
+
compactedTokens: newTokenCount.input,
|
|
3616
|
+
removedMessageCount: removedCount
|
|
3506
3617
|
};
|
|
3507
|
-
|
|
3508
|
-
method: "POST",
|
|
3509
|
-
headers,
|
|
3510
|
-
body: JSON.stringify(payload)
|
|
3511
|
-
});
|
|
3512
|
-
if (!response.ok) {
|
|
3513
|
-
consola.error("Failed to create chat completions", response);
|
|
3514
|
-
throw await HTTPError.fromResponse("Failed to create chat completions", response, payload.model);
|
|
3515
|
-
}
|
|
3516
|
-
if (payload.stream) return events(response);
|
|
3517
|
-
return await response.json();
|
|
3518
|
-
};
|
|
3618
|
+
}
|
|
3519
3619
|
|
|
3520
3620
|
//#endregion
|
|
3521
3621
|
//#region src/routes/shared.ts
|
|
@@ -3722,7 +3822,7 @@ async function handleCompletion$1(c) {
|
|
|
3722
3822
|
trackingId,
|
|
3723
3823
|
startTime
|
|
3724
3824
|
};
|
|
3725
|
-
const selectedModel =
|
|
3825
|
+
const selectedModel = findModelById(originalPayload.model);
|
|
3726
3826
|
await logTokenCount(originalPayload, selectedModel);
|
|
3727
3827
|
const { finalPayload, truncateResult } = await buildFinalPayload(originalPayload, selectedModel);
|
|
3728
3828
|
if (truncateResult) ctx.truncateResult = truncateResult;
|
|
@@ -3766,6 +3866,7 @@ async function executeRequest(opts) {
|
|
|
3766
3866
|
}
|
|
3767
3867
|
}
|
|
3768
3868
|
async function logTokenCount(payload, selectedModel) {
|
|
3869
|
+
if (consola.level < 4) return;
|
|
3769
3870
|
try {
|
|
3770
3871
|
if (selectedModel) {
|
|
3771
3872
|
const tokenCount = await getTokenCount(payload, selectedModel);
|
|
@@ -3779,7 +3880,7 @@ function handleNonStreamingResponse$1(c, originalResponse, ctx, payload) {
|
|
|
3779
3880
|
consola.debug("Non-streaming response:", JSON.stringify(originalResponse));
|
|
3780
3881
|
let response = originalResponse;
|
|
3781
3882
|
if (state.verbose && ctx.truncateResult?.wasCompacted && response.choices[0]?.message.content) {
|
|
3782
|
-
const marker =
|
|
3883
|
+
const marker = createTruncationMarker$1(ctx.truncateResult);
|
|
3783
3884
|
response = {
|
|
3784
3885
|
...response,
|
|
3785
3886
|
choices: response.choices.map((choice, i) => i === 0 ? {
|
|
@@ -3865,7 +3966,7 @@ async function handleStreamingResponse$1(opts) {
|
|
|
3865
3966
|
const checkRepetition = createStreamRepetitionChecker(`openai:${payload.model}`);
|
|
3866
3967
|
try {
|
|
3867
3968
|
if (state.verbose && ctx.truncateResult?.wasCompacted) {
|
|
3868
|
-
const marker =
|
|
3969
|
+
const marker = createTruncationMarker$1(ctx.truncateResult);
|
|
3869
3970
|
const markerChunk = {
|
|
3870
3971
|
id: `compact-marker-${Date.now()}`,
|
|
3871
3972
|
object: "chat.completion.chunk",
|
|
@@ -4224,7 +4325,7 @@ function isFileDataPart(part) {
|
|
|
4224
4325
|
async function handleGeminiCountTokens(c, model) {
|
|
4225
4326
|
try {
|
|
4226
4327
|
const { payload } = translateGeminiToOpenAI(await c.req.json(), model);
|
|
4227
|
-
const selectedModel =
|
|
4328
|
+
const selectedModel = findModelById(model);
|
|
4228
4329
|
if (!selectedModel) {
|
|
4229
4330
|
consola.warn("Model not found for count_tokens, returning estimate");
|
|
4230
4331
|
return c.json({ totalTokens: 1 });
|
|
@@ -4384,7 +4485,7 @@ async function handleGeminiGenerate(c, model, isStream) {
|
|
|
4384
4485
|
updateTrackerModel(trackingId, model);
|
|
4385
4486
|
const { payload } = translateGeminiToOpenAI(geminiRequest, model);
|
|
4386
4487
|
payload.stream = isStream;
|
|
4387
|
-
const selectedModel =
|
|
4488
|
+
const selectedModel = findModelById(model);
|
|
4388
4489
|
if (isNullish(payload.max_tokens) && selectedModel) payload.max_tokens = selectedModel.capabilities?.limits?.max_output_tokens;
|
|
4389
4490
|
const ctx = {
|
|
4390
4491
|
historyId: recordRequest("gemini", {
|
|
@@ -6104,9 +6205,6 @@ async function countTotalTokens(payload, model) {
|
|
|
6104
6205
|
}
|
|
6105
6206
|
return total;
|
|
6106
6207
|
}
|
|
6107
|
-
function getMessageBytes(msg) {
|
|
6108
|
-
return JSON.stringify(msg).length;
|
|
6109
|
-
}
|
|
6110
6208
|
/**
|
|
6111
6209
|
* Get tool_use IDs from an assistant message.
|
|
6112
6210
|
*/
|
|
@@ -6191,30 +6289,6 @@ function filterOrphanedToolUse(messages) {
|
|
|
6191
6289
|
return result;
|
|
6192
6290
|
}
|
|
6193
6291
|
/**
|
|
6194
|
-
* Ensure messages start with a user message.
|
|
6195
|
-
*/
|
|
6196
|
-
function ensureStartsWithUser(messages) {
|
|
6197
|
-
let startIndex = 0;
|
|
6198
|
-
while (startIndex < messages.length && messages[startIndex].role !== "user") startIndex++;
|
|
6199
|
-
if (startIndex > 0) consola.debug(`[AutoTruncate:Anthropic] Skipped ${startIndex} leading non-user messages`);
|
|
6200
|
-
return messages.slice(startIndex);
|
|
6201
|
-
}
|
|
6202
|
-
/** Threshold for large tool_result content (bytes) */
|
|
6203
|
-
const LARGE_TOOL_RESULT_THRESHOLD = 1e4;
|
|
6204
|
-
/** Maximum length for compressed tool_result summary */
|
|
6205
|
-
const COMPRESSED_SUMMARY_LENGTH = 500;
|
|
6206
|
-
/**
|
|
6207
|
-
* Compress a large tool_result content to a summary.
|
|
6208
|
-
* Keeps the first and last portions with a note about truncation.
|
|
6209
|
-
*/
|
|
6210
|
-
function compressToolResultContent(content) {
|
|
6211
|
-
if (content.length <= LARGE_TOOL_RESULT_THRESHOLD) return content;
|
|
6212
|
-
const halfLen = Math.floor(COMPRESSED_SUMMARY_LENGTH / 2);
|
|
6213
|
-
const start = content.slice(0, halfLen);
|
|
6214
|
-
const end = content.slice(-halfLen);
|
|
6215
|
-
return `${start}\n\n[... ${(content.length - COMPRESSED_SUMMARY_LENGTH).toLocaleString()} characters omitted for brevity ...]\n\n${end}`;
|
|
6216
|
-
}
|
|
6217
|
-
/**
|
|
6218
6292
|
* Compress a tool_result block in an Anthropic message.
|
|
6219
6293
|
*/
|
|
6220
6294
|
function compressToolResultBlock(block) {
|
|
@@ -6233,28 +6307,11 @@ function compressToolResultBlock(block) {
|
|
|
6233
6307
|
* @param preservePercent - Percentage of context to preserve uncompressed (0.0-1.0)
|
|
6234
6308
|
*/
|
|
6235
6309
|
function smartCompressToolResults(messages, tokenLimit, byteLimit, preservePercent) {
|
|
6236
|
-
const
|
|
6237
|
-
|
|
6238
|
-
const cumBytes = Array.from({ length: n + 1 }, () => 0);
|
|
6239
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
6240
|
-
const msg = messages[i];
|
|
6241
|
-
cumTokens[i] = cumTokens[i + 1] + estimateMessageTokens(msg);
|
|
6242
|
-
cumBytes[i] = cumBytes[i + 1] + getMessageBytes(msg) + 1;
|
|
6243
|
-
}
|
|
6244
|
-
const preserveTokenLimit = Math.floor(tokenLimit * preservePercent);
|
|
6245
|
-
const preserveByteLimit = Math.floor(byteLimit * preservePercent);
|
|
6246
|
-
let thresholdIndex = n;
|
|
6247
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
6248
|
-
if (cumTokens[i] > preserveTokenLimit || cumBytes[i] > preserveByteLimit) {
|
|
6249
|
-
thresholdIndex = i + 1;
|
|
6250
|
-
break;
|
|
6251
|
-
}
|
|
6252
|
-
thresholdIndex = i;
|
|
6253
|
-
}
|
|
6254
|
-
if (thresholdIndex >= n) return {
|
|
6310
|
+
const thresholdIndex = findCompressThreshold(messages, tokenLimit, byteLimit, preservePercent, estimateMessageTokens);
|
|
6311
|
+
if (thresholdIndex >= messages.length) return {
|
|
6255
6312
|
messages,
|
|
6256
6313
|
compressedCount: 0,
|
|
6257
|
-
compressThresholdIndex:
|
|
6314
|
+
compressThresholdIndex: messages.length
|
|
6258
6315
|
};
|
|
6259
6316
|
const result = [];
|
|
6260
6317
|
let compressedCount = 0;
|
|
@@ -6283,68 +6340,6 @@ function smartCompressToolResults(messages, tokenLimit, byteLimit, preservePerce
|
|
|
6283
6340
|
compressThresholdIndex: thresholdIndex
|
|
6284
6341
|
};
|
|
6285
6342
|
}
|
|
6286
|
-
/** Default fallback for when model capabilities are not available */
|
|
6287
|
-
const DEFAULT_CONTEXT_WINDOW = 2e5;
|
|
6288
|
-
function calculateLimits(model, config) {
|
|
6289
|
-
const rawTokenLimit = getEffectiveTokenLimit(model.id) ?? model.capabilities?.limits?.max_context_window_tokens ?? model.capabilities?.limits?.max_prompt_tokens ?? DEFAULT_CONTEXT_WINDOW;
|
|
6290
|
-
return {
|
|
6291
|
-
tokenLimit: Math.floor(rawTokenLimit * (1 - config.safetyMarginPercent / 100)),
|
|
6292
|
-
byteLimit: getEffectiveByteLimitBytes()
|
|
6293
|
-
};
|
|
6294
|
-
}
|
|
6295
|
-
function findOptimalPreserveIndex(params) {
|
|
6296
|
-
const { messages, systemBytes, systemTokens, payloadOverhead, tokenLimit, byteLimit } = params;
|
|
6297
|
-
if (messages.length === 0) return 0;
|
|
6298
|
-
const markerBytes = 200;
|
|
6299
|
-
const availableTokens = tokenLimit - systemTokens - 50;
|
|
6300
|
-
const availableBytes = byteLimit - payloadOverhead - systemBytes - markerBytes;
|
|
6301
|
-
if (availableTokens <= 0 || availableBytes <= 0) return messages.length;
|
|
6302
|
-
const n = messages.length;
|
|
6303
|
-
const cumTokens = Array.from({ length: n + 1 }, () => 0);
|
|
6304
|
-
const cumBytes = Array.from({ length: n + 1 }, () => 0);
|
|
6305
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
6306
|
-
const msg = messages[i];
|
|
6307
|
-
cumTokens[i] = cumTokens[i + 1] + estimateMessageTokens(msg);
|
|
6308
|
-
cumBytes[i] = cumBytes[i + 1] + getMessageBytes(msg) + 1;
|
|
6309
|
-
}
|
|
6310
|
-
let left = 0;
|
|
6311
|
-
let right = n;
|
|
6312
|
-
while (left < right) {
|
|
6313
|
-
const mid = left + right >>> 1;
|
|
6314
|
-
if (cumTokens[mid] <= availableTokens && cumBytes[mid] <= availableBytes) right = mid;
|
|
6315
|
-
else left = mid + 1;
|
|
6316
|
-
}
|
|
6317
|
-
return left;
|
|
6318
|
-
}
|
|
6319
|
-
/**
|
|
6320
|
-
* Generate a summary of removed messages for context.
|
|
6321
|
-
* Extracts key information like tool calls and topics.
|
|
6322
|
-
*/
|
|
6323
|
-
function generateRemovedMessagesSummary(removedMessages) {
|
|
6324
|
-
const toolCalls = [];
|
|
6325
|
-
let userMessageCount = 0;
|
|
6326
|
-
let assistantMessageCount = 0;
|
|
6327
|
-
for (const msg of removedMessages) {
|
|
6328
|
-
if (msg.role === "user") userMessageCount++;
|
|
6329
|
-
else assistantMessageCount++;
|
|
6330
|
-
if (Array.isArray(msg.content)) {
|
|
6331
|
-
for (const block of msg.content) if (block.type === "tool_use") toolCalls.push(block.name);
|
|
6332
|
-
}
|
|
6333
|
-
}
|
|
6334
|
-
const parts = [];
|
|
6335
|
-
if (userMessageCount > 0 || assistantMessageCount > 0) {
|
|
6336
|
-
const breakdown = [];
|
|
6337
|
-
if (userMessageCount > 0) breakdown.push(`${userMessageCount} user`);
|
|
6338
|
-
if (assistantMessageCount > 0) breakdown.push(`${assistantMessageCount} assistant`);
|
|
6339
|
-
parts.push(`Messages: ${breakdown.join(", ")}`);
|
|
6340
|
-
}
|
|
6341
|
-
if (toolCalls.length > 0) {
|
|
6342
|
-
const uniqueTools = [...new Set(toolCalls)];
|
|
6343
|
-
const displayTools = uniqueTools.length > 5 ? [...uniqueTools.slice(0, 5), `+${uniqueTools.length - 5} more`] : uniqueTools;
|
|
6344
|
-
parts.push(`Tools used: ${displayTools.join(", ")}`);
|
|
6345
|
-
}
|
|
6346
|
-
return parts.join(". ");
|
|
6347
|
-
}
|
|
6348
6343
|
/**
|
|
6349
6344
|
* Add a compression notice to the system prompt.
|
|
6350
6345
|
* Informs the model that some tool_result content has been compressed.
|
|
@@ -6396,7 +6391,7 @@ async function autoTruncateAnthropic(payload, model, config = {}) {
|
|
|
6396
6391
|
...DEFAULT_AUTO_TRUNCATE_CONFIG,
|
|
6397
6392
|
...config
|
|
6398
6393
|
};
|
|
6399
|
-
const { tokenLimit, byteLimit } = calculateLimits(model, cfg);
|
|
6394
|
+
const { tokenLimit, byteLimit } = calculateLimits(model, cfg, 2e5);
|
|
6400
6395
|
const originalBytes = JSON.stringify(payload).length;
|
|
6401
6396
|
const originalTokens = await countTotalTokens(payload, model);
|
|
6402
6397
|
if (originalTokens <= tokenLimit && originalBytes <= byteLimit) return {
|
|
@@ -6449,7 +6444,8 @@ async function autoTruncateAnthropic(payload, model, config = {}) {
|
|
|
6449
6444
|
systemTokens,
|
|
6450
6445
|
payloadOverhead,
|
|
6451
6446
|
tokenLimit,
|
|
6452
|
-
byteLimit
|
|
6447
|
+
byteLimit,
|
|
6448
|
+
estimateTokens: estimateMessageTokens
|
|
6453
6449
|
});
|
|
6454
6450
|
if (preserveIndex === 0) {
|
|
6455
6451
|
consola.warn("[AutoTruncate:Anthropic] Cannot truncate, system messages too large");
|
|
@@ -6474,7 +6470,7 @@ async function autoTruncateAnthropic(payload, model, config = {}) {
|
|
|
6474
6470
|
let preserved = workingMessages.slice(preserveIndex);
|
|
6475
6471
|
preserved = filterOrphanedToolResults(preserved);
|
|
6476
6472
|
preserved = filterOrphanedToolUse(preserved);
|
|
6477
|
-
preserved = ensureStartsWithUser(preserved);
|
|
6473
|
+
preserved = ensureStartsWithUser(preserved, "Anthropic");
|
|
6478
6474
|
preserved = filterOrphanedToolResults(preserved);
|
|
6479
6475
|
preserved = filterOrphanedToolUse(preserved);
|
|
6480
6476
|
if (preserved.length === 0) {
|
|
@@ -6489,7 +6485,10 @@ async function autoTruncateAnthropic(payload, model, config = {}) {
|
|
|
6489
6485
|
}
|
|
6490
6486
|
const removedMessages = payload.messages.slice(0, preserveIndex);
|
|
6491
6487
|
const removedCount = workingMessages.length - preserved.length;
|
|
6492
|
-
const summary = generateRemovedMessagesSummary(removedMessages)
|
|
6488
|
+
const summary = generateRemovedMessagesSummary(removedMessages, (msg) => {
|
|
6489
|
+
if (!Array.isArray(msg.content)) return [];
|
|
6490
|
+
return msg.content.filter((block) => block.type === "tool_use").map((block) => block.name);
|
|
6491
|
+
});
|
|
6493
6492
|
let newSystem = payload.system;
|
|
6494
6493
|
let newMessages = preserved;
|
|
6495
6494
|
if (payload.system !== void 0) {
|
|
@@ -6531,7 +6530,7 @@ async function checkNeedsCompactionAnthropic(payload, model, config = {}) {
|
|
|
6531
6530
|
const { tokenLimit, byteLimit } = calculateLimits(model, {
|
|
6532
6531
|
...DEFAULT_AUTO_TRUNCATE_CONFIG,
|
|
6533
6532
|
...config
|
|
6534
|
-
});
|
|
6533
|
+
}, 2e5);
|
|
6535
6534
|
const currentTokens = await countTotalTokens(payload, model);
|
|
6536
6535
|
const currentBytes = JSON.stringify(payload).length;
|
|
6537
6536
|
const exceedsTokens = currentTokens > tokenLimit;
|
|
@@ -6550,6 +6549,156 @@ async function checkNeedsCompactionAnthropic(payload, model, config = {}) {
|
|
|
6550
6549
|
};
|
|
6551
6550
|
}
|
|
6552
6551
|
|
|
6552
|
+
//#endregion
|
|
6553
|
+
//#region src/lib/anthropic/features.ts
|
|
6554
|
+
function normalizeForMatching(modelId) {
|
|
6555
|
+
return modelId.toLowerCase().replaceAll(/[-_.]/g, "").replace(/\d{8}$/, "");
|
|
6556
|
+
}
|
|
6557
|
+
function modelSupportsContextEditing(modelId) {
|
|
6558
|
+
const n = normalizeForMatching(modelId);
|
|
6559
|
+
return n.includes("claude") && (n.includes("haiku45") || n.includes("sonnet4") || n.includes("sonnet45") || n.includes("sonnet46") || n.includes("opus4") || n.includes("opus41") || n.includes("opus45") || n.includes("opus46"));
|
|
6560
|
+
}
|
|
6561
|
+
function modelSupportsToolSearch(modelId) {
|
|
6562
|
+
const n = normalizeForMatching(modelId);
|
|
6563
|
+
return n.includes("claude") && (n.includes("opus45") || n.includes("opus46") || n.includes("sonnet45") || n.includes("sonnet46"));
|
|
6564
|
+
}
|
|
6565
|
+
function isContextEditingEnabled(modelId) {
|
|
6566
|
+
return modelSupportsContextEditing(modelId) && state.contextEditingMode !== "off";
|
|
6567
|
+
}
|
|
6568
|
+
function modelHasAdaptiveThinking(resolvedModel) {
|
|
6569
|
+
return resolvedModel?.capabilities?.supports?.adaptive_thinking === true;
|
|
6570
|
+
}
|
|
6571
|
+
function buildAnthropicBetaHeaders(modelId, resolvedModel) {
|
|
6572
|
+
const headers = {};
|
|
6573
|
+
const betaFeatures = [];
|
|
6574
|
+
if (!modelHasAdaptiveThinking(resolvedModel)) betaFeatures.push("interleaved-thinking-2025-05-14");
|
|
6575
|
+
if (isContextEditingEnabled(modelId)) betaFeatures.push("context-management-2025-06-27");
|
|
6576
|
+
if (modelSupportsToolSearch(modelId)) betaFeatures.push("advanced-tool-use-2025-11-20");
|
|
6577
|
+
if (betaFeatures.length > 0) headers["anthropic-beta"] = betaFeatures.join(",");
|
|
6578
|
+
return headers;
|
|
6579
|
+
}
|
|
6580
|
+
const THINKING_KEEP_TURNS = 2;
|
|
6581
|
+
const TOOL_USE_TRIGGER_TYPE = "input_tokens";
|
|
6582
|
+
const TOOL_USE_TRIGGER_VALUE = 8e4;
|
|
6583
|
+
const TOOL_USE_KEEP_COUNT = 10;
|
|
6584
|
+
function buildContextManagement(mode, hasThinking) {
|
|
6585
|
+
if (mode === "off") return void 0;
|
|
6586
|
+
const edits = [];
|
|
6587
|
+
if ((mode === "clear-thinking" || mode === "clear-both") && hasThinking) edits.push({
|
|
6588
|
+
type: "clear_thinking_20251015",
|
|
6589
|
+
keep: {
|
|
6590
|
+
type: "thinking_turns",
|
|
6591
|
+
value: Math.max(1, THINKING_KEEP_TURNS)
|
|
6592
|
+
}
|
|
6593
|
+
});
|
|
6594
|
+
if (mode === "clear-tooluse" || mode === "clear-both") edits.push({
|
|
6595
|
+
type: "clear_tool_uses_20250919",
|
|
6596
|
+
trigger: {
|
|
6597
|
+
type: TOOL_USE_TRIGGER_TYPE,
|
|
6598
|
+
value: TOOL_USE_TRIGGER_VALUE
|
|
6599
|
+
},
|
|
6600
|
+
keep: {
|
|
6601
|
+
type: "tool_uses",
|
|
6602
|
+
value: TOOL_USE_KEEP_COUNT
|
|
6603
|
+
}
|
|
6604
|
+
});
|
|
6605
|
+
return edits.length > 0 ? { edits } : void 0;
|
|
6606
|
+
}
|
|
6607
|
+
|
|
6608
|
+
//#endregion
|
|
6609
|
+
//#region src/lib/anthropic/server-tool-filter.ts
|
|
6610
|
+
const SERVER_TOOL_TYPE_PREFIXES = [
|
|
6611
|
+
"web_search_",
|
|
6612
|
+
"web_fetch_",
|
|
6613
|
+
"code_execution_",
|
|
6614
|
+
"text_editor_",
|
|
6615
|
+
"computer_",
|
|
6616
|
+
"bash_"
|
|
6617
|
+
];
|
|
6618
|
+
/** Check if a block type is a server-side tool result (ends with _tool_result, but not plain tool_result) */
|
|
6619
|
+
function isServerToolResultType(type) {
|
|
6620
|
+
return type !== "tool_result" && type.endsWith("_tool_result");
|
|
6621
|
+
}
|
|
6622
|
+
/** Check if a content block is a server-side tool block */
|
|
6623
|
+
function isServerToolBlock(block) {
|
|
6624
|
+
if (block.type === "server_tool_use") return true;
|
|
6625
|
+
return isServerToolResultType(block.type);
|
|
6626
|
+
}
|
|
6627
|
+
/** Check if a tool's type field matches a known server tool prefix */
|
|
6628
|
+
function isServerToolType(type) {
|
|
6629
|
+
if (!type) return false;
|
|
6630
|
+
return SERVER_TOOL_TYPE_PREFIXES.some((prefix) => type.startsWith(prefix));
|
|
6631
|
+
}
|
|
6632
|
+
/** Log a single server tool block */
|
|
6633
|
+
function logServerToolBlock(block) {
|
|
6634
|
+
if (block.type === "server_tool_use") {
|
|
6635
|
+
consola.debug(`[ServerTool] server_tool_use: ${block.name}`);
|
|
6636
|
+
return;
|
|
6637
|
+
}
|
|
6638
|
+
if (!isServerToolResultType(block.type)) return;
|
|
6639
|
+
consola.debug(`[ServerTool] ${block.type}`);
|
|
6640
|
+
}
|
|
6641
|
+
/** Log all server tool blocks from a non-streaming response */
|
|
6642
|
+
function logServerToolBlocks(content) {
|
|
6643
|
+
for (const block of content) logServerToolBlock(block);
|
|
6644
|
+
}
|
|
6645
|
+
/**
|
|
6646
|
+
* Creates a filter for server tool blocks in SSE streams.
|
|
6647
|
+
* Handles index remapping so block indices remain dense/sequential after filtering.
|
|
6648
|
+
* Always active — matching vscode-copilot-chat behavior.
|
|
6649
|
+
*/
|
|
6650
|
+
function createServerToolBlockFilter() {
|
|
6651
|
+
const filteredIndices = /* @__PURE__ */ new Set();
|
|
6652
|
+
const clientIndexMap = /* @__PURE__ */ new Map();
|
|
6653
|
+
let nextClientIndex = 0;
|
|
6654
|
+
function getClientIndex(apiIndex) {
|
|
6655
|
+
let idx = clientIndexMap.get(apiIndex);
|
|
6656
|
+
if (idx === void 0) {
|
|
6657
|
+
idx = nextClientIndex++;
|
|
6658
|
+
clientIndexMap.set(apiIndex, idx);
|
|
6659
|
+
}
|
|
6660
|
+
return idx;
|
|
6661
|
+
}
|
|
6662
|
+
return { rewriteEvent(parsed, rawData) {
|
|
6663
|
+
if (!parsed) return rawData;
|
|
6664
|
+
if (parsed.type === "content_block_start") {
|
|
6665
|
+
const block = parsed.content_block;
|
|
6666
|
+
if (isServerToolBlock(block)) {
|
|
6667
|
+
filteredIndices.add(parsed.index);
|
|
6668
|
+
return null;
|
|
6669
|
+
}
|
|
6670
|
+
if (filteredIndices.size === 0) {
|
|
6671
|
+
getClientIndex(parsed.index);
|
|
6672
|
+
return rawData;
|
|
6673
|
+
}
|
|
6674
|
+
const clientIndex = getClientIndex(parsed.index);
|
|
6675
|
+
if (clientIndex === parsed.index) return rawData;
|
|
6676
|
+
const obj = JSON.parse(rawData);
|
|
6677
|
+
obj.index = clientIndex;
|
|
6678
|
+
return JSON.stringify(obj);
|
|
6679
|
+
}
|
|
6680
|
+
if (parsed.type === "content_block_delta" || parsed.type === "content_block_stop") {
|
|
6681
|
+
if (filteredIndices.has(parsed.index)) return null;
|
|
6682
|
+
if (filteredIndices.size === 0) return rawData;
|
|
6683
|
+
const clientIndex = getClientIndex(parsed.index);
|
|
6684
|
+
if (clientIndex === parsed.index) return rawData;
|
|
6685
|
+
const obj = JSON.parse(rawData);
|
|
6686
|
+
obj.index = clientIndex;
|
|
6687
|
+
return JSON.stringify(obj);
|
|
6688
|
+
}
|
|
6689
|
+
return rawData;
|
|
6690
|
+
} };
|
|
6691
|
+
}
|
|
6692
|
+
/** Filter server tool blocks from a non-streaming response */
|
|
6693
|
+
function filterServerToolBlocksFromResponse(response) {
|
|
6694
|
+
const filtered = response.content.filter((block) => !isServerToolBlock(block));
|
|
6695
|
+
if (filtered.length === response.content.length) return response;
|
|
6696
|
+
return {
|
|
6697
|
+
...response,
|
|
6698
|
+
content: filtered
|
|
6699
|
+
};
|
|
6700
|
+
}
|
|
6701
|
+
|
|
6553
6702
|
//#endregion
|
|
6554
6703
|
//#region src/services/copilot/create-anthropic-messages.ts
|
|
6555
6704
|
/**
|
|
@@ -6574,14 +6723,15 @@ const COPILOT_SUPPORTED_FIELDS = new Set([
|
|
|
6574
6723
|
"tools",
|
|
6575
6724
|
"tool_choice",
|
|
6576
6725
|
"thinking",
|
|
6577
|
-
"service_tier"
|
|
6726
|
+
"service_tier",
|
|
6727
|
+
"context_management"
|
|
6578
6728
|
]);
|
|
6579
6729
|
/**
|
|
6580
6730
|
* Filter payload to only include fields supported by Copilot's Anthropic API.
|
|
6581
6731
|
* This prevents errors like "Extra inputs are not permitted" for unsupported
|
|
6582
6732
|
* fields like `output_config`.
|
|
6583
6733
|
*
|
|
6584
|
-
*
|
|
6734
|
+
* Optionally strips server-side tools when state.stripServerTools is enabled.
|
|
6585
6735
|
*/
|
|
6586
6736
|
function filterPayloadForCopilot(payload) {
|
|
6587
6737
|
const filtered = {};
|
|
@@ -6589,7 +6739,7 @@ function filterPayloadForCopilot(payload) {
|
|
|
6589
6739
|
for (const [key, value] of Object.entries(payload)) if (COPILOT_SUPPORTED_FIELDS.has(key)) filtered[key] = value;
|
|
6590
6740
|
else unsupportedFields.push(key);
|
|
6591
6741
|
if (unsupportedFields.length > 0) consola.debug(`[DirectAnthropic] Filtered unsupported fields: ${unsupportedFields.join(", ")}`);
|
|
6592
|
-
if (filtered.tools) filtered.tools =
|
|
6742
|
+
if (filtered.tools) filtered.tools = stripServerToolsFromPayload(filtered.tools);
|
|
6593
6743
|
return filtered;
|
|
6594
6744
|
}
|
|
6595
6745
|
/**
|
|
@@ -6620,6 +6770,7 @@ async function createAnthropicMessages(payload, options) {
|
|
|
6620
6770
|
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
6621
6771
|
let filteredPayload = filterPayloadForCopilot(payload);
|
|
6622
6772
|
filteredPayload = adjustMaxTokensForThinking(filteredPayload);
|
|
6773
|
+
const resolvedModel = findModelById(filteredPayload.model);
|
|
6623
6774
|
const enableVision = filteredPayload.messages.some((msg) => {
|
|
6624
6775
|
if (typeof msg.content === "string") return false;
|
|
6625
6776
|
return msg.content.some((block) => block.type === "image");
|
|
@@ -6633,6 +6784,16 @@ async function createAnthropicMessages(payload, options) {
|
|
|
6633
6784
|
"X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user"),
|
|
6634
6785
|
"anthropic-version": "2023-06-01"
|
|
6635
6786
|
};
|
|
6787
|
+
const betaHeaders = buildAnthropicBetaHeaders(filteredPayload.model, resolvedModel);
|
|
6788
|
+
Object.assign(headers, betaHeaders);
|
|
6789
|
+
if (isContextEditingEnabled(filteredPayload.model)) {
|
|
6790
|
+
const hasThinking = filteredPayload.thinking?.type === "enabled";
|
|
6791
|
+
const cm = buildContextManagement(state.contextEditingMode, hasThinking);
|
|
6792
|
+
if (cm) {
|
|
6793
|
+
filteredPayload.context_management = cm;
|
|
6794
|
+
consola.debug("[DirectAnthropic] Added context_management:", JSON.stringify(cm));
|
|
6795
|
+
}
|
|
6796
|
+
}
|
|
6636
6797
|
consola.debug("Sending direct Anthropic request to Copilot /v1/messages");
|
|
6637
6798
|
const response = await fetch(`${copilotBaseUrl(state)}/v1/messages`, {
|
|
6638
6799
|
method: "POST",
|
|
@@ -6656,96 +6817,21 @@ async function createAnthropicMessages(payload, options) {
|
|
|
6656
6817
|
if (payload.stream) return events(response);
|
|
6657
6818
|
return await response.json();
|
|
6658
6819
|
}
|
|
6659
|
-
const SERVER_TOOL_CONFIGS = {
|
|
6660
|
-
web_search: {
|
|
6661
|
-
description: "Search the web for current information. Returns web search results that can help answer questions about recent events, current data, or information that may have changed since your knowledge cutoff.",
|
|
6662
|
-
input_schema: {
|
|
6663
|
-
type: "object",
|
|
6664
|
-
properties: { query: {
|
|
6665
|
-
type: "string",
|
|
6666
|
-
description: "The search query"
|
|
6667
|
-
} },
|
|
6668
|
-
required: ["query"]
|
|
6669
|
-
}
|
|
6670
|
-
},
|
|
6671
|
-
web_fetch: {
|
|
6672
|
-
description: "Fetch content from a URL. NOTE: This is a client-side tool - the client must fetch the URL and return the content.",
|
|
6673
|
-
input_schema: {
|
|
6674
|
-
type: "object",
|
|
6675
|
-
properties: { url: {
|
|
6676
|
-
type: "string",
|
|
6677
|
-
description: "The URL to fetch"
|
|
6678
|
-
} },
|
|
6679
|
-
required: ["url"]
|
|
6680
|
-
}
|
|
6681
|
-
},
|
|
6682
|
-
code_execution: {
|
|
6683
|
-
description: "Execute code in a sandbox. NOTE: This is a client-side tool - the client must execute the code.",
|
|
6684
|
-
input_schema: {
|
|
6685
|
-
type: "object",
|
|
6686
|
-
properties: {
|
|
6687
|
-
code: {
|
|
6688
|
-
type: "string",
|
|
6689
|
-
description: "The code to execute"
|
|
6690
|
-
},
|
|
6691
|
-
language: {
|
|
6692
|
-
type: "string",
|
|
6693
|
-
description: "The programming language"
|
|
6694
|
-
}
|
|
6695
|
-
},
|
|
6696
|
-
required: ["code"]
|
|
6697
|
-
}
|
|
6698
|
-
},
|
|
6699
|
-
computer: {
|
|
6700
|
-
description: "Control computer desktop. NOTE: This is a client-side tool - the client must handle computer control.",
|
|
6701
|
-
input_schema: {
|
|
6702
|
-
type: "object",
|
|
6703
|
-
properties: { action: {
|
|
6704
|
-
type: "string",
|
|
6705
|
-
description: "The action to perform"
|
|
6706
|
-
} },
|
|
6707
|
-
required: ["action"]
|
|
6708
|
-
}
|
|
6709
|
-
}
|
|
6710
|
-
};
|
|
6711
|
-
/**
|
|
6712
|
-
* Check if a tool is a server-side tool that needs conversion.
|
|
6713
|
-
*/
|
|
6714
|
-
function getServerToolPrefix(tool) {
|
|
6715
|
-
if (tool.type) {
|
|
6716
|
-
for (const prefix of Object.keys(SERVER_TOOL_CONFIGS)) if (tool.type.startsWith(prefix)) return prefix;
|
|
6717
|
-
}
|
|
6718
|
-
return null;
|
|
6719
|
-
}
|
|
6720
6820
|
/**
|
|
6721
|
-
*
|
|
6722
|
-
*
|
|
6723
|
-
*
|
|
6724
|
-
* Note: Server-side tools are only converted if state.rewriteAnthropicTools is enabled.
|
|
6821
|
+
* Strip server-side tools from the tools array when configured.
|
|
6822
|
+
* When state.stripServerTools is enabled, server tools are removed from the request.
|
|
6823
|
+
* When disabled (default), server tools are passed through unchanged.
|
|
6725
6824
|
*/
|
|
6726
|
-
function
|
|
6727
|
-
if (!tools) return;
|
|
6825
|
+
function stripServerToolsFromPayload(tools) {
|
|
6826
|
+
if (!tools) return void 0;
|
|
6827
|
+
if (!state.stripServerTools) return tools;
|
|
6728
6828
|
const result = [];
|
|
6729
6829
|
for (const tool of tools) {
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
result.push(tool);
|
|
6736
|
-
continue;
|
|
6737
|
-
}
|
|
6738
|
-
if (config.remove) {
|
|
6739
|
-
consola.warn(`[DirectAnthropic] Removing unsupported server tool: ${tool.name}. Reason: ${config.removalReason}`);
|
|
6740
|
-
continue;
|
|
6741
|
-
}
|
|
6742
|
-
consola.debug(`[DirectAnthropic] Converting server tool to custom: ${tool.name} (type: ${tool.type})`);
|
|
6743
|
-
result.push({
|
|
6744
|
-
name: tool.name,
|
|
6745
|
-
description: config.description,
|
|
6746
|
-
input_schema: config.input_schema
|
|
6747
|
-
});
|
|
6748
|
-
} else result.push(tool);
|
|
6830
|
+
if (isServerToolType(tool.type)) {
|
|
6831
|
+
consola.warn(`[DirectAnthropic] Stripping server tool: ${tool.name} (type: ${tool.type})`);
|
|
6832
|
+
continue;
|
|
6833
|
+
}
|
|
6834
|
+
result.push(tool);
|
|
6749
6835
|
}
|
|
6750
6836
|
return result.length > 0 ? result : void 0;
|
|
6751
6837
|
}
|
|
@@ -6755,7 +6841,7 @@ function convertServerToolsToCustom(tools) {
|
|
|
6755
6841
|
*/
|
|
6756
6842
|
function supportsDirectAnthropicApi(modelId) {
|
|
6757
6843
|
if (state.redirectAnthropic) return false;
|
|
6758
|
-
return (
|
|
6844
|
+
return findModelById(modelId)?.vendor === "Anthropic";
|
|
6759
6845
|
}
|
|
6760
6846
|
|
|
6761
6847
|
//#endregion
|
|
@@ -6799,15 +6885,6 @@ function extractSystemPrompt(system) {
|
|
|
6799
6885
|
return system.map((block) => block.text).join("\n");
|
|
6800
6886
|
}
|
|
6801
6887
|
function extractToolCallsFromContent(content) {
|
|
6802
|
-
const tools = [];
|
|
6803
|
-
for (const block of content) if (typeof block === "object" && block !== null && "type" in block && block.type === "tool_use" && "id" in block && "name" in block && "input" in block) tools.push({
|
|
6804
|
-
id: String(block.id),
|
|
6805
|
-
name: String(block.name),
|
|
6806
|
-
input: JSON.stringify(block.input)
|
|
6807
|
-
});
|
|
6808
|
-
return tools.length > 0 ? tools : void 0;
|
|
6809
|
-
}
|
|
6810
|
-
function extractToolCallsFromAnthropicContent(content) {
|
|
6811
6888
|
const tools = [];
|
|
6812
6889
|
for (const block of content) if (block.type === "tool_use") tools.push({
|
|
6813
6890
|
id: block.id,
|
|
@@ -6825,6 +6902,25 @@ function mapOpenAIStopReasonToAnthropic(finishReason) {
|
|
|
6825
6902
|
content_filter: "end_turn"
|
|
6826
6903
|
}[finishReason];
|
|
6827
6904
|
}
|
|
6905
|
+
function prependMarkerToResponse(response, marker) {
|
|
6906
|
+
if (!marker) return response;
|
|
6907
|
+
const content = [...response.content];
|
|
6908
|
+
const firstTextIndex = content.findIndex((block) => block.type === "text");
|
|
6909
|
+
if (firstTextIndex !== -1) {
|
|
6910
|
+
const textBlock = content[firstTextIndex];
|
|
6911
|
+
if (textBlock.type === "text") content[firstTextIndex] = {
|
|
6912
|
+
...textBlock,
|
|
6913
|
+
text: marker + (textBlock.text ?? "")
|
|
6914
|
+
};
|
|
6915
|
+
} else content.unshift({
|
|
6916
|
+
type: "text",
|
|
6917
|
+
text: marker
|
|
6918
|
+
});
|
|
6919
|
+
return {
|
|
6920
|
+
...response,
|
|
6921
|
+
content
|
|
6922
|
+
};
|
|
6923
|
+
}
|
|
6828
6924
|
|
|
6829
6925
|
//#endregion
|
|
6830
6926
|
//#region src/routes/messages/stream-accumulator.ts
|
|
@@ -6840,9 +6936,6 @@ function createAnthropicStreamAccumulator() {
|
|
|
6840
6936
|
currentToolCall: null
|
|
6841
6937
|
};
|
|
6842
6938
|
}
|
|
6843
|
-
function isServerToolResultType(type) {
|
|
6844
|
-
return type !== "tool_result" && type.endsWith("_tool_result");
|
|
6845
|
-
}
|
|
6846
6939
|
function processAnthropicEvent(event, acc) {
|
|
6847
6940
|
switch (event.type) {
|
|
6848
6941
|
case "content_block_delta":
|
|
@@ -7389,7 +7482,7 @@ function translateErrorToAnthropicErrorEvent() {
|
|
|
7389
7482
|
*/
|
|
7390
7483
|
async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiatorOverride) {
|
|
7391
7484
|
consola.debug("Using direct Anthropic API path for model:", anthropicPayload.model);
|
|
7392
|
-
const selectedModel =
|
|
7485
|
+
const selectedModel = findModelById(anthropicPayload.model);
|
|
7393
7486
|
let effectivePayload = anthropicPayload;
|
|
7394
7487
|
let truncateResult;
|
|
7395
7488
|
if (state.autoTruncate && selectedModel) {
|
|
@@ -7472,7 +7565,7 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
|
|
|
7472
7565
|
}
|
|
7473
7566
|
})
|
|
7474
7567
|
},
|
|
7475
|
-
toolCalls:
|
|
7568
|
+
toolCalls: extractToolCallsFromContent(response.content)
|
|
7476
7569
|
}, Date.now() - ctx.startTime);
|
|
7477
7570
|
if (ctx.trackingId) requestTracker.updateRequest(ctx.trackingId, {
|
|
7478
7571
|
inputTokens: response.usage.input_tokens,
|
|
@@ -7490,38 +7583,19 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
|
|
|
7490
7583
|
stopReason: response.stop_reason ?? void 0
|
|
7491
7584
|
});
|
|
7492
7585
|
let finalResponse = response;
|
|
7493
|
-
if (state.verbose && truncateResult?.wasCompacted) finalResponse =
|
|
7586
|
+
if (state.verbose && truncateResult?.wasCompacted) finalResponse = prependMarkerToResponse(response, createTruncationMarker$1(truncateResult));
|
|
7587
|
+
logServerToolBlocks(finalResponse.content);
|
|
7588
|
+
finalResponse = filterServerToolBlocksFromResponse(finalResponse);
|
|
7494
7589
|
return c.json(finalResponse);
|
|
7495
7590
|
}
|
|
7496
7591
|
/**
|
|
7497
|
-
* Prepend marker to Anthropic response content (at the beginning of first text block)
|
|
7498
|
-
*/
|
|
7499
|
-
function prependMarkerToAnthropicResponse$1(response, marker) {
|
|
7500
|
-
if (!marker) return response;
|
|
7501
|
-
const content = [...response.content];
|
|
7502
|
-
const firstTextIndex = content.findIndex((block) => block.type === "text");
|
|
7503
|
-
if (firstTextIndex !== -1) {
|
|
7504
|
-
const textBlock = content[firstTextIndex];
|
|
7505
|
-
if (textBlock.type === "text") content[firstTextIndex] = {
|
|
7506
|
-
...textBlock,
|
|
7507
|
-
text: marker + textBlock.text
|
|
7508
|
-
};
|
|
7509
|
-
} else content.unshift({
|
|
7510
|
-
type: "text",
|
|
7511
|
-
text: marker
|
|
7512
|
-
});
|
|
7513
|
-
return {
|
|
7514
|
-
...response,
|
|
7515
|
-
content
|
|
7516
|
-
};
|
|
7517
|
-
}
|
|
7518
|
-
/**
|
|
7519
7592
|
* Handle streaming direct Anthropic response (passthrough SSE events)
|
|
7520
7593
|
*/
|
|
7521
7594
|
async function handleDirectAnthropicStreamingResponse(opts) {
|
|
7522
7595
|
const { stream, response, anthropicPayload, ctx } = opts;
|
|
7523
7596
|
const acc = createAnthropicStreamAccumulator();
|
|
7524
7597
|
const checkRepetition = createStreamRepetitionChecker(`anthropic:${anthropicPayload.model}`);
|
|
7598
|
+
const serverToolFilter = createServerToolBlockFilter();
|
|
7525
7599
|
try {
|
|
7526
7600
|
for await (const rawEvent of response) {
|
|
7527
7601
|
consola.debug("Direct Anthropic raw stream event:", JSON.stringify(rawEvent));
|
|
@@ -7535,10 +7609,13 @@ async function handleDirectAnthropicStreamingResponse(opts) {
|
|
|
7535
7609
|
continue;
|
|
7536
7610
|
}
|
|
7537
7611
|
processAnthropicEvent(event, acc);
|
|
7612
|
+
if (event.type === "content_block_start") logServerToolBlock(event.content_block);
|
|
7538
7613
|
if (event.type === "content_block_delta" && event.delta.type === "text_delta") checkRepetition(event.delta.text);
|
|
7614
|
+
const forwardData = serverToolFilter.rewriteEvent(event, rawEvent.data);
|
|
7615
|
+
if (forwardData === null) continue;
|
|
7539
7616
|
await stream.writeSSE({
|
|
7540
7617
|
event: rawEvent.event || event.type,
|
|
7541
|
-
data:
|
|
7618
|
+
data: forwardData
|
|
7542
7619
|
});
|
|
7543
7620
|
}
|
|
7544
7621
|
recordAnthropicStreamingResponse(acc, anthropicPayload.model, ctx);
|
|
@@ -7620,7 +7697,7 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
|
|
|
7620
7697
|
const { payload: translatedPayload, toolNameMapping } = translateToOpenAI(anthropicPayload);
|
|
7621
7698
|
consola.debug("Translated OpenAI request payload:", JSON.stringify(translatedPayload));
|
|
7622
7699
|
updateTrackerResolvedModel(ctx.trackingId, translatedPayload.model);
|
|
7623
|
-
const selectedModel =
|
|
7700
|
+
const selectedModel = findModelById(translatedPayload.model);
|
|
7624
7701
|
const { finalPayload: openAIPayload, truncateResult } = await buildFinalPayload(translatedPayload, selectedModel);
|
|
7625
7702
|
if (truncateResult) ctx.truncateResult = truncateResult;
|
|
7626
7703
|
if (state.manualApprove) await awaitApproval();
|
|
@@ -7657,8 +7734,8 @@ function handleNonStreamingResponse(opts) {
|
|
|
7657
7734
|
let anthropicResponse = translateToAnthropic(response, toolNameMapping);
|
|
7658
7735
|
consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
|
|
7659
7736
|
if (state.verbose && ctx.truncateResult?.wasCompacted) {
|
|
7660
|
-
const marker =
|
|
7661
|
-
anthropicResponse =
|
|
7737
|
+
const marker = createTruncationMarker$1(ctx.truncateResult);
|
|
7738
|
+
anthropicResponse = prependMarkerToResponse(anthropicResponse, marker);
|
|
7662
7739
|
}
|
|
7663
7740
|
recordResponse(ctx.historyId, {
|
|
7664
7741
|
success: true,
|
|
@@ -7700,24 +7777,6 @@ function handleNonStreamingResponse(opts) {
|
|
|
7700
7777
|
});
|
|
7701
7778
|
return c.json(anthropicResponse);
|
|
7702
7779
|
}
|
|
7703
|
-
function prependMarkerToAnthropicResponse(response, marker) {
|
|
7704
|
-
const content = [...response.content];
|
|
7705
|
-
const firstTextIndex = content.findIndex((block) => block.type === "text");
|
|
7706
|
-
if (firstTextIndex !== -1) {
|
|
7707
|
-
const textBlock = content[firstTextIndex];
|
|
7708
|
-
if (textBlock.type === "text") content[firstTextIndex] = {
|
|
7709
|
-
...textBlock,
|
|
7710
|
-
text: marker + textBlock.text
|
|
7711
|
-
};
|
|
7712
|
-
} else content.unshift({
|
|
7713
|
-
type: "text",
|
|
7714
|
-
text: marker
|
|
7715
|
-
});
|
|
7716
|
-
return {
|
|
7717
|
-
...response,
|
|
7718
|
-
content
|
|
7719
|
-
};
|
|
7720
|
-
}
|
|
7721
7780
|
async function handleStreamingResponse(opts) {
|
|
7722
7781
|
const { stream, response, toolNameMapping, anthropicPayload, ctx } = opts;
|
|
7723
7782
|
const streamState = {
|
|
@@ -7730,7 +7789,7 @@ async function handleStreamingResponse(opts) {
|
|
|
7730
7789
|
const checkRepetition = createStreamRepetitionChecker(`translated:${anthropicPayload.model}`);
|
|
7731
7790
|
try {
|
|
7732
7791
|
if (ctx.truncateResult?.wasCompacted) {
|
|
7733
|
-
const marker =
|
|
7792
|
+
const marker = createTruncationMarker$1(ctx.truncateResult);
|
|
7734
7793
|
await sendTruncationMarkerEvent(stream, streamState, marker);
|
|
7735
7794
|
acc.content += marker;
|
|
7736
7795
|
}
|
|
@@ -7904,7 +7963,7 @@ async function handleCountTokens(c) {
|
|
|
7904
7963
|
const anthropicPayload = await c.req.json();
|
|
7905
7964
|
anthropicPayload.model = resolveModelFromBetaHeader(anthropicPayload.model, anthropicBeta);
|
|
7906
7965
|
const { payload: openAIPayload } = translateToOpenAI(anthropicPayload);
|
|
7907
|
-
const selectedModel =
|
|
7966
|
+
const selectedModel = findModelById(openAIPayload.model);
|
|
7908
7967
|
if (!selectedModel) {
|
|
7909
7968
|
consola.warn("Model not found, returning default token count");
|
|
7910
7969
|
return c.json({ input_tokens: 1 });
|
|
@@ -8056,6 +8115,38 @@ const handleItemId = (parsed, tracker) => {
|
|
|
8056
8115
|
|
|
8057
8116
|
//#endregion
|
|
8058
8117
|
//#region src/routes/responses/utils.ts
|
|
8118
|
+
const CALL_PREFIX = "call_";
|
|
8119
|
+
const FC_PREFIX = "fc_";
|
|
8120
|
+
/**
|
|
8121
|
+
* Normalize function call IDs in Responses API input.
|
|
8122
|
+
* Converts Chat Completions format `call_xxx` IDs to Responses format `fc_xxx` IDs
|
|
8123
|
+
* on function_call and function_call_output items.
|
|
8124
|
+
*/
|
|
8125
|
+
function normalizeCallIds(payload) {
|
|
8126
|
+
if (typeof payload.input === "string") return payload;
|
|
8127
|
+
let count = 0;
|
|
8128
|
+
const input = payload.input.map((item) => {
|
|
8129
|
+
if ("call_id" in item && typeof item.call_id === "string") {
|
|
8130
|
+
const callId = item.call_id;
|
|
8131
|
+
if (callId.startsWith(CALL_PREFIX)) {
|
|
8132
|
+
count++;
|
|
8133
|
+
return {
|
|
8134
|
+
...item,
|
|
8135
|
+
call_id: FC_PREFIX + callId.slice(5)
|
|
8136
|
+
};
|
|
8137
|
+
}
|
|
8138
|
+
}
|
|
8139
|
+
return item;
|
|
8140
|
+
});
|
|
8141
|
+
if (count > 0) {
|
|
8142
|
+
consola.debug(`[Responses] Normalized ${count} call IDs (call_ → fc_)`);
|
|
8143
|
+
return {
|
|
8144
|
+
...payload,
|
|
8145
|
+
input
|
|
8146
|
+
};
|
|
8147
|
+
}
|
|
8148
|
+
return payload;
|
|
8149
|
+
}
|
|
8059
8150
|
const getResponsesRequestOptions = (payload) => {
|
|
8060
8151
|
return {
|
|
8061
8152
|
vision: hasVisionInput(payload),
|
|
@@ -8206,7 +8297,8 @@ const TERMINAL_EVENTS = new Set([
|
|
|
8206
8297
|
"error"
|
|
8207
8298
|
]);
|
|
8208
8299
|
const handleResponses = async (c) => {
|
|
8209
|
-
|
|
8300
|
+
let payload = await c.req.json();
|
|
8301
|
+
if (state.normalizeResponsesCallIds) payload = normalizeCallIds(payload);
|
|
8210
8302
|
consola.debug("Responses request payload:", JSON.stringify(payload));
|
|
8211
8303
|
const trackingId = c.get("trackingId");
|
|
8212
8304
|
const startTime = (trackingId ? requestTracker.getRequest(trackingId) : void 0)?.startTime ?? Date.now();
|
|
@@ -8230,7 +8322,7 @@ const handleResponses = async (c) => {
|
|
|
8230
8322
|
trackingId,
|
|
8231
8323
|
startTime
|
|
8232
8324
|
};
|
|
8233
|
-
if (!((
|
|
8325
|
+
if (!(findModelById(payload.model)?.supported_endpoints?.includes(RESPONSES_ENDPOINT) ?? false)) {
|
|
8234
8326
|
recordErrorResponse(ctx, model, /* @__PURE__ */ new Error("This model does not support the responses endpoint."));
|
|
8235
8327
|
return c.json({ error: {
|
|
8236
8328
|
message: "This model does not support the responses endpoint. Please choose a different model.",
|
|
@@ -8485,7 +8577,8 @@ async function runServer(options) {
|
|
|
8485
8577
|
state.autoTruncate = options.autoTruncate;
|
|
8486
8578
|
state.compressToolResults = options.compressToolResults;
|
|
8487
8579
|
state.redirectAnthropic = options.redirectAnthropic;
|
|
8488
|
-
state.
|
|
8580
|
+
state.stripServerTools = options.stripServerTools;
|
|
8581
|
+
state.contextEditingMode = options.contextEditing;
|
|
8489
8582
|
state.timezoneOffset = options.timezoneOffset;
|
|
8490
8583
|
if (options.rateLimit) initAdaptiveRateLimiter({
|
|
8491
8584
|
baseRetryIntervalSeconds: options.retryInterval,
|
|
@@ -8497,11 +8590,13 @@ async function runServer(options) {
|
|
|
8497
8590
|
if (!options.autoTruncate) consola.info("Auto-truncate disabled");
|
|
8498
8591
|
if (options.compressToolResults) consola.info("Tool result compression enabled");
|
|
8499
8592
|
if (options.redirectAnthropic) consola.info("Anthropic API redirect enabled (using OpenAI translation)");
|
|
8500
|
-
if (
|
|
8593
|
+
if (options.stripServerTools) consola.info("Server-side tools will be stripped from requests");
|
|
8594
|
+
if (options.contextEditing !== "off") consola.info(`Context editing mode: ${options.contextEditing}`);
|
|
8501
8595
|
initHistory(options.history, options.historyLimit);
|
|
8502
8596
|
if (options.history) {
|
|
8503
8597
|
const limitText = options.historyLimit === 0 ? "unlimited" : `max ${options.historyLimit}`;
|
|
8504
8598
|
consola.info(`History recording enabled (${limitText} entries)`);
|
|
8599
|
+
startMemoryPressureMonitor();
|
|
8505
8600
|
}
|
|
8506
8601
|
if (options.posthogKey) {
|
|
8507
8602
|
initPostHog(options.posthogKey);
|
|
@@ -8567,6 +8662,17 @@ function parseTimezoneOffset(value) {
|
|
|
8567
8662
|
if (!Number.isFinite(n)) return 8;
|
|
8568
8663
|
return n;
|
|
8569
8664
|
}
|
|
8665
|
+
const validContextEditingModes = [
|
|
8666
|
+
"off",
|
|
8667
|
+
"clear-thinking",
|
|
8668
|
+
"clear-tooluse",
|
|
8669
|
+
"clear-both"
|
|
8670
|
+
];
|
|
8671
|
+
function parseContextEditing(value) {
|
|
8672
|
+
if (validContextEditingModes.includes(value)) return value;
|
|
8673
|
+
consola.warn(`Invalid context editing mode: "${value}", using "off". Valid: ${validContextEditingModes.join(", ")}`);
|
|
8674
|
+
return "off";
|
|
8675
|
+
}
|
|
8570
8676
|
const start = defineCommand({
|
|
8571
8677
|
meta: {
|
|
8572
8678
|
name: "start",
|
|
@@ -8672,10 +8778,15 @@ const start = defineCommand({
|
|
|
8672
8778
|
default: false,
|
|
8673
8779
|
description: "Redirect Anthropic models through OpenAI translation (instead of direct API)"
|
|
8674
8780
|
},
|
|
8675
|
-
"
|
|
8781
|
+
"strip-server-tools": {
|
|
8676
8782
|
type: "boolean",
|
|
8677
8783
|
default: false,
|
|
8678
|
-
description: "
|
|
8784
|
+
description: "Strip Anthropic server-side tools (web_search, etc.) from requests"
|
|
8785
|
+
},
|
|
8786
|
+
"context-editing": {
|
|
8787
|
+
type: "string",
|
|
8788
|
+
default: "off",
|
|
8789
|
+
description: "Context editing mode: off, clear-thinking, clear-tooluse, clear-both"
|
|
8679
8790
|
},
|
|
8680
8791
|
"timezone-offset": {
|
|
8681
8792
|
type: "string",
|
|
@@ -8708,7 +8819,8 @@ const start = defineCommand({
|
|
|
8708
8819
|
autoTruncate: !args["no-auto-truncate"],
|
|
8709
8820
|
compressToolResults: args["compress-tool-results"],
|
|
8710
8821
|
redirectAnthropic: args["redirect-anthropic"],
|
|
8711
|
-
|
|
8822
|
+
stripServerTools: args["strip-server-tools"],
|
|
8823
|
+
contextEditing: parseContextEditing(args["context-editing"]),
|
|
8712
8824
|
timezoneOffset: parseTimezoneOffset(args["timezone-offset"]),
|
|
8713
8825
|
posthogKey: args["posthog-key"]
|
|
8714
8826
|
});
|