@dianshuv/copilot-api 0.6.3 → 0.7.1
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 +422 -556
- 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.1";
|
|
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");
|
|
@@ -3392,246 +3552,70 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
|
|
|
3392
3552
|
removedMessageCount: 0
|
|
3393
3553
|
};
|
|
3394
3554
|
}
|
|
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
|
|
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
|
|
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;
|
|
3587
|
-
}
|
|
3588
|
-
const period = n - pi[n - 1];
|
|
3589
|
-
if (period < n && n % period === 0) return period;
|
|
3590
|
-
if (period < n && pi[n - 1] >= period) return period;
|
|
3591
|
-
return n;
|
|
3592
|
-
}
|
|
3593
|
-
/**
|
|
3594
|
-
* Create a repetition detector callback for use in stream processing.
|
|
3595
|
-
* Returns a function that accepts text deltas and logs a warning on first detection.
|
|
3596
|
-
*/
|
|
3597
|
-
function createStreamRepetitionChecker(label, config) {
|
|
3598
|
-
const detector = new RepetitionDetector(config);
|
|
3599
|
-
let warned = false;
|
|
3600
|
-
return (textDelta) => {
|
|
3601
|
-
const isRepetitive = detector.feed(textDelta);
|
|
3602
|
-
if (isRepetitive && !warned) {
|
|
3603
|
-
warned = true;
|
|
3604
|
-
consola.warn(`[RepetitionDetector] ${label}: Repetitive output detected in stream`);
|
|
3605
|
-
}
|
|
3606
|
-
return isRepetitive;
|
|
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", {
|
|
@@ -4602,25 +4587,27 @@ function handleNonStreamResponse(c, response, model, ctx, payload) {
|
|
|
4602
4587
|
//#endregion
|
|
4603
4588
|
//#region src/routes/gemini/model-alias.ts
|
|
4604
4589
|
/**
|
|
4605
|
-
* Maps Gemini model names
|
|
4606
|
-
*
|
|
4590
|
+
* Maps Gemini model names to equivalent models available on GitHub Copilot.
|
|
4591
|
+
*
|
|
4592
|
+
* Two types of aliases:
|
|
4607
4593
|
*
|
|
4608
|
-
*
|
|
4609
|
-
*
|
|
4610
|
-
* the closest available flash model.
|
|
4594
|
+
* - **Forced**: Always applied regardless of Copilot model availability.
|
|
4595
|
+
* Use when the old model name should never reach the backend.
|
|
4611
4596
|
*
|
|
4612
|
-
*
|
|
4613
|
-
*
|
|
4614
|
-
*
|
|
4597
|
+
* - **Conditional**: Only applied when the requested model is absent from
|
|
4598
|
+
* the Copilot model list, so if Copilot adds native support the request
|
|
4599
|
+
* goes through unchanged.
|
|
4615
4600
|
*/
|
|
4616
|
-
const
|
|
4601
|
+
const GEMINI_FORCED_ALIASES = { "gemini-2.5-pro": "gemini-3.1-pro-preview" };
|
|
4602
|
+
const GEMINI_CONDITIONAL_ALIASES = {
|
|
4617
4603
|
"gemini-2.5-flash-lite": "gemini-3-flash-preview",
|
|
4618
4604
|
"gemini-2.5-flash": "gemini-3-flash-preview"
|
|
4619
4605
|
};
|
|
4620
4606
|
function resolveGeminiModelAlias(model) {
|
|
4621
|
-
if (
|
|
4622
|
-
if (
|
|
4623
|
-
return
|
|
4607
|
+
if (model in GEMINI_FORCED_ALIASES) return GEMINI_FORCED_ALIASES[model];
|
|
4608
|
+
if (!(model in GEMINI_CONDITIONAL_ALIASES)) return model;
|
|
4609
|
+
if (findModelById(model)) return model;
|
|
4610
|
+
return GEMINI_CONDITIONAL_ALIASES[model];
|
|
4624
4611
|
}
|
|
4625
4612
|
|
|
4626
4613
|
//#endregion
|
|
@@ -6220,9 +6207,6 @@ async function countTotalTokens(payload, model) {
|
|
|
6220
6207
|
}
|
|
6221
6208
|
return total;
|
|
6222
6209
|
}
|
|
6223
|
-
function getMessageBytes(msg) {
|
|
6224
|
-
return JSON.stringify(msg).length;
|
|
6225
|
-
}
|
|
6226
6210
|
/**
|
|
6227
6211
|
* Get tool_use IDs from an assistant message.
|
|
6228
6212
|
*/
|
|
@@ -6307,30 +6291,6 @@ function filterOrphanedToolUse(messages) {
|
|
|
6307
6291
|
return result;
|
|
6308
6292
|
}
|
|
6309
6293
|
/**
|
|
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
6294
|
* Compress a tool_result block in an Anthropic message.
|
|
6335
6295
|
*/
|
|
6336
6296
|
function compressToolResultBlock(block) {
|
|
@@ -6349,28 +6309,11 @@ function compressToolResultBlock(block) {
|
|
|
6349
6309
|
* @param preservePercent - Percentage of context to preserve uncompressed (0.0-1.0)
|
|
6350
6310
|
*/
|
|
6351
6311
|
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 {
|
|
6312
|
+
const thresholdIndex = findCompressThreshold(messages, tokenLimit, byteLimit, preservePercent, estimateMessageTokens);
|
|
6313
|
+
if (thresholdIndex >= messages.length) return {
|
|
6371
6314
|
messages,
|
|
6372
6315
|
compressedCount: 0,
|
|
6373
|
-
compressThresholdIndex:
|
|
6316
|
+
compressThresholdIndex: messages.length
|
|
6374
6317
|
};
|
|
6375
6318
|
const result = [];
|
|
6376
6319
|
let compressedCount = 0;
|
|
@@ -6399,68 +6342,6 @@ function smartCompressToolResults(messages, tokenLimit, byteLimit, preservePerce
|
|
|
6399
6342
|
compressThresholdIndex: thresholdIndex
|
|
6400
6343
|
};
|
|
6401
6344
|
}
|
|
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
6345
|
/**
|
|
6465
6346
|
* Add a compression notice to the system prompt.
|
|
6466
6347
|
* Informs the model that some tool_result content has been compressed.
|
|
@@ -6512,7 +6393,7 @@ async function autoTruncateAnthropic(payload, model, config = {}) {
|
|
|
6512
6393
|
...DEFAULT_AUTO_TRUNCATE_CONFIG,
|
|
6513
6394
|
...config
|
|
6514
6395
|
};
|
|
6515
|
-
const { tokenLimit, byteLimit } = calculateLimits(model, cfg);
|
|
6396
|
+
const { tokenLimit, byteLimit } = calculateLimits(model, cfg, 2e5);
|
|
6516
6397
|
const originalBytes = JSON.stringify(payload).length;
|
|
6517
6398
|
const originalTokens = await countTotalTokens(payload, model);
|
|
6518
6399
|
if (originalTokens <= tokenLimit && originalBytes <= byteLimit) return {
|
|
@@ -6565,7 +6446,8 @@ async function autoTruncateAnthropic(payload, model, config = {}) {
|
|
|
6565
6446
|
systemTokens,
|
|
6566
6447
|
payloadOverhead,
|
|
6567
6448
|
tokenLimit,
|
|
6568
|
-
byteLimit
|
|
6449
|
+
byteLimit,
|
|
6450
|
+
estimateTokens: estimateMessageTokens
|
|
6569
6451
|
});
|
|
6570
6452
|
if (preserveIndex === 0) {
|
|
6571
6453
|
consola.warn("[AutoTruncate:Anthropic] Cannot truncate, system messages too large");
|
|
@@ -6590,7 +6472,7 @@ async function autoTruncateAnthropic(payload, model, config = {}) {
|
|
|
6590
6472
|
let preserved = workingMessages.slice(preserveIndex);
|
|
6591
6473
|
preserved = filterOrphanedToolResults(preserved);
|
|
6592
6474
|
preserved = filterOrphanedToolUse(preserved);
|
|
6593
|
-
preserved = ensureStartsWithUser(preserved);
|
|
6475
|
+
preserved = ensureStartsWithUser(preserved, "Anthropic");
|
|
6594
6476
|
preserved = filterOrphanedToolResults(preserved);
|
|
6595
6477
|
preserved = filterOrphanedToolUse(preserved);
|
|
6596
6478
|
if (preserved.length === 0) {
|
|
@@ -6605,7 +6487,10 @@ async function autoTruncateAnthropic(payload, model, config = {}) {
|
|
|
6605
6487
|
}
|
|
6606
6488
|
const removedMessages = payload.messages.slice(0, preserveIndex);
|
|
6607
6489
|
const removedCount = workingMessages.length - preserved.length;
|
|
6608
|
-
const summary = generateRemovedMessagesSummary(removedMessages)
|
|
6490
|
+
const summary = generateRemovedMessagesSummary(removedMessages, (msg) => {
|
|
6491
|
+
if (!Array.isArray(msg.content)) return [];
|
|
6492
|
+
return msg.content.filter((block) => block.type === "tool_use").map((block) => block.name);
|
|
6493
|
+
});
|
|
6609
6494
|
let newSystem = payload.system;
|
|
6610
6495
|
let newMessages = preserved;
|
|
6611
6496
|
if (payload.system !== void 0) {
|
|
@@ -6647,7 +6532,7 @@ async function checkNeedsCompactionAnthropic(payload, model, config = {}) {
|
|
|
6647
6532
|
const { tokenLimit, byteLimit } = calculateLimits(model, {
|
|
6648
6533
|
...DEFAULT_AUTO_TRUNCATE_CONFIG,
|
|
6649
6534
|
...config
|
|
6650
|
-
});
|
|
6535
|
+
}, 2e5);
|
|
6651
6536
|
const currentTokens = await countTotalTokens(payload, model);
|
|
6652
6537
|
const currentBytes = JSON.stringify(payload).length;
|
|
6653
6538
|
const exceedsTokens = currentTokens > tokenLimit;
|
|
@@ -6887,7 +6772,7 @@ async function createAnthropicMessages(payload, options) {
|
|
|
6887
6772
|
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
6888
6773
|
let filteredPayload = filterPayloadForCopilot(payload);
|
|
6889
6774
|
filteredPayload = adjustMaxTokensForThinking(filteredPayload);
|
|
6890
|
-
const resolvedModel =
|
|
6775
|
+
const resolvedModel = findModelById(filteredPayload.model);
|
|
6891
6776
|
const enableVision = filteredPayload.messages.some((msg) => {
|
|
6892
6777
|
if (typeof msg.content === "string") return false;
|
|
6893
6778
|
return msg.content.some((block) => block.type === "image");
|
|
@@ -6958,7 +6843,7 @@ function stripServerToolsFromPayload(tools) {
|
|
|
6958
6843
|
*/
|
|
6959
6844
|
function supportsDirectAnthropicApi(modelId) {
|
|
6960
6845
|
if (state.redirectAnthropic) return false;
|
|
6961
|
-
return (
|
|
6846
|
+
return findModelById(modelId)?.vendor === "Anthropic";
|
|
6962
6847
|
}
|
|
6963
6848
|
|
|
6964
6849
|
//#endregion
|
|
@@ -7002,15 +6887,6 @@ function extractSystemPrompt(system) {
|
|
|
7002
6887
|
return system.map((block) => block.text).join("\n");
|
|
7003
6888
|
}
|
|
7004
6889
|
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
6890
|
const tools = [];
|
|
7015
6891
|
for (const block of content) if (block.type === "tool_use") tools.push({
|
|
7016
6892
|
id: block.id,
|
|
@@ -7028,6 +6904,25 @@ function mapOpenAIStopReasonToAnthropic(finishReason) {
|
|
|
7028
6904
|
content_filter: "end_turn"
|
|
7029
6905
|
}[finishReason];
|
|
7030
6906
|
}
|
|
6907
|
+
function prependMarkerToResponse(response, marker) {
|
|
6908
|
+
if (!marker) return response;
|
|
6909
|
+
const content = [...response.content];
|
|
6910
|
+
const firstTextIndex = content.findIndex((block) => block.type === "text");
|
|
6911
|
+
if (firstTextIndex !== -1) {
|
|
6912
|
+
const textBlock = content[firstTextIndex];
|
|
6913
|
+
if (textBlock.type === "text") content[firstTextIndex] = {
|
|
6914
|
+
...textBlock,
|
|
6915
|
+
text: marker + (textBlock.text ?? "")
|
|
6916
|
+
};
|
|
6917
|
+
} else content.unshift({
|
|
6918
|
+
type: "text",
|
|
6919
|
+
text: marker
|
|
6920
|
+
});
|
|
6921
|
+
return {
|
|
6922
|
+
...response,
|
|
6923
|
+
content
|
|
6924
|
+
};
|
|
6925
|
+
}
|
|
7031
6926
|
|
|
7032
6927
|
//#endregion
|
|
7033
6928
|
//#region src/routes/messages/stream-accumulator.ts
|
|
@@ -7589,7 +7484,7 @@ function translateErrorToAnthropicErrorEvent() {
|
|
|
7589
7484
|
*/
|
|
7590
7485
|
async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiatorOverride) {
|
|
7591
7486
|
consola.debug("Using direct Anthropic API path for model:", anthropicPayload.model);
|
|
7592
|
-
const selectedModel =
|
|
7487
|
+
const selectedModel = findModelById(anthropicPayload.model);
|
|
7593
7488
|
let effectivePayload = anthropicPayload;
|
|
7594
7489
|
let truncateResult;
|
|
7595
7490
|
if (state.autoTruncate && selectedModel) {
|
|
@@ -7672,7 +7567,7 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
|
|
|
7672
7567
|
}
|
|
7673
7568
|
})
|
|
7674
7569
|
},
|
|
7675
|
-
toolCalls:
|
|
7570
|
+
toolCalls: extractToolCallsFromContent(response.content)
|
|
7676
7571
|
}, Date.now() - ctx.startTime);
|
|
7677
7572
|
if (ctx.trackingId) requestTracker.updateRequest(ctx.trackingId, {
|
|
7678
7573
|
inputTokens: response.usage.input_tokens,
|
|
@@ -7690,34 +7585,12 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
|
|
|
7690
7585
|
stopReason: response.stop_reason ?? void 0
|
|
7691
7586
|
});
|
|
7692
7587
|
let finalResponse = response;
|
|
7693
|
-
if (state.verbose && truncateResult?.wasCompacted) finalResponse =
|
|
7588
|
+
if (state.verbose && truncateResult?.wasCompacted) finalResponse = prependMarkerToResponse(response, createTruncationMarker$1(truncateResult));
|
|
7694
7589
|
logServerToolBlocks(finalResponse.content);
|
|
7695
7590
|
finalResponse = filterServerToolBlocksFromResponse(finalResponse);
|
|
7696
7591
|
return c.json(finalResponse);
|
|
7697
7592
|
}
|
|
7698
7593
|
/**
|
|
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
7594
|
* Handle streaming direct Anthropic response (passthrough SSE events)
|
|
7722
7595
|
*/
|
|
7723
7596
|
async function handleDirectAnthropicStreamingResponse(opts) {
|
|
@@ -7826,7 +7699,7 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
|
|
|
7826
7699
|
const { payload: translatedPayload, toolNameMapping } = translateToOpenAI(anthropicPayload);
|
|
7827
7700
|
consola.debug("Translated OpenAI request payload:", JSON.stringify(translatedPayload));
|
|
7828
7701
|
updateTrackerResolvedModel(ctx.trackingId, translatedPayload.model);
|
|
7829
|
-
const selectedModel =
|
|
7702
|
+
const selectedModel = findModelById(translatedPayload.model);
|
|
7830
7703
|
const { finalPayload: openAIPayload, truncateResult } = await buildFinalPayload(translatedPayload, selectedModel);
|
|
7831
7704
|
if (truncateResult) ctx.truncateResult = truncateResult;
|
|
7832
7705
|
if (state.manualApprove) await awaitApproval();
|
|
@@ -7863,8 +7736,8 @@ function handleNonStreamingResponse(opts) {
|
|
|
7863
7736
|
let anthropicResponse = translateToAnthropic(response, toolNameMapping);
|
|
7864
7737
|
consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
|
|
7865
7738
|
if (state.verbose && ctx.truncateResult?.wasCompacted) {
|
|
7866
|
-
const marker =
|
|
7867
|
-
anthropicResponse =
|
|
7739
|
+
const marker = createTruncationMarker$1(ctx.truncateResult);
|
|
7740
|
+
anthropicResponse = prependMarkerToResponse(anthropicResponse, marker);
|
|
7868
7741
|
}
|
|
7869
7742
|
recordResponse(ctx.historyId, {
|
|
7870
7743
|
success: true,
|
|
@@ -7906,24 +7779,6 @@ function handleNonStreamingResponse(opts) {
|
|
|
7906
7779
|
});
|
|
7907
7780
|
return c.json(anthropicResponse);
|
|
7908
7781
|
}
|
|
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
7782
|
async function handleStreamingResponse(opts) {
|
|
7928
7783
|
const { stream, response, toolNameMapping, anthropicPayload, ctx } = opts;
|
|
7929
7784
|
const streamState = {
|
|
@@ -7936,7 +7791,7 @@ async function handleStreamingResponse(opts) {
|
|
|
7936
7791
|
const checkRepetition = createStreamRepetitionChecker(`translated:${anthropicPayload.model}`);
|
|
7937
7792
|
try {
|
|
7938
7793
|
if (ctx.truncateResult?.wasCompacted) {
|
|
7939
|
-
const marker =
|
|
7794
|
+
const marker = createTruncationMarker$1(ctx.truncateResult);
|
|
7940
7795
|
await sendTruncationMarkerEvent(stream, streamState, marker);
|
|
7941
7796
|
acc.content += marker;
|
|
7942
7797
|
}
|
|
@@ -8110,7 +7965,7 @@ async function handleCountTokens(c) {
|
|
|
8110
7965
|
const anthropicPayload = await c.req.json();
|
|
8111
7966
|
anthropicPayload.model = resolveModelFromBetaHeader(anthropicPayload.model, anthropicBeta);
|
|
8112
7967
|
const { payload: openAIPayload } = translateToOpenAI(anthropicPayload);
|
|
8113
|
-
const selectedModel =
|
|
7968
|
+
const selectedModel = findModelById(openAIPayload.model);
|
|
8114
7969
|
if (!selectedModel) {
|
|
8115
7970
|
consola.warn("Model not found, returning default token count");
|
|
8116
7971
|
return c.json({ input_tokens: 1 });
|
|
@@ -8469,7 +8324,7 @@ const handleResponses = async (c) => {
|
|
|
8469
8324
|
trackingId,
|
|
8470
8325
|
startTime
|
|
8471
8326
|
};
|
|
8472
|
-
if (!((
|
|
8327
|
+
if (!(findModelById(payload.model)?.supported_endpoints?.includes(RESPONSES_ENDPOINT) ?? false)) {
|
|
8473
8328
|
recordErrorResponse(ctx, model, /* @__PURE__ */ new Error("This model does not support the responses endpoint."));
|
|
8474
8329
|
return c.json({ error: {
|
|
8475
8330
|
message: "This model does not support the responses endpoint. Please choose a different model.",
|
|
@@ -8809,6 +8664,17 @@ function parseTimezoneOffset(value) {
|
|
|
8809
8664
|
if (!Number.isFinite(n)) return 8;
|
|
8810
8665
|
return n;
|
|
8811
8666
|
}
|
|
8667
|
+
const validContextEditingModes = [
|
|
8668
|
+
"off",
|
|
8669
|
+
"clear-thinking",
|
|
8670
|
+
"clear-tooluse",
|
|
8671
|
+
"clear-both"
|
|
8672
|
+
];
|
|
8673
|
+
function parseContextEditing(value) {
|
|
8674
|
+
if (validContextEditingModes.includes(value)) return value;
|
|
8675
|
+
consola.warn(`Invalid context editing mode: "${value}", using "off". Valid: ${validContextEditingModes.join(", ")}`);
|
|
8676
|
+
return "off";
|
|
8677
|
+
}
|
|
8812
8678
|
const start = defineCommand({
|
|
8813
8679
|
meta: {
|
|
8814
8680
|
name: "start",
|
|
@@ -8956,7 +8822,7 @@ const start = defineCommand({
|
|
|
8956
8822
|
compressToolResults: args["compress-tool-results"],
|
|
8957
8823
|
redirectAnthropic: args["redirect-anthropic"],
|
|
8958
8824
|
stripServerTools: args["strip-server-tools"],
|
|
8959
|
-
contextEditing: args["context-editing"],
|
|
8825
|
+
contextEditing: parseContextEditing(args["context-editing"]),
|
|
8960
8826
|
timezoneOffset: parseTimezoneOffset(args["timezone-offset"]),
|
|
8961
8827
|
posthogKey: args["posthog-key"]
|
|
8962
8828
|
});
|