@dianshuv/copilot-api 0.6.3 → 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/dist/main.mjs +409 -545
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -190,10 +190,6 @@ const GITHUB_APP_SCOPES = ["read:user"].join(" ");
|
|
|
190
190
|
|
|
191
191
|
//#endregion
|
|
192
192
|
//#region src/lib/auto-truncate-common.ts
|
|
193
|
-
/**
|
|
194
|
-
* Common types and configuration for auto-truncate modules.
|
|
195
|
-
* Shared between OpenAI and Anthropic format handlers.
|
|
196
|
-
*/
|
|
197
193
|
const DEFAULT_AUTO_TRUNCATE_CONFIG = {
|
|
198
194
|
safetyMarginPercent: 2,
|
|
199
195
|
maxRequestBodyBytes: Infinity,
|
|
@@ -234,6 +230,99 @@ function onTokenLimitExceeded(modelId, reportedLimit) {
|
|
|
234
230
|
function getEffectiveTokenLimit(modelId) {
|
|
235
231
|
return dynamicTokenLimits.get(modelId) ?? null;
|
|
236
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
|
+
}
|
|
237
326
|
|
|
238
327
|
//#endregion
|
|
239
328
|
//#region src/lib/error.ts
|
|
@@ -431,6 +520,9 @@ const sleep = (ms) => new Promise((resolve) => {
|
|
|
431
520
|
setTimeout(resolve, ms);
|
|
432
521
|
});
|
|
433
522
|
const isNullish = (value) => value === null || value === void 0;
|
|
523
|
+
function findModelById(modelId) {
|
|
524
|
+
return state.models?.data.find((m) => m.id === modelId);
|
|
525
|
+
}
|
|
434
526
|
async function cacheModels() {
|
|
435
527
|
state.models = await getModels();
|
|
436
528
|
}
|
|
@@ -1121,7 +1213,7 @@ const patchClaude = defineCommand({
|
|
|
1121
1213
|
|
|
1122
1214
|
//#endregion
|
|
1123
1215
|
//#region package.json
|
|
1124
|
-
var version = "0.
|
|
1216
|
+
var version = "0.7.0";
|
|
1125
1217
|
|
|
1126
1218
|
//#endregion
|
|
1127
1219
|
//#region src/lib/adaptive-rate-limiter.ts
|
|
@@ -1729,12 +1821,14 @@ const historyState = {
|
|
|
1729
1821
|
maxEntries: 1e3,
|
|
1730
1822
|
sessionTimeoutMs: 1800 * 1e3
|
|
1731
1823
|
};
|
|
1824
|
+
const entryIndex = /* @__PURE__ */ new Map();
|
|
1732
1825
|
function initHistory(enabled, maxEntries) {
|
|
1733
1826
|
historyState.enabled = enabled;
|
|
1734
1827
|
historyState.maxEntries = maxEntries;
|
|
1735
1828
|
historyState.entries = [];
|
|
1736
1829
|
historyState.sessions = /* @__PURE__ */ new Map();
|
|
1737
1830
|
historyState.currentSessionId = enabled ? generateId$1() : "";
|
|
1831
|
+
entryIndex.clear();
|
|
1738
1832
|
}
|
|
1739
1833
|
function isHistoryEnabled() {
|
|
1740
1834
|
return historyState.enabled;
|
|
@@ -1783,6 +1877,7 @@ function recordRequest(endpoint, request) {
|
|
|
1783
1877
|
}
|
|
1784
1878
|
};
|
|
1785
1879
|
historyState.entries.push(entry);
|
|
1880
|
+
entryIndex.set(entry.id, entry);
|
|
1786
1881
|
session.requestCount++;
|
|
1787
1882
|
if (!session.models.includes(request.model)) session.models.push(request.model);
|
|
1788
1883
|
if (request.tools && request.tools.length > 0) {
|
|
@@ -1792,6 +1887,7 @@ function recordRequest(endpoint, request) {
|
|
|
1792
1887
|
while (historyState.maxEntries > 0 && historyState.entries.length > historyState.maxEntries) {
|
|
1793
1888
|
const removed = historyState.entries.shift();
|
|
1794
1889
|
if (removed) {
|
|
1890
|
+
entryIndex.delete(removed.id);
|
|
1795
1891
|
if (historyState.entries.filter((e) => e.sessionId === removed.sessionId).length === 0) historyState.sessions.delete(removed.sessionId);
|
|
1796
1892
|
}
|
|
1797
1893
|
}
|
|
@@ -1806,7 +1902,7 @@ function recordRequest(endpoint, request) {
|
|
|
1806
1902
|
}
|
|
1807
1903
|
function recordResponse(id, response, durationMs) {
|
|
1808
1904
|
if (!historyState.enabled || !id) return;
|
|
1809
|
-
const entry =
|
|
1905
|
+
const entry = entryIndex.get(id);
|
|
1810
1906
|
if (entry) {
|
|
1811
1907
|
entry.response = response;
|
|
1812
1908
|
entry.durationMs = durationMs;
|
|
@@ -1885,7 +1981,7 @@ function getHistory(options = {}) {
|
|
|
1885
1981
|
};
|
|
1886
1982
|
}
|
|
1887
1983
|
function getEntry(id) {
|
|
1888
|
-
return
|
|
1984
|
+
return entryIndex.get(id);
|
|
1889
1985
|
}
|
|
1890
1986
|
function getSessions() {
|
|
1891
1987
|
const sessions = Array.from(historyState.sessions.values()).sort((a, b) => b.lastActivity - a.lastActivity);
|
|
@@ -1904,11 +2000,14 @@ function clearHistory() {
|
|
|
1904
2000
|
historyState.entries = [];
|
|
1905
2001
|
historyState.sessions = /* @__PURE__ */ new Map();
|
|
1906
2002
|
historyState.currentSessionId = generateId$1();
|
|
2003
|
+
entryIndex.clear();
|
|
1907
2004
|
notifyHistoryCleared();
|
|
1908
2005
|
}
|
|
1909
2006
|
function deleteSession(sessionId) {
|
|
1910
2007
|
if (!historyState.sessions.has(sessionId)) return false;
|
|
2008
|
+
const removedEntries = historyState.entries.filter((e) => e.sessionId === sessionId);
|
|
1911
2009
|
historyState.entries = historyState.entries.filter((e) => e.sessionId !== sessionId);
|
|
2010
|
+
for (const e of removedEntries) entryIndex.delete(e.id);
|
|
1912
2011
|
historyState.sessions.delete(sessionId);
|
|
1913
2012
|
if (historyState.currentSessionId === sessionId) historyState.currentSessionId = generateId$1();
|
|
1914
2013
|
notifySessionDeleted(sessionId);
|
|
@@ -2010,6 +2109,7 @@ function evictOldestEntries(count) {
|
|
|
2010
2109
|
if (count <= 0) return 0;
|
|
2011
2110
|
const actual = Math.min(count, historyState.entries.length);
|
|
2012
2111
|
const removed = historyState.entries.splice(0, actual);
|
|
2112
|
+
for (const e of removed) entryIndex.delete(e.id);
|
|
2013
2113
|
for (const entry of removed) if (!historyState.entries.some((e) => e.sessionId === entry.sessionId)) historyState.sessions.delete(entry.sessionId);
|
|
2014
2114
|
return actual;
|
|
2015
2115
|
}
|
|
@@ -2797,6 +2897,147 @@ const awaitApproval = async () => {
|
|
|
2797
2897
|
if (!await consola.prompt(`Accept incoming request?`, { type: "confirm" })) throw new HTTPError("Request rejected", 403, JSON.stringify({ message: "Request rejected" }));
|
|
2798
2898
|
};
|
|
2799
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
|
+
|
|
2800
3041
|
//#endregion
|
|
2801
3042
|
//#region src/lib/tokenizer.ts
|
|
2802
3043
|
const ENCODING_MAP = {
|
|
@@ -3009,6 +3250,32 @@ const getTokenCount = async (payload, model) => {
|
|
|
3009
3250
|
};
|
|
3010
3251
|
};
|
|
3011
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
|
+
|
|
3012
3279
|
//#endregion
|
|
3013
3280
|
//#region src/lib/auto-truncate-openai.ts
|
|
3014
3281
|
/**
|
|
@@ -3023,13 +3290,6 @@ const getTokenCount = async (payload, model) => {
|
|
|
3023
3290
|
* - Dynamic byte limit adjustment on 413 errors
|
|
3024
3291
|
* - Optional smart compression of old tool_result content
|
|
3025
3292
|
*/
|
|
3026
|
-
function calculateLimits$1(model, config) {
|
|
3027
|
-
const rawTokenLimit = getEffectiveTokenLimit(model.id) ?? model.capabilities?.limits?.max_context_window_tokens ?? model.capabilities?.limits?.max_prompt_tokens ?? 128e3;
|
|
3028
|
-
return {
|
|
3029
|
-
tokenLimit: Math.floor(rawTokenLimit * (1 - config.safetyMarginPercent / 100)),
|
|
3030
|
-
byteLimit: getEffectiveByteLimitBytes()
|
|
3031
|
-
};
|
|
3032
|
-
}
|
|
3033
3293
|
/** Estimate tokens for a single message (fast approximation) */
|
|
3034
3294
|
function estimateMessageTokens$1(msg) {
|
|
3035
3295
|
let charCount = 0;
|
|
@@ -3041,10 +3301,6 @@ function estimateMessageTokens$1(msg) {
|
|
|
3041
3301
|
if (msg.tool_calls) charCount += JSON.stringify(msg.tool_calls).length;
|
|
3042
3302
|
return Math.ceil(charCount / 4) + 10;
|
|
3043
3303
|
}
|
|
3044
|
-
/** Get byte size of a message */
|
|
3045
|
-
function getMessageBytes$1(msg) {
|
|
3046
|
-
return JSON.stringify(msg).length;
|
|
3047
|
-
}
|
|
3048
3304
|
/** Extract system/developer messages from the beginning */
|
|
3049
3305
|
function extractSystemMessages(messages) {
|
|
3050
3306
|
let splitIndex = 0;
|
|
@@ -3116,28 +3372,6 @@ function filterOrphanedToolUse$1(messages) {
|
|
|
3116
3372
|
if (removedCount > 0) consola.debug(`[AutoTruncate:OpenAI] Filtered ${removedCount} orphaned tool_use`);
|
|
3117
3373
|
return result;
|
|
3118
3374
|
}
|
|
3119
|
-
/** Ensure messages start with a user message */
|
|
3120
|
-
function ensureStartsWithUser$1(messages) {
|
|
3121
|
-
let startIndex = 0;
|
|
3122
|
-
while (startIndex < messages.length && messages[startIndex].role !== "user") startIndex++;
|
|
3123
|
-
if (startIndex > 0) consola.debug(`[AutoTruncate:OpenAI] Skipped ${startIndex} leading non-user messages`);
|
|
3124
|
-
return messages.slice(startIndex);
|
|
3125
|
-
}
|
|
3126
|
-
/** Threshold for large tool message content (bytes) */
|
|
3127
|
-
const LARGE_TOOL_RESULT_THRESHOLD$1 = 1e4;
|
|
3128
|
-
/** Maximum length for compressed tool_result summary */
|
|
3129
|
-
const COMPRESSED_SUMMARY_LENGTH$1 = 500;
|
|
3130
|
-
/**
|
|
3131
|
-
* Compress a large tool message content to a summary.
|
|
3132
|
-
* Keeps the first and last portions with a note about truncation.
|
|
3133
|
-
*/
|
|
3134
|
-
function compressToolResultContent$1(content) {
|
|
3135
|
-
if (content.length <= LARGE_TOOL_RESULT_THRESHOLD$1) return content;
|
|
3136
|
-
const halfLen = Math.floor(COMPRESSED_SUMMARY_LENGTH$1 / 2);
|
|
3137
|
-
const start = content.slice(0, halfLen);
|
|
3138
|
-
const end = content.slice(-halfLen);
|
|
3139
|
-
return `${start}\n\n[... ${(content.length - COMPRESSED_SUMMARY_LENGTH$1).toLocaleString()} characters omitted for brevity ...]\n\n${end}`;
|
|
3140
|
-
}
|
|
3141
3375
|
/**
|
|
3142
3376
|
* Smart compression strategy for OpenAI format:
|
|
3143
3377
|
* 1. Calculate tokens/bytes from the end until reaching preservePercent of limit
|
|
@@ -3147,37 +3381,20 @@ function compressToolResultContent$1(content) {
|
|
|
3147
3381
|
* @param preservePercent - Percentage of context to preserve uncompressed (0.0-1.0)
|
|
3148
3382
|
*/
|
|
3149
3383
|
function smartCompressToolResults$1(messages, tokenLimit, byteLimit, preservePercent) {
|
|
3150
|
-
const
|
|
3151
|
-
|
|
3152
|
-
const cumBytes = Array.from({ length: n + 1 }, () => 0);
|
|
3153
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
3154
|
-
const msg = messages[i];
|
|
3155
|
-
cumTokens[i] = cumTokens[i + 1] + estimateMessageTokens$1(msg);
|
|
3156
|
-
cumBytes[i] = cumBytes[i + 1] + getMessageBytes$1(msg) + 1;
|
|
3157
|
-
}
|
|
3158
|
-
const preserveTokenLimit = Math.floor(tokenLimit * preservePercent);
|
|
3159
|
-
const preserveByteLimit = Math.floor(byteLimit * preservePercent);
|
|
3160
|
-
let thresholdIndex = n;
|
|
3161
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
3162
|
-
if (cumTokens[i] > preserveTokenLimit || cumBytes[i] > preserveByteLimit) {
|
|
3163
|
-
thresholdIndex = i + 1;
|
|
3164
|
-
break;
|
|
3165
|
-
}
|
|
3166
|
-
thresholdIndex = i;
|
|
3167
|
-
}
|
|
3168
|
-
if (thresholdIndex >= n) return {
|
|
3384
|
+
const thresholdIndex = findCompressThreshold(messages, tokenLimit, byteLimit, preservePercent, estimateMessageTokens$1);
|
|
3385
|
+
if (thresholdIndex >= messages.length) return {
|
|
3169
3386
|
messages,
|
|
3170
3387
|
compressedCount: 0,
|
|
3171
|
-
compressThresholdIndex:
|
|
3388
|
+
compressThresholdIndex: messages.length
|
|
3172
3389
|
};
|
|
3173
3390
|
const result = [];
|
|
3174
3391
|
let compressedCount = 0;
|
|
3175
3392
|
for (const [i, msg] of messages.entries()) {
|
|
3176
|
-
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) {
|
|
3177
3394
|
compressedCount++;
|
|
3178
3395
|
result.push({
|
|
3179
3396
|
...msg,
|
|
3180
|
-
content: compressToolResultContent
|
|
3397
|
+
content: compressToolResultContent(msg.content)
|
|
3181
3398
|
});
|
|
3182
3399
|
continue;
|
|
3183
3400
|
}
|
|
@@ -3190,42 +3407,13 @@ function smartCompressToolResults$1(messages, tokenLimit, byteLimit, preservePer
|
|
|
3190
3407
|
};
|
|
3191
3408
|
}
|
|
3192
3409
|
/**
|
|
3193
|
-
* Find the optimal index from which to preserve messages.
|
|
3194
|
-
* Uses binary search with pre-calculated cumulative sums.
|
|
3195
|
-
* Returns the smallest index where the preserved portion fits within limits.
|
|
3196
|
-
*/
|
|
3197
|
-
function findOptimalPreserveIndex$1(params) {
|
|
3198
|
-
const { messages, systemBytes, systemTokens, payloadOverhead, tokenLimit, byteLimit } = params;
|
|
3199
|
-
if (messages.length === 0) return 0;
|
|
3200
|
-
const markerBytes = 200;
|
|
3201
|
-
const availableTokens = tokenLimit - systemTokens - 50;
|
|
3202
|
-
const availableBytes = byteLimit - payloadOverhead - systemBytes - markerBytes;
|
|
3203
|
-
if (availableTokens <= 0 || availableBytes <= 0) return messages.length;
|
|
3204
|
-
const n = messages.length;
|
|
3205
|
-
const cumTokens = Array.from({ length: n + 1 }, () => 0);
|
|
3206
|
-
const cumBytes = Array.from({ length: n + 1 }, () => 0);
|
|
3207
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
3208
|
-
const msg = messages[i];
|
|
3209
|
-
cumTokens[i] = cumTokens[i + 1] + estimateMessageTokens$1(msg);
|
|
3210
|
-
cumBytes[i] = cumBytes[i + 1] + getMessageBytes$1(msg) + 1;
|
|
3211
|
-
}
|
|
3212
|
-
let left = 0;
|
|
3213
|
-
let right = n;
|
|
3214
|
-
while (left < right) {
|
|
3215
|
-
const mid = left + right >>> 1;
|
|
3216
|
-
if (cumTokens[mid] <= availableTokens && cumBytes[mid] <= availableBytes) right = mid;
|
|
3217
|
-
else left = mid + 1;
|
|
3218
|
-
}
|
|
3219
|
-
return left;
|
|
3220
|
-
}
|
|
3221
|
-
/**
|
|
3222
3410
|
* Check if payload needs compaction based on model limits or byte size.
|
|
3223
3411
|
*/
|
|
3224
3412
|
async function checkNeedsCompactionOpenAI(payload, model, config = {}) {
|
|
3225
|
-
const { tokenLimit, byteLimit } = calculateLimits
|
|
3413
|
+
const { tokenLimit, byteLimit } = calculateLimits(model, {
|
|
3226
3414
|
...DEFAULT_AUTO_TRUNCATE_CONFIG,
|
|
3227
3415
|
...config
|
|
3228
|
-
});
|
|
3416
|
+
}, 128e3);
|
|
3229
3417
|
const currentTokens = (await getTokenCount(payload, model)).input;
|
|
3230
3418
|
const currentBytes = JSON.stringify(payload).length;
|
|
3231
3419
|
const exceedsTokens = currentTokens > tokenLimit;
|
|
@@ -3244,35 +3432,6 @@ async function checkNeedsCompactionOpenAI(payload, model, config = {}) {
|
|
|
3244
3432
|
};
|
|
3245
3433
|
}
|
|
3246
3434
|
/**
|
|
3247
|
-
* Generate a summary of removed messages for context.
|
|
3248
|
-
* Extracts key information like tool calls and topics.
|
|
3249
|
-
*/
|
|
3250
|
-
function generateRemovedMessagesSummary$1(removedMessages) {
|
|
3251
|
-
const toolCalls = [];
|
|
3252
|
-
let userMessageCount = 0;
|
|
3253
|
-
let assistantMessageCount = 0;
|
|
3254
|
-
for (const msg of removedMessages) {
|
|
3255
|
-
if (msg.role === "user") userMessageCount++;
|
|
3256
|
-
else if (msg.role === "assistant") assistantMessageCount++;
|
|
3257
|
-
if (msg.tool_calls) {
|
|
3258
|
-
for (const tc of msg.tool_calls) if (tc.function.name) toolCalls.push(tc.function.name);
|
|
3259
|
-
}
|
|
3260
|
-
}
|
|
3261
|
-
const parts = [];
|
|
3262
|
-
if (userMessageCount > 0 || assistantMessageCount > 0) {
|
|
3263
|
-
const breakdown = [];
|
|
3264
|
-
if (userMessageCount > 0) breakdown.push(`${userMessageCount} user`);
|
|
3265
|
-
if (assistantMessageCount > 0) breakdown.push(`${assistantMessageCount} assistant`);
|
|
3266
|
-
parts.push(`Messages: ${breakdown.join(", ")}`);
|
|
3267
|
-
}
|
|
3268
|
-
if (toolCalls.length > 0) {
|
|
3269
|
-
const uniqueTools = [...new Set(toolCalls)];
|
|
3270
|
-
const displayTools = uniqueTools.length > 5 ? [...uniqueTools.slice(0, 5), `+${uniqueTools.length - 5} more`] : uniqueTools;
|
|
3271
|
-
parts.push(`Tools used: ${displayTools.join(", ")}`);
|
|
3272
|
-
}
|
|
3273
|
-
return parts.join(". ");
|
|
3274
|
-
}
|
|
3275
|
-
/**
|
|
3276
3435
|
* Add a compression notice to the system message.
|
|
3277
3436
|
* Informs the model that some tool content has been compressed.
|
|
3278
3437
|
*/
|
|
@@ -3326,7 +3485,7 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
|
|
|
3326
3485
|
...DEFAULT_AUTO_TRUNCATE_CONFIG,
|
|
3327
3486
|
...config
|
|
3328
3487
|
};
|
|
3329
|
-
const { tokenLimit, byteLimit } = calculateLimits
|
|
3488
|
+
const { tokenLimit, byteLimit } = calculateLimits(model, cfg, 128e3);
|
|
3330
3489
|
const originalBytes = JSON.stringify(payload).length;
|
|
3331
3490
|
const originalTokens = (await getTokenCount(payload, model)).input;
|
|
3332
3491
|
if (originalTokens <= tokenLimit && originalBytes <= byteLimit) return {
|
|
@@ -3371,16 +3530,17 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
|
|
|
3371
3530
|
...payload,
|
|
3372
3531
|
messages: workingMessages
|
|
3373
3532
|
}).length - messagesJson.length;
|
|
3374
|
-
const systemBytes = systemMessages.reduce((sum, m) => sum + getMessageBytes
|
|
3533
|
+
const systemBytes = systemMessages.reduce((sum, m) => sum + getMessageBytes(m) + 1, 0);
|
|
3375
3534
|
const systemTokens = systemMessages.reduce((sum, m) => sum + estimateMessageTokens$1(m), 0);
|
|
3376
3535
|
consola.debug(`[AutoTruncate:OpenAI] overhead=${Math.round(payloadOverhead / 1024)}KB, system=${systemMessages.length} msgs (${Math.round(systemBytes / 1024)}KB)`);
|
|
3377
|
-
const preserveIndex = findOptimalPreserveIndex
|
|
3536
|
+
const preserveIndex = findOptimalPreserveIndex({
|
|
3378
3537
|
messages: conversationMessages,
|
|
3379
3538
|
systemBytes,
|
|
3380
3539
|
systemTokens,
|
|
3381
3540
|
payloadOverhead,
|
|
3382
3541
|
tokenLimit,
|
|
3383
|
-
byteLimit
|
|
3542
|
+
byteLimit,
|
|
3543
|
+
estimateTokens: estimateMessageTokens$1
|
|
3384
3544
|
});
|
|
3385
3545
|
if (preserveIndex === 0) {
|
|
3386
3546
|
consola.warn("[AutoTruncate:OpenAI] Cannot truncate, system messages too large");
|
|
@@ -3390,248 +3550,72 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
|
|
|
3390
3550
|
originalTokens,
|
|
3391
3551
|
compactedTokens: originalTokens,
|
|
3392
3552
|
removedMessageCount: 0
|
|
3393
|
-
};
|
|
3394
|
-
}
|
|
3395
|
-
if (preserveIndex >= conversationMessages.length) {
|
|
3396
|
-
consola.warn("[AutoTruncate:OpenAI] Would need to remove all messages");
|
|
3397
|
-
return {
|
|
3398
|
-
payload,
|
|
3399
|
-
wasCompacted: false,
|
|
3400
|
-
originalTokens,
|
|
3401
|
-
compactedTokens: originalTokens,
|
|
3402
|
-
removedMessageCount: 0
|
|
3403
|
-
};
|
|
3404
|
-
}
|
|
3405
|
-
let preserved = conversationMessages.slice(preserveIndex);
|
|
3406
|
-
preserved = filterOrphanedToolResults$1(preserved);
|
|
3407
|
-
preserved = filterOrphanedToolUse$1(preserved);
|
|
3408
|
-
preserved = ensureStartsWithUser$1(preserved);
|
|
3409
|
-
preserved = filterOrphanedToolResults$1(preserved);
|
|
3410
|
-
preserved = filterOrphanedToolUse$1(preserved);
|
|
3411
|
-
if (preserved.length === 0) {
|
|
3412
|
-
consola.warn("[AutoTruncate:OpenAI] All messages filtered out after cleanup");
|
|
3413
|
-
return {
|
|
3414
|
-
payload,
|
|
3415
|
-
wasCompacted: false,
|
|
3416
|
-
originalTokens,
|
|
3417
|
-
compactedTokens: originalTokens,
|
|
3418
|
-
removedMessageCount: 0
|
|
3419
|
-
};
|
|
3420
|
-
}
|
|
3421
|
-
const removedMessages = conversationMessages.slice(0, preserveIndex);
|
|
3422
|
-
const removedCount = conversationMessages.length - preserved.length;
|
|
3423
|
-
const summary = generateRemovedMessagesSummary$1(removedMessages);
|
|
3424
|
-
let newSystemMessages = systemMessages;
|
|
3425
|
-
let newMessages = preserved;
|
|
3426
|
-
if (systemMessages.length > 0) {
|
|
3427
|
-
const truncationContext = createTruncationSystemContext$1(removedCount, compressedCount, summary);
|
|
3428
|
-
const lastSystemIdx = systemMessages.length - 1;
|
|
3429
|
-
const lastSystem = systemMessages[lastSystemIdx];
|
|
3430
|
-
const updatedSystem = {
|
|
3431
|
-
...lastSystem,
|
|
3432
|
-
content: typeof lastSystem.content === "string" ? lastSystem.content + truncationContext : lastSystem.content
|
|
3433
|
-
};
|
|
3434
|
-
newSystemMessages = [...systemMessages.slice(0, lastSystemIdx), updatedSystem];
|
|
3435
|
-
} else newMessages = [createTruncationMarker$2(removedCount, compressedCount, summary), ...preserved];
|
|
3436
|
-
const newPayload = {
|
|
3437
|
-
...payload,
|
|
3438
|
-
messages: [...newSystemMessages, ...newMessages]
|
|
3439
|
-
};
|
|
3440
|
-
const newBytes = JSON.stringify(newPayload).length;
|
|
3441
|
-
const newTokenCount = await getTokenCount(newPayload, model);
|
|
3442
|
-
let reason = "tokens";
|
|
3443
|
-
if (exceedsTokens && exceedsBytes) reason = "tokens+size";
|
|
3444
|
-
else if (exceedsBytes) reason = "size";
|
|
3445
|
-
const actions = [];
|
|
3446
|
-
if (removedCount > 0) actions.push(`removed ${removedCount} msgs`);
|
|
3447
|
-
if (compressedCount > 0) actions.push(`compressed ${compressedCount} tool_results`);
|
|
3448
|
-
const actionInfo = actions.length > 0 ? ` (${actions.join(", ")})` : "";
|
|
3449
|
-
consola.info(`[AutoTruncate:OpenAI] ${reason}: ${originalTokens}→${newTokenCount.input} tokens, ${Math.round(originalBytes / 1024)}→${Math.round(newBytes / 1024)}KB${actionInfo}`);
|
|
3450
|
-
if (newBytes > byteLimit) consola.warn(`[AutoTruncate:OpenAI] Result still over byte limit (${Math.round(newBytes / 1024)}KB > ${Math.round(byteLimit / 1024)}KB)`);
|
|
3451
|
-
return {
|
|
3452
|
-
payload: newPayload,
|
|
3453
|
-
wasCompacted: true,
|
|
3454
|
-
originalTokens,
|
|
3455
|
-
compactedTokens: newTokenCount.input,
|
|
3456
|
-
removedMessageCount: removedCount
|
|
3457
|
-
};
|
|
3458
|
-
}
|
|
3459
|
-
/**
|
|
3460
|
-
* Create a marker to prepend to responses indicating auto-truncation occurred.
|
|
3461
|
-
*/
|
|
3462
|
-
function createTruncationResponseMarkerOpenAI(result) {
|
|
3463
|
-
if (!result.wasCompacted) return "";
|
|
3464
|
-
const reduction = result.originalTokens - result.compactedTokens;
|
|
3465
|
-
const percentage = Math.round(reduction / result.originalTokens * 100);
|
|
3466
|
-
return `\n\n---\n[Auto-truncated: ${result.removedMessageCount} messages removed, ${result.originalTokens} → ${result.compactedTokens} tokens (${percentage}% reduction)]`;
|
|
3467
|
-
}
|
|
3468
|
-
|
|
3469
|
-
//#endregion
|
|
3470
|
-
//#region src/lib/message-sanitizer.ts
|
|
3471
|
-
const startPattern = /^\s*<system-reminder>[\s\S]*?<\/system-reminder>\n*/;
|
|
3472
|
-
const endPatternWithNewline = /\n+<system-reminder>[\s\S]*?<\/system-reminder>\s*$/;
|
|
3473
|
-
const endPatternOnly = /^\s*<system-reminder>[\s\S]*?<\/system-reminder>\s*$/;
|
|
3474
|
-
function removeSystemReminderTags(text) {
|
|
3475
|
-
let result = text;
|
|
3476
|
-
let prev;
|
|
3477
|
-
do {
|
|
3478
|
-
prev = result;
|
|
3479
|
-
result = result.replace(startPattern, "");
|
|
3480
|
-
} while (result !== prev);
|
|
3481
|
-
do {
|
|
3482
|
-
prev = result;
|
|
3483
|
-
result = result.replace(endPatternWithNewline, "");
|
|
3484
|
-
} while (result !== prev);
|
|
3485
|
-
result = result.replace(endPatternOnly, "");
|
|
3486
|
-
return result;
|
|
3487
|
-
}
|
|
3488
|
-
|
|
3489
|
-
//#endregion
|
|
3490
|
-
//#region src/lib/repetition-detector.ts
|
|
3491
|
-
/**
|
|
3492
|
-
* Stream repetition detector.
|
|
3493
|
-
*
|
|
3494
|
-
* Uses the KMP failure function (prefix function) to detect repeated patterns
|
|
3495
|
-
* in streaming text output. When a model gets stuck in a repetitive loop,
|
|
3496
|
-
* it wastes tokens producing the same content over and over. This detector
|
|
3497
|
-
* identifies such loops early so the caller can take action (log warning,
|
|
3498
|
-
* abort stream, etc.).
|
|
3499
|
-
*
|
|
3500
|
-
* The algorithm works by maintaining a sliding buffer of recent text and
|
|
3501
|
-
* computing the longest proper prefix that is also a suffix — if this
|
|
3502
|
-
* length exceeds `(text.length - period) >= minRepetitions * period`,
|
|
3503
|
-
* it means a pattern of length `period` has repeated enough times.
|
|
3504
|
-
*/
|
|
3505
|
-
const DEFAULT_CONFIG = {
|
|
3506
|
-
minPatternLength: 10,
|
|
3507
|
-
minRepetitions: 3,
|
|
3508
|
-
maxBufferSize: 5e3
|
|
3509
|
-
};
|
|
3510
|
-
var RepetitionDetector = class {
|
|
3511
|
-
buffer = "";
|
|
3512
|
-
config;
|
|
3513
|
-
detected = false;
|
|
3514
|
-
constructor(config) {
|
|
3515
|
-
this.config = {
|
|
3516
|
-
...DEFAULT_CONFIG,
|
|
3517
|
-
...config
|
|
3518
|
-
};
|
|
3519
|
-
}
|
|
3520
|
-
/**
|
|
3521
|
-
* Feed a text chunk into the detector.
|
|
3522
|
-
* Returns `true` if repetition has been detected (now or previously).
|
|
3523
|
-
* Once detected, subsequent calls return `true` without further analysis.
|
|
3524
|
-
*/
|
|
3525
|
-
feed(text) {
|
|
3526
|
-
if (this.detected) return true;
|
|
3527
|
-
if (!text) return false;
|
|
3528
|
-
this.buffer += text;
|
|
3529
|
-
if (this.buffer.length > this.config.maxBufferSize) this.buffer = this.buffer.slice(-this.config.maxBufferSize);
|
|
3530
|
-
const minRequired = this.config.minPatternLength * this.config.minRepetitions;
|
|
3531
|
-
if (this.buffer.length < minRequired) return false;
|
|
3532
|
-
this.detected = detectRepetition(this.buffer, this.config.minPatternLength, this.config.minRepetitions);
|
|
3533
|
-
return this.detected;
|
|
3534
|
-
}
|
|
3535
|
-
/** Reset detector state for a new stream */
|
|
3536
|
-
reset() {
|
|
3537
|
-
this.buffer = "";
|
|
3538
|
-
this.detected = false;
|
|
3539
|
-
}
|
|
3540
|
-
/** Whether repetition has been detected */
|
|
3541
|
-
get isDetected() {
|
|
3542
|
-
return this.detected;
|
|
3543
|
-
}
|
|
3544
|
-
};
|
|
3545
|
-
/**
|
|
3546
|
-
* Detect if the tail of `text` contains a repeating pattern.
|
|
3547
|
-
*
|
|
3548
|
-
* Uses the KMP prefix function: for a string S, the prefix function π[i]
|
|
3549
|
-
* gives the length of the longest proper prefix of S[0..i] that is also
|
|
3550
|
-
* a suffix. If π[n-1] ≥ (n - period) where period = n - π[n-1], then
|
|
3551
|
-
* the string is composed of a repeating unit of length `period`.
|
|
3552
|
-
*
|
|
3553
|
-
* We check the suffix of the buffer (last `checkLength` chars) to detect
|
|
3554
|
-
* if a pattern of at least `minPatternLength` chars repeats at least
|
|
3555
|
-
* `minRepetitions` times.
|
|
3556
|
-
*/
|
|
3557
|
-
function detectRepetition(text, minPatternLength, minRepetitions) {
|
|
3558
|
-
const minWindow = minPatternLength * minRepetitions;
|
|
3559
|
-
const maxWindow = Math.min(text.length, 2e3);
|
|
3560
|
-
const windowSizes = [
|
|
3561
|
-
minWindow,
|
|
3562
|
-
Math.floor(maxWindow * .5),
|
|
3563
|
-
maxWindow
|
|
3564
|
-
].filter((w) => w >= minWindow && w <= text.length);
|
|
3565
|
-
for (const windowSize of windowSizes) {
|
|
3566
|
-
const window = text.slice(-windowSize);
|
|
3567
|
-
const period = findRepeatingPeriod(window);
|
|
3568
|
-
if (period >= minPatternLength) {
|
|
3569
|
-
if (Math.floor(window.length / period) >= minRepetitions) return true;
|
|
3570
|
-
}
|
|
3571
|
-
}
|
|
3572
|
-
return false;
|
|
3573
|
-
}
|
|
3574
|
-
/**
|
|
3575
|
-
* Find the shortest repeating period in a string using KMP prefix function.
|
|
3576
|
-
* Returns the period length, or the string length if no repetition found.
|
|
3577
|
-
*/
|
|
3578
|
-
function findRepeatingPeriod(s) {
|
|
3579
|
-
const n = s.length;
|
|
3580
|
-
if (n === 0) return 0;
|
|
3581
|
-
const pi = new Int32Array(n);
|
|
3582
|
-
for (let i = 1; i < n; i++) {
|
|
3583
|
-
let j = pi[i - 1] ?? 0;
|
|
3584
|
-
while (j > 0 && s[i] !== s[j]) j = pi[j - 1] ?? 0;
|
|
3585
|
-
if (s[i] === s[j]) j++;
|
|
3586
|
-
pi[i] = j;
|
|
3553
|
+
};
|
|
3587
3554
|
}
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
return
|
|
3555
|
+
if (preserveIndex >= conversationMessages.length) {
|
|
3556
|
+
consola.warn("[AutoTruncate:OpenAI] Would need to remove all messages");
|
|
3557
|
+
return {
|
|
3558
|
+
payload,
|
|
3559
|
+
wasCompacted: false,
|
|
3560
|
+
originalTokens,
|
|
3561
|
+
compactedTokens: originalTokens,
|
|
3562
|
+
removedMessageCount: 0
|
|
3563
|
+
};
|
|
3564
|
+
}
|
|
3565
|
+
let preserved = conversationMessages.slice(preserveIndex);
|
|
3566
|
+
preserved = filterOrphanedToolResults$1(preserved);
|
|
3567
|
+
preserved = filterOrphanedToolUse$1(preserved);
|
|
3568
|
+
preserved = ensureStartsWithUser(preserved, "OpenAI");
|
|
3569
|
+
preserved = filterOrphanedToolResults$1(preserved);
|
|
3570
|
+
preserved = filterOrphanedToolUse$1(preserved);
|
|
3571
|
+
if (preserved.length === 0) {
|
|
3572
|
+
consola.warn("[AutoTruncate:OpenAI] All messages filtered out after cleanup");
|
|
3573
|
+
return {
|
|
3574
|
+
payload,
|
|
3575
|
+
wasCompacted: false,
|
|
3576
|
+
originalTokens,
|
|
3577
|
+
compactedTokens: originalTokens,
|
|
3578
|
+
removedMessageCount: 0
|
|
3579
|
+
};
|
|
3580
|
+
}
|
|
3581
|
+
const removedMessages = conversationMessages.slice(0, preserveIndex);
|
|
3582
|
+
const removedCount = conversationMessages.length - preserved.length;
|
|
3583
|
+
const summary = generateRemovedMessagesSummary(removedMessages, (msg) => msg.tool_calls?.map((tc) => tc.function.name).filter(Boolean) ?? []);
|
|
3584
|
+
let newSystemMessages = systemMessages;
|
|
3585
|
+
let newMessages = preserved;
|
|
3586
|
+
if (systemMessages.length > 0) {
|
|
3587
|
+
const truncationContext = createTruncationSystemContext$1(removedCount, compressedCount, summary);
|
|
3588
|
+
const lastSystemIdx = systemMessages.length - 1;
|
|
3589
|
+
const lastSystem = systemMessages[lastSystemIdx];
|
|
3590
|
+
const updatedSystem = {
|
|
3591
|
+
...lastSystem,
|
|
3592
|
+
content: typeof lastSystem.content === "string" ? lastSystem.content + truncationContext : lastSystem.content
|
|
3593
|
+
};
|
|
3594
|
+
newSystemMessages = [...systemMessages.slice(0, lastSystemIdx), updatedSystem];
|
|
3595
|
+
} else newMessages = [createTruncationMarker$2(removedCount, compressedCount, summary), ...preserved];
|
|
3596
|
+
const newPayload = {
|
|
3597
|
+
...payload,
|
|
3598
|
+
messages: [...newSystemMessages, ...newMessages]
|
|
3607
3599
|
};
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
const
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3600
|
+
const newBytes = JSON.stringify(newPayload).length;
|
|
3601
|
+
const newTokenCount = await getTokenCount(newPayload, model);
|
|
3602
|
+
let reason = "tokens";
|
|
3603
|
+
if (exceedsTokens && exceedsBytes) reason = "tokens+size";
|
|
3604
|
+
else if (exceedsBytes) reason = "size";
|
|
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
|
|
3622
3617
|
};
|
|
3623
|
-
|
|
3624
|
-
method: "POST",
|
|
3625
|
-
headers,
|
|
3626
|
-
body: JSON.stringify(payload)
|
|
3627
|
-
});
|
|
3628
|
-
if (!response.ok) {
|
|
3629
|
-
consola.error("Failed to create chat completions", response);
|
|
3630
|
-
throw await HTTPError.fromResponse("Failed to create chat completions", response, payload.model);
|
|
3631
|
-
}
|
|
3632
|
-
if (payload.stream) return events(response);
|
|
3633
|
-
return await response.json();
|
|
3634
|
-
};
|
|
3618
|
+
}
|
|
3635
3619
|
|
|
3636
3620
|
//#endregion
|
|
3637
3621
|
//#region src/routes/shared.ts
|
|
@@ -3838,7 +3822,7 @@ async function handleCompletion$1(c) {
|
|
|
3838
3822
|
trackingId,
|
|
3839
3823
|
startTime
|
|
3840
3824
|
};
|
|
3841
|
-
const selectedModel =
|
|
3825
|
+
const selectedModel = findModelById(originalPayload.model);
|
|
3842
3826
|
await logTokenCount(originalPayload, selectedModel);
|
|
3843
3827
|
const { finalPayload, truncateResult } = await buildFinalPayload(originalPayload, selectedModel);
|
|
3844
3828
|
if (truncateResult) ctx.truncateResult = truncateResult;
|
|
@@ -3882,6 +3866,7 @@ async function executeRequest(opts) {
|
|
|
3882
3866
|
}
|
|
3883
3867
|
}
|
|
3884
3868
|
async function logTokenCount(payload, selectedModel) {
|
|
3869
|
+
if (consola.level < 4) return;
|
|
3885
3870
|
try {
|
|
3886
3871
|
if (selectedModel) {
|
|
3887
3872
|
const tokenCount = await getTokenCount(payload, selectedModel);
|
|
@@ -3895,7 +3880,7 @@ function handleNonStreamingResponse$1(c, originalResponse, ctx, payload) {
|
|
|
3895
3880
|
consola.debug("Non-streaming response:", JSON.stringify(originalResponse));
|
|
3896
3881
|
let response = originalResponse;
|
|
3897
3882
|
if (state.verbose && ctx.truncateResult?.wasCompacted && response.choices[0]?.message.content) {
|
|
3898
|
-
const marker =
|
|
3883
|
+
const marker = createTruncationMarker$1(ctx.truncateResult);
|
|
3899
3884
|
response = {
|
|
3900
3885
|
...response,
|
|
3901
3886
|
choices: response.choices.map((choice, i) => i === 0 ? {
|
|
@@ -3981,7 +3966,7 @@ async function handleStreamingResponse$1(opts) {
|
|
|
3981
3966
|
const checkRepetition = createStreamRepetitionChecker(`openai:${payload.model}`);
|
|
3982
3967
|
try {
|
|
3983
3968
|
if (state.verbose && ctx.truncateResult?.wasCompacted) {
|
|
3984
|
-
const marker =
|
|
3969
|
+
const marker = createTruncationMarker$1(ctx.truncateResult);
|
|
3985
3970
|
const markerChunk = {
|
|
3986
3971
|
id: `compact-marker-${Date.now()}`,
|
|
3987
3972
|
object: "chat.completion.chunk",
|
|
@@ -4340,7 +4325,7 @@ function isFileDataPart(part) {
|
|
|
4340
4325
|
async function handleGeminiCountTokens(c, model) {
|
|
4341
4326
|
try {
|
|
4342
4327
|
const { payload } = translateGeminiToOpenAI(await c.req.json(), model);
|
|
4343
|
-
const selectedModel =
|
|
4328
|
+
const selectedModel = findModelById(model);
|
|
4344
4329
|
if (!selectedModel) {
|
|
4345
4330
|
consola.warn("Model not found for count_tokens, returning estimate");
|
|
4346
4331
|
return c.json({ totalTokens: 1 });
|
|
@@ -4500,7 +4485,7 @@ async function handleGeminiGenerate(c, model, isStream) {
|
|
|
4500
4485
|
updateTrackerModel(trackingId, model);
|
|
4501
4486
|
const { payload } = translateGeminiToOpenAI(geminiRequest, model);
|
|
4502
4487
|
payload.stream = isStream;
|
|
4503
|
-
const selectedModel =
|
|
4488
|
+
const selectedModel = findModelById(model);
|
|
4504
4489
|
if (isNullish(payload.max_tokens) && selectedModel) payload.max_tokens = selectedModel.capabilities?.limits?.max_output_tokens;
|
|
4505
4490
|
const ctx = {
|
|
4506
4491
|
historyId: recordRequest("gemini", {
|
|
@@ -6220,9 +6205,6 @@ async function countTotalTokens(payload, model) {
|
|
|
6220
6205
|
}
|
|
6221
6206
|
return total;
|
|
6222
6207
|
}
|
|
6223
|
-
function getMessageBytes(msg) {
|
|
6224
|
-
return JSON.stringify(msg).length;
|
|
6225
|
-
}
|
|
6226
6208
|
/**
|
|
6227
6209
|
* Get tool_use IDs from an assistant message.
|
|
6228
6210
|
*/
|
|
@@ -6307,30 +6289,6 @@ function filterOrphanedToolUse(messages) {
|
|
|
6307
6289
|
return result;
|
|
6308
6290
|
}
|
|
6309
6291
|
/**
|
|
6310
|
-
* Ensure messages start with a user message.
|
|
6311
|
-
*/
|
|
6312
|
-
function ensureStartsWithUser(messages) {
|
|
6313
|
-
let startIndex = 0;
|
|
6314
|
-
while (startIndex < messages.length && messages[startIndex].role !== "user") startIndex++;
|
|
6315
|
-
if (startIndex > 0) consola.debug(`[AutoTruncate:Anthropic] Skipped ${startIndex} leading non-user messages`);
|
|
6316
|
-
return messages.slice(startIndex);
|
|
6317
|
-
}
|
|
6318
|
-
/** Threshold for large tool_result content (bytes) */
|
|
6319
|
-
const LARGE_TOOL_RESULT_THRESHOLD = 1e4;
|
|
6320
|
-
/** Maximum length for compressed tool_result summary */
|
|
6321
|
-
const COMPRESSED_SUMMARY_LENGTH = 500;
|
|
6322
|
-
/**
|
|
6323
|
-
* Compress a large tool_result content to a summary.
|
|
6324
|
-
* Keeps the first and last portions with a note about truncation.
|
|
6325
|
-
*/
|
|
6326
|
-
function compressToolResultContent(content) {
|
|
6327
|
-
if (content.length <= LARGE_TOOL_RESULT_THRESHOLD) return content;
|
|
6328
|
-
const halfLen = Math.floor(COMPRESSED_SUMMARY_LENGTH / 2);
|
|
6329
|
-
const start = content.slice(0, halfLen);
|
|
6330
|
-
const end = content.slice(-halfLen);
|
|
6331
|
-
return `${start}\n\n[... ${(content.length - COMPRESSED_SUMMARY_LENGTH).toLocaleString()} characters omitted for brevity ...]\n\n${end}`;
|
|
6332
|
-
}
|
|
6333
|
-
/**
|
|
6334
6292
|
* Compress a tool_result block in an Anthropic message.
|
|
6335
6293
|
*/
|
|
6336
6294
|
function compressToolResultBlock(block) {
|
|
@@ -6349,28 +6307,11 @@ function compressToolResultBlock(block) {
|
|
|
6349
6307
|
* @param preservePercent - Percentage of context to preserve uncompressed (0.0-1.0)
|
|
6350
6308
|
*/
|
|
6351
6309
|
function smartCompressToolResults(messages, tokenLimit, byteLimit, preservePercent) {
|
|
6352
|
-
const
|
|
6353
|
-
|
|
6354
|
-
const cumBytes = Array.from({ length: n + 1 }, () => 0);
|
|
6355
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
6356
|
-
const msg = messages[i];
|
|
6357
|
-
cumTokens[i] = cumTokens[i + 1] + estimateMessageTokens(msg);
|
|
6358
|
-
cumBytes[i] = cumBytes[i + 1] + getMessageBytes(msg) + 1;
|
|
6359
|
-
}
|
|
6360
|
-
const preserveTokenLimit = Math.floor(tokenLimit * preservePercent);
|
|
6361
|
-
const preserveByteLimit = Math.floor(byteLimit * preservePercent);
|
|
6362
|
-
let thresholdIndex = n;
|
|
6363
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
6364
|
-
if (cumTokens[i] > preserveTokenLimit || cumBytes[i] > preserveByteLimit) {
|
|
6365
|
-
thresholdIndex = i + 1;
|
|
6366
|
-
break;
|
|
6367
|
-
}
|
|
6368
|
-
thresholdIndex = i;
|
|
6369
|
-
}
|
|
6370
|
-
if (thresholdIndex >= n) return {
|
|
6310
|
+
const thresholdIndex = findCompressThreshold(messages, tokenLimit, byteLimit, preservePercent, estimateMessageTokens);
|
|
6311
|
+
if (thresholdIndex >= messages.length) return {
|
|
6371
6312
|
messages,
|
|
6372
6313
|
compressedCount: 0,
|
|
6373
|
-
compressThresholdIndex:
|
|
6314
|
+
compressThresholdIndex: messages.length
|
|
6374
6315
|
};
|
|
6375
6316
|
const result = [];
|
|
6376
6317
|
let compressedCount = 0;
|
|
@@ -6399,68 +6340,6 @@ function smartCompressToolResults(messages, tokenLimit, byteLimit, preservePerce
|
|
|
6399
6340
|
compressThresholdIndex: thresholdIndex
|
|
6400
6341
|
};
|
|
6401
6342
|
}
|
|
6402
|
-
/** Default fallback for when model capabilities are not available */
|
|
6403
|
-
const DEFAULT_CONTEXT_WINDOW = 2e5;
|
|
6404
|
-
function calculateLimits(model, config) {
|
|
6405
|
-
const rawTokenLimit = getEffectiveTokenLimit(model.id) ?? model.capabilities?.limits?.max_context_window_tokens ?? model.capabilities?.limits?.max_prompt_tokens ?? DEFAULT_CONTEXT_WINDOW;
|
|
6406
|
-
return {
|
|
6407
|
-
tokenLimit: Math.floor(rawTokenLimit * (1 - config.safetyMarginPercent / 100)),
|
|
6408
|
-
byteLimit: getEffectiveByteLimitBytes()
|
|
6409
|
-
};
|
|
6410
|
-
}
|
|
6411
|
-
function findOptimalPreserveIndex(params) {
|
|
6412
|
-
const { messages, systemBytes, systemTokens, payloadOverhead, tokenLimit, byteLimit } = params;
|
|
6413
|
-
if (messages.length === 0) return 0;
|
|
6414
|
-
const markerBytes = 200;
|
|
6415
|
-
const availableTokens = tokenLimit - systemTokens - 50;
|
|
6416
|
-
const availableBytes = byteLimit - payloadOverhead - systemBytes - markerBytes;
|
|
6417
|
-
if (availableTokens <= 0 || availableBytes <= 0) return messages.length;
|
|
6418
|
-
const n = messages.length;
|
|
6419
|
-
const cumTokens = Array.from({ length: n + 1 }, () => 0);
|
|
6420
|
-
const cumBytes = Array.from({ length: n + 1 }, () => 0);
|
|
6421
|
-
for (let i = n - 1; i >= 0; i--) {
|
|
6422
|
-
const msg = messages[i];
|
|
6423
|
-
cumTokens[i] = cumTokens[i + 1] + estimateMessageTokens(msg);
|
|
6424
|
-
cumBytes[i] = cumBytes[i + 1] + getMessageBytes(msg) + 1;
|
|
6425
|
-
}
|
|
6426
|
-
let left = 0;
|
|
6427
|
-
let right = n;
|
|
6428
|
-
while (left < right) {
|
|
6429
|
-
const mid = left + right >>> 1;
|
|
6430
|
-
if (cumTokens[mid] <= availableTokens && cumBytes[mid] <= availableBytes) right = mid;
|
|
6431
|
-
else left = mid + 1;
|
|
6432
|
-
}
|
|
6433
|
-
return left;
|
|
6434
|
-
}
|
|
6435
|
-
/**
|
|
6436
|
-
* Generate a summary of removed messages for context.
|
|
6437
|
-
* Extracts key information like tool calls and topics.
|
|
6438
|
-
*/
|
|
6439
|
-
function generateRemovedMessagesSummary(removedMessages) {
|
|
6440
|
-
const toolCalls = [];
|
|
6441
|
-
let userMessageCount = 0;
|
|
6442
|
-
let assistantMessageCount = 0;
|
|
6443
|
-
for (const msg of removedMessages) {
|
|
6444
|
-
if (msg.role === "user") userMessageCount++;
|
|
6445
|
-
else assistantMessageCount++;
|
|
6446
|
-
if (Array.isArray(msg.content)) {
|
|
6447
|
-
for (const block of msg.content) if (block.type === "tool_use") toolCalls.push(block.name);
|
|
6448
|
-
}
|
|
6449
|
-
}
|
|
6450
|
-
const parts = [];
|
|
6451
|
-
if (userMessageCount > 0 || assistantMessageCount > 0) {
|
|
6452
|
-
const breakdown = [];
|
|
6453
|
-
if (userMessageCount > 0) breakdown.push(`${userMessageCount} user`);
|
|
6454
|
-
if (assistantMessageCount > 0) breakdown.push(`${assistantMessageCount} assistant`);
|
|
6455
|
-
parts.push(`Messages: ${breakdown.join(", ")}`);
|
|
6456
|
-
}
|
|
6457
|
-
if (toolCalls.length > 0) {
|
|
6458
|
-
const uniqueTools = [...new Set(toolCalls)];
|
|
6459
|
-
const displayTools = uniqueTools.length > 5 ? [...uniqueTools.slice(0, 5), `+${uniqueTools.length - 5} more`] : uniqueTools;
|
|
6460
|
-
parts.push(`Tools used: ${displayTools.join(", ")}`);
|
|
6461
|
-
}
|
|
6462
|
-
return parts.join(". ");
|
|
6463
|
-
}
|
|
6464
6343
|
/**
|
|
6465
6344
|
* Add a compression notice to the system prompt.
|
|
6466
6345
|
* Informs the model that some tool_result content has been compressed.
|
|
@@ -6512,7 +6391,7 @@ async function autoTruncateAnthropic(payload, model, config = {}) {
|
|
|
6512
6391
|
...DEFAULT_AUTO_TRUNCATE_CONFIG,
|
|
6513
6392
|
...config
|
|
6514
6393
|
};
|
|
6515
|
-
const { tokenLimit, byteLimit } = calculateLimits(model, cfg);
|
|
6394
|
+
const { tokenLimit, byteLimit } = calculateLimits(model, cfg, 2e5);
|
|
6516
6395
|
const originalBytes = JSON.stringify(payload).length;
|
|
6517
6396
|
const originalTokens = await countTotalTokens(payload, model);
|
|
6518
6397
|
if (originalTokens <= tokenLimit && originalBytes <= byteLimit) return {
|
|
@@ -6565,7 +6444,8 @@ async function autoTruncateAnthropic(payload, model, config = {}) {
|
|
|
6565
6444
|
systemTokens,
|
|
6566
6445
|
payloadOverhead,
|
|
6567
6446
|
tokenLimit,
|
|
6568
|
-
byteLimit
|
|
6447
|
+
byteLimit,
|
|
6448
|
+
estimateTokens: estimateMessageTokens
|
|
6569
6449
|
});
|
|
6570
6450
|
if (preserveIndex === 0) {
|
|
6571
6451
|
consola.warn("[AutoTruncate:Anthropic] Cannot truncate, system messages too large");
|
|
@@ -6590,7 +6470,7 @@ async function autoTruncateAnthropic(payload, model, config = {}) {
|
|
|
6590
6470
|
let preserved = workingMessages.slice(preserveIndex);
|
|
6591
6471
|
preserved = filterOrphanedToolResults(preserved);
|
|
6592
6472
|
preserved = filterOrphanedToolUse(preserved);
|
|
6593
|
-
preserved = ensureStartsWithUser(preserved);
|
|
6473
|
+
preserved = ensureStartsWithUser(preserved, "Anthropic");
|
|
6594
6474
|
preserved = filterOrphanedToolResults(preserved);
|
|
6595
6475
|
preserved = filterOrphanedToolUse(preserved);
|
|
6596
6476
|
if (preserved.length === 0) {
|
|
@@ -6605,7 +6485,10 @@ async function autoTruncateAnthropic(payload, model, config = {}) {
|
|
|
6605
6485
|
}
|
|
6606
6486
|
const removedMessages = payload.messages.slice(0, preserveIndex);
|
|
6607
6487
|
const removedCount = workingMessages.length - preserved.length;
|
|
6608
|
-
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
|
+
});
|
|
6609
6492
|
let newSystem = payload.system;
|
|
6610
6493
|
let newMessages = preserved;
|
|
6611
6494
|
if (payload.system !== void 0) {
|
|
@@ -6647,7 +6530,7 @@ async function checkNeedsCompactionAnthropic(payload, model, config = {}) {
|
|
|
6647
6530
|
const { tokenLimit, byteLimit } = calculateLimits(model, {
|
|
6648
6531
|
...DEFAULT_AUTO_TRUNCATE_CONFIG,
|
|
6649
6532
|
...config
|
|
6650
|
-
});
|
|
6533
|
+
}, 2e5);
|
|
6651
6534
|
const currentTokens = await countTotalTokens(payload, model);
|
|
6652
6535
|
const currentBytes = JSON.stringify(payload).length;
|
|
6653
6536
|
const exceedsTokens = currentTokens > tokenLimit;
|
|
@@ -6887,7 +6770,7 @@ async function createAnthropicMessages(payload, options) {
|
|
|
6887
6770
|
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
6888
6771
|
let filteredPayload = filterPayloadForCopilot(payload);
|
|
6889
6772
|
filteredPayload = adjustMaxTokensForThinking(filteredPayload);
|
|
6890
|
-
const resolvedModel =
|
|
6773
|
+
const resolvedModel = findModelById(filteredPayload.model);
|
|
6891
6774
|
const enableVision = filteredPayload.messages.some((msg) => {
|
|
6892
6775
|
if (typeof msg.content === "string") return false;
|
|
6893
6776
|
return msg.content.some((block) => block.type === "image");
|
|
@@ -6958,7 +6841,7 @@ function stripServerToolsFromPayload(tools) {
|
|
|
6958
6841
|
*/
|
|
6959
6842
|
function supportsDirectAnthropicApi(modelId) {
|
|
6960
6843
|
if (state.redirectAnthropic) return false;
|
|
6961
|
-
return (
|
|
6844
|
+
return findModelById(modelId)?.vendor === "Anthropic";
|
|
6962
6845
|
}
|
|
6963
6846
|
|
|
6964
6847
|
//#endregion
|
|
@@ -7002,15 +6885,6 @@ function extractSystemPrompt(system) {
|
|
|
7002
6885
|
return system.map((block) => block.text).join("\n");
|
|
7003
6886
|
}
|
|
7004
6887
|
function extractToolCallsFromContent(content) {
|
|
7005
|
-
const tools = [];
|
|
7006
|
-
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({
|
|
7007
|
-
id: String(block.id),
|
|
7008
|
-
name: String(block.name),
|
|
7009
|
-
input: JSON.stringify(block.input)
|
|
7010
|
-
});
|
|
7011
|
-
return tools.length > 0 ? tools : void 0;
|
|
7012
|
-
}
|
|
7013
|
-
function extractToolCallsFromAnthropicContent(content) {
|
|
7014
6888
|
const tools = [];
|
|
7015
6889
|
for (const block of content) if (block.type === "tool_use") tools.push({
|
|
7016
6890
|
id: block.id,
|
|
@@ -7028,6 +6902,25 @@ function mapOpenAIStopReasonToAnthropic(finishReason) {
|
|
|
7028
6902
|
content_filter: "end_turn"
|
|
7029
6903
|
}[finishReason];
|
|
7030
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
|
+
}
|
|
7031
6924
|
|
|
7032
6925
|
//#endregion
|
|
7033
6926
|
//#region src/routes/messages/stream-accumulator.ts
|
|
@@ -7589,7 +7482,7 @@ function translateErrorToAnthropicErrorEvent() {
|
|
|
7589
7482
|
*/
|
|
7590
7483
|
async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiatorOverride) {
|
|
7591
7484
|
consola.debug("Using direct Anthropic API path for model:", anthropicPayload.model);
|
|
7592
|
-
const selectedModel =
|
|
7485
|
+
const selectedModel = findModelById(anthropicPayload.model);
|
|
7593
7486
|
let effectivePayload = anthropicPayload;
|
|
7594
7487
|
let truncateResult;
|
|
7595
7488
|
if (state.autoTruncate && selectedModel) {
|
|
@@ -7672,7 +7565,7 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
|
|
|
7672
7565
|
}
|
|
7673
7566
|
})
|
|
7674
7567
|
},
|
|
7675
|
-
toolCalls:
|
|
7568
|
+
toolCalls: extractToolCallsFromContent(response.content)
|
|
7676
7569
|
}, Date.now() - ctx.startTime);
|
|
7677
7570
|
if (ctx.trackingId) requestTracker.updateRequest(ctx.trackingId, {
|
|
7678
7571
|
inputTokens: response.usage.input_tokens,
|
|
@@ -7690,34 +7583,12 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
|
|
|
7690
7583
|
stopReason: response.stop_reason ?? void 0
|
|
7691
7584
|
});
|
|
7692
7585
|
let finalResponse = response;
|
|
7693
|
-
if (state.verbose && truncateResult?.wasCompacted) finalResponse =
|
|
7586
|
+
if (state.verbose && truncateResult?.wasCompacted) finalResponse = prependMarkerToResponse(response, createTruncationMarker$1(truncateResult));
|
|
7694
7587
|
logServerToolBlocks(finalResponse.content);
|
|
7695
7588
|
finalResponse = filterServerToolBlocksFromResponse(finalResponse);
|
|
7696
7589
|
return c.json(finalResponse);
|
|
7697
7590
|
}
|
|
7698
7591
|
/**
|
|
7699
|
-
* Prepend marker to Anthropic response content (at the beginning of first text block)
|
|
7700
|
-
*/
|
|
7701
|
-
function prependMarkerToAnthropicResponse$1(response, marker) {
|
|
7702
|
-
if (!marker) return response;
|
|
7703
|
-
const content = [...response.content];
|
|
7704
|
-
const firstTextIndex = content.findIndex((block) => block.type === "text");
|
|
7705
|
-
if (firstTextIndex !== -1) {
|
|
7706
|
-
const textBlock = content[firstTextIndex];
|
|
7707
|
-
if (textBlock.type === "text") content[firstTextIndex] = {
|
|
7708
|
-
...textBlock,
|
|
7709
|
-
text: marker + textBlock.text
|
|
7710
|
-
};
|
|
7711
|
-
} else content.unshift({
|
|
7712
|
-
type: "text",
|
|
7713
|
-
text: marker
|
|
7714
|
-
});
|
|
7715
|
-
return {
|
|
7716
|
-
...response,
|
|
7717
|
-
content
|
|
7718
|
-
};
|
|
7719
|
-
}
|
|
7720
|
-
/**
|
|
7721
7592
|
* Handle streaming direct Anthropic response (passthrough SSE events)
|
|
7722
7593
|
*/
|
|
7723
7594
|
async function handleDirectAnthropicStreamingResponse(opts) {
|
|
@@ -7826,7 +7697,7 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
|
|
|
7826
7697
|
const { payload: translatedPayload, toolNameMapping } = translateToOpenAI(anthropicPayload);
|
|
7827
7698
|
consola.debug("Translated OpenAI request payload:", JSON.stringify(translatedPayload));
|
|
7828
7699
|
updateTrackerResolvedModel(ctx.trackingId, translatedPayload.model);
|
|
7829
|
-
const selectedModel =
|
|
7700
|
+
const selectedModel = findModelById(translatedPayload.model);
|
|
7830
7701
|
const { finalPayload: openAIPayload, truncateResult } = await buildFinalPayload(translatedPayload, selectedModel);
|
|
7831
7702
|
if (truncateResult) ctx.truncateResult = truncateResult;
|
|
7832
7703
|
if (state.manualApprove) await awaitApproval();
|
|
@@ -7863,8 +7734,8 @@ function handleNonStreamingResponse(opts) {
|
|
|
7863
7734
|
let anthropicResponse = translateToAnthropic(response, toolNameMapping);
|
|
7864
7735
|
consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
|
|
7865
7736
|
if (state.verbose && ctx.truncateResult?.wasCompacted) {
|
|
7866
|
-
const marker =
|
|
7867
|
-
anthropicResponse =
|
|
7737
|
+
const marker = createTruncationMarker$1(ctx.truncateResult);
|
|
7738
|
+
anthropicResponse = prependMarkerToResponse(anthropicResponse, marker);
|
|
7868
7739
|
}
|
|
7869
7740
|
recordResponse(ctx.historyId, {
|
|
7870
7741
|
success: true,
|
|
@@ -7906,24 +7777,6 @@ function handleNonStreamingResponse(opts) {
|
|
|
7906
7777
|
});
|
|
7907
7778
|
return c.json(anthropicResponse);
|
|
7908
7779
|
}
|
|
7909
|
-
function prependMarkerToAnthropicResponse(response, marker) {
|
|
7910
|
-
const content = [...response.content];
|
|
7911
|
-
const firstTextIndex = content.findIndex((block) => block.type === "text");
|
|
7912
|
-
if (firstTextIndex !== -1) {
|
|
7913
|
-
const textBlock = content[firstTextIndex];
|
|
7914
|
-
if (textBlock.type === "text") content[firstTextIndex] = {
|
|
7915
|
-
...textBlock,
|
|
7916
|
-
text: marker + textBlock.text
|
|
7917
|
-
};
|
|
7918
|
-
} else content.unshift({
|
|
7919
|
-
type: "text",
|
|
7920
|
-
text: marker
|
|
7921
|
-
});
|
|
7922
|
-
return {
|
|
7923
|
-
...response,
|
|
7924
|
-
content
|
|
7925
|
-
};
|
|
7926
|
-
}
|
|
7927
7780
|
async function handleStreamingResponse(opts) {
|
|
7928
7781
|
const { stream, response, toolNameMapping, anthropicPayload, ctx } = opts;
|
|
7929
7782
|
const streamState = {
|
|
@@ -7936,7 +7789,7 @@ async function handleStreamingResponse(opts) {
|
|
|
7936
7789
|
const checkRepetition = createStreamRepetitionChecker(`translated:${anthropicPayload.model}`);
|
|
7937
7790
|
try {
|
|
7938
7791
|
if (ctx.truncateResult?.wasCompacted) {
|
|
7939
|
-
const marker =
|
|
7792
|
+
const marker = createTruncationMarker$1(ctx.truncateResult);
|
|
7940
7793
|
await sendTruncationMarkerEvent(stream, streamState, marker);
|
|
7941
7794
|
acc.content += marker;
|
|
7942
7795
|
}
|
|
@@ -8110,7 +7963,7 @@ async function handleCountTokens(c) {
|
|
|
8110
7963
|
const anthropicPayload = await c.req.json();
|
|
8111
7964
|
anthropicPayload.model = resolveModelFromBetaHeader(anthropicPayload.model, anthropicBeta);
|
|
8112
7965
|
const { payload: openAIPayload } = translateToOpenAI(anthropicPayload);
|
|
8113
|
-
const selectedModel =
|
|
7966
|
+
const selectedModel = findModelById(openAIPayload.model);
|
|
8114
7967
|
if (!selectedModel) {
|
|
8115
7968
|
consola.warn("Model not found, returning default token count");
|
|
8116
7969
|
return c.json({ input_tokens: 1 });
|
|
@@ -8469,7 +8322,7 @@ const handleResponses = async (c) => {
|
|
|
8469
8322
|
trackingId,
|
|
8470
8323
|
startTime
|
|
8471
8324
|
};
|
|
8472
|
-
if (!((
|
|
8325
|
+
if (!(findModelById(payload.model)?.supported_endpoints?.includes(RESPONSES_ENDPOINT) ?? false)) {
|
|
8473
8326
|
recordErrorResponse(ctx, model, /* @__PURE__ */ new Error("This model does not support the responses endpoint."));
|
|
8474
8327
|
return c.json({ error: {
|
|
8475
8328
|
message: "This model does not support the responses endpoint. Please choose a different model.",
|
|
@@ -8809,6 +8662,17 @@ function parseTimezoneOffset(value) {
|
|
|
8809
8662
|
if (!Number.isFinite(n)) return 8;
|
|
8810
8663
|
return n;
|
|
8811
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
|
+
}
|
|
8812
8676
|
const start = defineCommand({
|
|
8813
8677
|
meta: {
|
|
8814
8678
|
name: "start",
|
|
@@ -8956,7 +8820,7 @@ const start = defineCommand({
|
|
|
8956
8820
|
compressToolResults: args["compress-tool-results"],
|
|
8957
8821
|
redirectAnthropic: args["redirect-anthropic"],
|
|
8958
8822
|
stripServerTools: args["strip-server-tools"],
|
|
8959
|
-
contextEditing: args["context-editing"],
|
|
8823
|
+
contextEditing: parseContextEditing(args["context-editing"]),
|
|
8960
8824
|
timezoneOffset: parseTimezoneOffset(args["timezone-offset"]),
|
|
8961
8825
|
posthogKey: args["posthog-key"]
|
|
8962
8826
|
});
|