@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.
Files changed (2) hide show
  1. package/dist/main.mjs +409 -545
  2. 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.6.3";
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 = historyState.entries.find((e) => e.id === id);
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 historyState.entries.find((e) => e.id === id);
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 n = messages.length;
3151
- const cumTokens = Array.from({ length: n + 1 }, () => 0);
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: n
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$1) {
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$1(msg.content)
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$1(model, {
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$1(model, cfg);
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$1(m) + 1, 0);
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$1({
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
- 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
- //#endregion
3611
- //#region src/services/copilot/create-chat-completions.ts
3612
- const createChatCompletions = async (payload, options) => {
3613
- if (!state.copilotToken) throw new Error("Copilot token not found");
3614
- const enableVision = payload.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
3615
- const isAgentCall = payload.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
3616
- const headers = {
3617
- ...copilotHeaders(state, {
3618
- vision: enableVision,
3619
- intent: isAgentCall ? "conversation-agent" : "conversation-panel"
3620
- }),
3621
- "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
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
- const response = await fetch(`${copilotBaseUrl(state)}/chat/completions`, {
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 = state.models?.data.find((model) => model.id === originalPayload.model);
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 = createTruncationResponseMarkerOpenAI(ctx.truncateResult);
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 = createTruncationResponseMarkerOpenAI(ctx.truncateResult);
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 = state.models?.data.find((m) => m.id === model);
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 = state.models?.data.find((m) => m.id === model);
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 n = messages.length;
6353
- const cumTokens = Array.from({ length: n + 1 }, () => 0);
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: n
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 = state.models?.data.find((m) => m.id === filteredPayload.model);
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 (state.models?.data.find((m) => m.id === modelId))?.vendor === "Anthropic";
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 = state.models?.data.find((m) => m.id === anthropicPayload.model);
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: extractToolCallsFromAnthropicContent(response.content)
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 = prependMarkerToAnthropicResponse$1(response, createTruncationMarker$1(truncateResult));
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 = state.models?.data.find((model) => model.id === translatedPayload.model);
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 = createTruncationResponseMarkerOpenAI(ctx.truncateResult);
7867
- anthropicResponse = prependMarkerToAnthropicResponse(anthropicResponse, marker);
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 = createTruncationResponseMarkerOpenAI(ctx.truncateResult);
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 = state.models?.data.find((model) => model.id === openAIPayload.model);
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 (!((state.models?.data.find((m) => m.id === payload.model))?.supported_endpoints?.includes(RESPONSES_ENDPOINT) ?? false)) {
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!",
5
5
  "author": "dianshuv",
6
6
  "type": "module",