@dianshuv/copilot-api 0.6.2 → 0.7.0

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