@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.
Files changed (2) hide show
  1. package/dist/main.mjs +422 -556
  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.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 = 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");
@@ -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$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;
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
- //#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", {
@@ -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 that aren't available on GitHub Copilot
4606
- * to equivalent models that are.
4590
+ * Maps Gemini model names to equivalent models available on GitHub Copilot.
4591
+ *
4592
+ * Two types of aliases:
4607
4593
  *
4608
- * The Gemini CLI's routing classifier requests gemini-2.5-flash-lite
4609
- * and gemini-2.5-flash, which Copilot doesn't serve. We map them to
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
- * Aliases are only applied when the requested model is absent from
4613
- * the Copilot model list, so if Copilot adds support for these models
4614
- * natively, requests will go through unchanged.
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 GEMINI_MODEL_ALIASES = {
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 (!(model in GEMINI_MODEL_ALIASES)) return model;
4622
- if (state.models?.data.some((m) => m.id === model)) return model;
4623
- return GEMINI_MODEL_ALIASES[model];
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 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 {
6312
+ const thresholdIndex = findCompressThreshold(messages, tokenLimit, byteLimit, preservePercent, estimateMessageTokens);
6313
+ if (thresholdIndex >= messages.length) return {
6371
6314
  messages,
6372
6315
  compressedCount: 0,
6373
- compressThresholdIndex: n
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 = state.models?.data.find((m) => m.id === filteredPayload.model);
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 (state.models?.data.find((m) => m.id === modelId))?.vendor === "Anthropic";
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 = state.models?.data.find((m) => m.id === anthropicPayload.model);
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: extractToolCallsFromAnthropicContent(response.content)
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 = prependMarkerToAnthropicResponse$1(response, createTruncationMarker$1(truncateResult));
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 = state.models?.data.find((model) => model.id === translatedPayload.model);
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 = createTruncationResponseMarkerOpenAI(ctx.truncateResult);
7867
- anthropicResponse = prependMarkerToAnthropicResponse(anthropicResponse, marker);
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 = createTruncationResponseMarkerOpenAI(ctx.truncateResult);
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 = state.models?.data.find((model) => model.id === openAIPayload.model);
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 (!((state.models?.data.find((m) => m.id === payload.model))?.supported_endpoints?.includes(RESPONSES_ENDPOINT) ?? false)) {
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
  });