@dianshuv/copilot-api 0.6.2 → 0.6.3

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 +428 -180
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -66,7 +66,8 @@ copilot-api start
66
66
  | `--no-auto-truncate` | Disable auto-truncate when exceeding token limits | false |
67
67
  | `--compress-tool-results` | Compress old tool results before truncating | false |
68
68
  | `--redirect-anthropic` | Force Anthropic through OpenAI translation | false |
69
- | `--no-rewrite-anthropic-tools` | Don't rewrite server-side tools | false |
69
+ | `--strip-server-tools` | Strip server-side tools from Anthropic requests | false |
70
+ | `--context-editing` | Context editing mode: off, clear-thinking, clear-tooluse, clear-both | off |
70
71
  | `--timezone-offset` | Timezone offset in hours from UTC for log timestamps (e.g., +8, -5, 0) | +8 |
71
72
  | `--posthog-key` | PostHog API key for token usage analytics (opt-in) | none |
72
73
 
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,
@@ -482,6 +563,7 @@ async function runAuth(options) {
482
563
  consola.info("Verbose logging enabled");
483
564
  }
484
565
  state.showToken = options.showToken;
566
+ initProxyFromEnv();
485
567
  await ensurePaths();
486
568
  await setupGitHubToken({ force: true });
487
569
  consola.success("GitHub token written to", PATHS.GITHUB_TOKEN_PATH);
@@ -528,6 +610,7 @@ const checkUsage = defineCommand({
528
610
  description: "Show current GitHub Copilot usage/quota information"
529
611
  },
530
612
  async run() {
613
+ initProxyFromEnv();
531
614
  await ensurePaths();
532
615
  await setupGitHubToken();
533
616
  try {
@@ -585,6 +668,7 @@ async function checkTokenExists() {
585
668
  }
586
669
  async function getAccountInfo() {
587
670
  try {
671
+ initProxyFromEnv();
588
672
  await ensurePaths();
589
673
  await setupGitHubToken();
590
674
  if (!state.githubToken) return null;
@@ -673,6 +757,7 @@ const debugModels = defineCommand({
673
757
  },
674
758
  async run({ args }) {
675
759
  state.accountType = args["account-type"];
760
+ initProxyFromEnv();
676
761
  await ensurePaths();
677
762
  if (args["github-token"]) {
678
763
  state.githubToken = args["github-token"];
@@ -1036,7 +1121,7 @@ const patchClaude = defineCommand({
1036
1121
 
1037
1122
  //#endregion
1038
1123
  //#region package.json
1039
- var version = "0.6.2";
1124
+ var version = "0.6.3";
1040
1125
 
1041
1126
  //#endregion
1042
1127
  //#region src/lib/adaptive-rate-limiter.ts
@@ -1908,6 +1993,26 @@ function getTokenStats() {
1908
1993
  timeline
1909
1994
  };
1910
1995
  }
1996
+ function getHistoryEntryCount() {
1997
+ return historyState.entries.length;
1998
+ }
1999
+ function getHistoryMaxEntries() {
2000
+ return historyState.maxEntries;
2001
+ }
2002
+ function setHistoryMaxEntries(max) {
2003
+ historyState.maxEntries = max;
2004
+ }
2005
+ /**
2006
+ * Evict the oldest `count` entries from the history store.
2007
+ * Returns the actual number of entries evicted.
2008
+ */
2009
+ function evictOldestEntries(count) {
2010
+ if (count <= 0) return 0;
2011
+ const actual = Math.min(count, historyState.entries.length);
2012
+ const removed = historyState.entries.splice(0, actual);
2013
+ for (const entry of removed) if (!historyState.entries.some((e) => e.sessionId === entry.sessionId)) historyState.sessions.delete(entry.sessionId);
2014
+ return actual;
2015
+ }
1911
2016
  function exportHistory(format = "json") {
1912
2017
  if (format === "json") return JSON.stringify({
1913
2018
  sessions: Array.from(historyState.sessions.values()),
@@ -1948,6 +2053,91 @@ function exportHistory(format = "json") {
1948
2053
  return [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
1949
2054
  }
1950
2055
 
2056
+ //#endregion
2057
+ //#region src/lib/history-memory-pressure.ts
2058
+ /**
2059
+ * Memory pressure monitor — proactively evicts old history entries
2060
+ * when heap usage approaches the V8 heap limit, preventing OOM crashes.
2061
+ *
2062
+ * Graduated response:
2063
+ * 75–80% Warning: log only, no eviction
2064
+ * 80–90% High: evict entries, reduce maxEntries by 25%
2065
+ * 90%+ Critical: aggressive eviction, reduce maxEntries by 50%
2066
+ */
2067
+ const CHECK_INTERVAL_MS = 3e4;
2068
+ const WARN_THRESHOLD = .75;
2069
+ const EVICT_THRESHOLD = .8;
2070
+ const CRITICAL_THRESHOLD = .9;
2071
+ const WARN_LOG_COOLDOWN_MS = 3e5;
2072
+ let resolvedHeapLimit = null;
2073
+ let timer = null;
2074
+ let lastWarningTime = 0;
2075
+ let totalEvictedCount = 0;
2076
+ async function resolveHeapLimit() {
2077
+ if (resolvedHeapLimit !== null) return resolvedHeapLimit;
2078
+ let limit;
2079
+ try {
2080
+ limit = (await import("node:v8")).getHeapStatistics().heap_size_limit;
2081
+ } catch {
2082
+ limit = 512 * 1024 * 1024;
2083
+ }
2084
+ resolvedHeapLimit = limit;
2085
+ return limit;
2086
+ }
2087
+ function formatMB(bytes) {
2088
+ return `${Math.round(bytes / 1024 / 1024)}MB`;
2089
+ }
2090
+ function formatPct(ratio) {
2091
+ return `${Math.round(ratio * 100)}%`;
2092
+ }
2093
+ async function checkMemoryPressure() {
2094
+ const heapLimit = await resolveHeapLimit();
2095
+ const { heapUsed } = process.memoryUsage();
2096
+ const ratio = heapUsed / heapLimit;
2097
+ if (ratio < WARN_THRESHOLD) return;
2098
+ const currentEntries = getHistoryEntryCount();
2099
+ if (currentEntries <= state.historyMinEntries) {
2100
+ if (ratio >= EVICT_THRESHOLD && Date.now() - lastWarningTime > WARN_LOG_COOLDOWN_MS) {
2101
+ lastWarningTime = Date.now();
2102
+ consola.warn(`[memory] Heap ${formatMB(heapUsed)}/${formatMB(heapLimit)} (${formatPct(ratio)}) — only ${currentEntries} history entries remain. Consider increasing --max-old-space-size`);
2103
+ }
2104
+ return;
2105
+ }
2106
+ if (ratio < EVICT_THRESHOLD) {
2107
+ if (Date.now() - lastWarningTime > WARN_LOG_COOLDOWN_MS) {
2108
+ lastWarningTime = Date.now();
2109
+ consola.warn(`[memory] Heap ${formatMB(heapUsed)}/${formatMB(heapLimit)} (${formatPct(ratio)}) — approaching limit, ${currentEntries} history entries in memory`);
2110
+ }
2111
+ return;
2112
+ }
2113
+ lastWarningTime = Date.now();
2114
+ const currentMax = getHistoryMaxEntries();
2115
+ const newMaxEntries = ratio >= CRITICAL_THRESHOLD ? Math.max(state.historyMinEntries, Math.floor(currentMax * .5)) : Math.max(state.historyMinEntries, Math.floor(currentMax * .75));
2116
+ const evictCount = Math.max(0, currentEntries - newMaxEntries);
2117
+ if (evictCount <= 0) return;
2118
+ const evicted = evictOldestEntries(evictCount);
2119
+ totalEvictedCount += evicted;
2120
+ if (newMaxEntries < currentMax) setHistoryMaxEntries(newMaxEntries);
2121
+ const afterHeapUsed = process.memoryUsage().heapUsed;
2122
+ consola.warn(`[memory] Evicted ${evicted} history entries due to memory pressure (heap: ${formatMB(heapUsed)} → ${formatMB(afterHeapUsed)}/${formatMB(heapLimit)}, entries: ${currentEntries} → ${currentEntries - evicted}, max: ${newMaxEntries})`);
2123
+ globalThis.gc?.();
2124
+ }
2125
+ function startMemoryPressureMonitor() {
2126
+ if (timer) return;
2127
+ timer = setInterval(() => {
2128
+ checkMemoryPressure().catch((error) => {
2129
+ consola.error("[memory] Error in memory pressure check:", error);
2130
+ });
2131
+ }, CHECK_INTERVAL_MS);
2132
+ if ("unref" in timer) timer.unref();
2133
+ }
2134
+ function stopMemoryPressureMonitor() {
2135
+ if (timer) {
2136
+ clearInterval(timer);
2137
+ timer = null;
2138
+ }
2139
+ }
2140
+
1951
2141
  //#endregion
1952
2142
  //#region src/lib/posthog.ts
1953
2143
  let client = null;
@@ -1997,81 +2187,6 @@ async function shutdownPostHog() {
1997
2187
  }
1998
2188
  }
1999
2189
 
2000
- //#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) {
2009
- 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);
2019
- } 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);
2035
- }
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;
2044
- }
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
2190
  //#endregion
2076
2191
  //#region src/lib/shell.ts
2077
2192
  function getShell() {
@@ -2184,6 +2299,7 @@ async function gracefulShutdown(signal, deps) {
2184
2299
  try {
2185
2300
  deps?.contextManager?.stopReaper();
2186
2301
  } catch {}
2302
+ stopMemoryPressureMonitor();
2187
2303
  stopRefresh();
2188
2304
  const wsClients = getWsCount();
2189
2305
  if (wsClients > 0) {
@@ -6550,6 +6666,156 @@ async function checkNeedsCompactionAnthropic(payload, model, config = {}) {
6550
6666
  };
6551
6667
  }
6552
6668
 
6669
+ //#endregion
6670
+ //#region src/lib/anthropic/features.ts
6671
+ function normalizeForMatching(modelId) {
6672
+ return modelId.toLowerCase().replaceAll(/[-_.]/g, "").replace(/\d{8}$/, "");
6673
+ }
6674
+ function modelSupportsContextEditing(modelId) {
6675
+ const n = normalizeForMatching(modelId);
6676
+ 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"));
6677
+ }
6678
+ function modelSupportsToolSearch(modelId) {
6679
+ const n = normalizeForMatching(modelId);
6680
+ return n.includes("claude") && (n.includes("opus45") || n.includes("opus46") || n.includes("sonnet45") || n.includes("sonnet46"));
6681
+ }
6682
+ function isContextEditingEnabled(modelId) {
6683
+ return modelSupportsContextEditing(modelId) && state.contextEditingMode !== "off";
6684
+ }
6685
+ function modelHasAdaptiveThinking(resolvedModel) {
6686
+ return resolvedModel?.capabilities?.supports?.adaptive_thinking === true;
6687
+ }
6688
+ function buildAnthropicBetaHeaders(modelId, resolvedModel) {
6689
+ const headers = {};
6690
+ const betaFeatures = [];
6691
+ if (!modelHasAdaptiveThinking(resolvedModel)) betaFeatures.push("interleaved-thinking-2025-05-14");
6692
+ if (isContextEditingEnabled(modelId)) betaFeatures.push("context-management-2025-06-27");
6693
+ if (modelSupportsToolSearch(modelId)) betaFeatures.push("advanced-tool-use-2025-11-20");
6694
+ if (betaFeatures.length > 0) headers["anthropic-beta"] = betaFeatures.join(",");
6695
+ return headers;
6696
+ }
6697
+ const THINKING_KEEP_TURNS = 2;
6698
+ const TOOL_USE_TRIGGER_TYPE = "input_tokens";
6699
+ const TOOL_USE_TRIGGER_VALUE = 8e4;
6700
+ const TOOL_USE_KEEP_COUNT = 10;
6701
+ function buildContextManagement(mode, hasThinking) {
6702
+ if (mode === "off") return void 0;
6703
+ const edits = [];
6704
+ if ((mode === "clear-thinking" || mode === "clear-both") && hasThinking) edits.push({
6705
+ type: "clear_thinking_20251015",
6706
+ keep: {
6707
+ type: "thinking_turns",
6708
+ value: Math.max(1, THINKING_KEEP_TURNS)
6709
+ }
6710
+ });
6711
+ if (mode === "clear-tooluse" || mode === "clear-both") edits.push({
6712
+ type: "clear_tool_uses_20250919",
6713
+ trigger: {
6714
+ type: TOOL_USE_TRIGGER_TYPE,
6715
+ value: TOOL_USE_TRIGGER_VALUE
6716
+ },
6717
+ keep: {
6718
+ type: "tool_uses",
6719
+ value: TOOL_USE_KEEP_COUNT
6720
+ }
6721
+ });
6722
+ return edits.length > 0 ? { edits } : void 0;
6723
+ }
6724
+
6725
+ //#endregion
6726
+ //#region src/lib/anthropic/server-tool-filter.ts
6727
+ const SERVER_TOOL_TYPE_PREFIXES = [
6728
+ "web_search_",
6729
+ "web_fetch_",
6730
+ "code_execution_",
6731
+ "text_editor_",
6732
+ "computer_",
6733
+ "bash_"
6734
+ ];
6735
+ /** Check if a block type is a server-side tool result (ends with _tool_result, but not plain tool_result) */
6736
+ function isServerToolResultType(type) {
6737
+ return type !== "tool_result" && type.endsWith("_tool_result");
6738
+ }
6739
+ /** Check if a content block is a server-side tool block */
6740
+ function isServerToolBlock(block) {
6741
+ if (block.type === "server_tool_use") return true;
6742
+ return isServerToolResultType(block.type);
6743
+ }
6744
+ /** Check if a tool's type field matches a known server tool prefix */
6745
+ function isServerToolType(type) {
6746
+ if (!type) return false;
6747
+ return SERVER_TOOL_TYPE_PREFIXES.some((prefix) => type.startsWith(prefix));
6748
+ }
6749
+ /** Log a single server tool block */
6750
+ function logServerToolBlock(block) {
6751
+ if (block.type === "server_tool_use") {
6752
+ consola.debug(`[ServerTool] server_tool_use: ${block.name}`);
6753
+ return;
6754
+ }
6755
+ if (!isServerToolResultType(block.type)) return;
6756
+ consola.debug(`[ServerTool] ${block.type}`);
6757
+ }
6758
+ /** Log all server tool blocks from a non-streaming response */
6759
+ function logServerToolBlocks(content) {
6760
+ for (const block of content) logServerToolBlock(block);
6761
+ }
6762
+ /**
6763
+ * Creates a filter for server tool blocks in SSE streams.
6764
+ * Handles index remapping so block indices remain dense/sequential after filtering.
6765
+ * Always active — matching vscode-copilot-chat behavior.
6766
+ */
6767
+ function createServerToolBlockFilter() {
6768
+ const filteredIndices = /* @__PURE__ */ new Set();
6769
+ const clientIndexMap = /* @__PURE__ */ new Map();
6770
+ let nextClientIndex = 0;
6771
+ function getClientIndex(apiIndex) {
6772
+ let idx = clientIndexMap.get(apiIndex);
6773
+ if (idx === void 0) {
6774
+ idx = nextClientIndex++;
6775
+ clientIndexMap.set(apiIndex, idx);
6776
+ }
6777
+ return idx;
6778
+ }
6779
+ return { rewriteEvent(parsed, rawData) {
6780
+ if (!parsed) return rawData;
6781
+ if (parsed.type === "content_block_start") {
6782
+ const block = parsed.content_block;
6783
+ if (isServerToolBlock(block)) {
6784
+ filteredIndices.add(parsed.index);
6785
+ return null;
6786
+ }
6787
+ if (filteredIndices.size === 0) {
6788
+ getClientIndex(parsed.index);
6789
+ return rawData;
6790
+ }
6791
+ const clientIndex = getClientIndex(parsed.index);
6792
+ if (clientIndex === parsed.index) return rawData;
6793
+ const obj = JSON.parse(rawData);
6794
+ obj.index = clientIndex;
6795
+ return JSON.stringify(obj);
6796
+ }
6797
+ if (parsed.type === "content_block_delta" || parsed.type === "content_block_stop") {
6798
+ if (filteredIndices.has(parsed.index)) return null;
6799
+ if (filteredIndices.size === 0) return rawData;
6800
+ const clientIndex = getClientIndex(parsed.index);
6801
+ if (clientIndex === parsed.index) return rawData;
6802
+ const obj = JSON.parse(rawData);
6803
+ obj.index = clientIndex;
6804
+ return JSON.stringify(obj);
6805
+ }
6806
+ return rawData;
6807
+ } };
6808
+ }
6809
+ /** Filter server tool blocks from a non-streaming response */
6810
+ function filterServerToolBlocksFromResponse(response) {
6811
+ const filtered = response.content.filter((block) => !isServerToolBlock(block));
6812
+ if (filtered.length === response.content.length) return response;
6813
+ return {
6814
+ ...response,
6815
+ content: filtered
6816
+ };
6817
+ }
6818
+
6553
6819
  //#endregion
6554
6820
  //#region src/services/copilot/create-anthropic-messages.ts
6555
6821
  /**
@@ -6574,14 +6840,15 @@ const COPILOT_SUPPORTED_FIELDS = new Set([
6574
6840
  "tools",
6575
6841
  "tool_choice",
6576
6842
  "thinking",
6577
- "service_tier"
6843
+ "service_tier",
6844
+ "context_management"
6578
6845
  ]);
6579
6846
  /**
6580
6847
  * Filter payload to only include fields supported by Copilot's Anthropic API.
6581
6848
  * This prevents errors like "Extra inputs are not permitted" for unsupported
6582
6849
  * fields like `output_config`.
6583
6850
  *
6584
- * Also converts server-side tools (web_search, etc.) to custom tools.
6851
+ * Optionally strips server-side tools when state.stripServerTools is enabled.
6585
6852
  */
6586
6853
  function filterPayloadForCopilot(payload) {
6587
6854
  const filtered = {};
@@ -6589,7 +6856,7 @@ function filterPayloadForCopilot(payload) {
6589
6856
  for (const [key, value] of Object.entries(payload)) if (COPILOT_SUPPORTED_FIELDS.has(key)) filtered[key] = value;
6590
6857
  else unsupportedFields.push(key);
6591
6858
  if (unsupportedFields.length > 0) consola.debug(`[DirectAnthropic] Filtered unsupported fields: ${unsupportedFields.join(", ")}`);
6592
- if (filtered.tools) filtered.tools = convertServerToolsToCustom(filtered.tools);
6859
+ if (filtered.tools) filtered.tools = stripServerToolsFromPayload(filtered.tools);
6593
6860
  return filtered;
6594
6861
  }
6595
6862
  /**
@@ -6620,6 +6887,7 @@ async function createAnthropicMessages(payload, options) {
6620
6887
  if (!state.copilotToken) throw new Error("Copilot token not found");
6621
6888
  let filteredPayload = filterPayloadForCopilot(payload);
6622
6889
  filteredPayload = adjustMaxTokensForThinking(filteredPayload);
6890
+ const resolvedModel = state.models?.data.find((m) => m.id === filteredPayload.model);
6623
6891
  const enableVision = filteredPayload.messages.some((msg) => {
6624
6892
  if (typeof msg.content === "string") return false;
6625
6893
  return msg.content.some((block) => block.type === "image");
@@ -6633,6 +6901,16 @@ async function createAnthropicMessages(payload, options) {
6633
6901
  "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user"),
6634
6902
  "anthropic-version": "2023-06-01"
6635
6903
  };
6904
+ const betaHeaders = buildAnthropicBetaHeaders(filteredPayload.model, resolvedModel);
6905
+ Object.assign(headers, betaHeaders);
6906
+ if (isContextEditingEnabled(filteredPayload.model)) {
6907
+ const hasThinking = filteredPayload.thinking?.type === "enabled";
6908
+ const cm = buildContextManagement(state.contextEditingMode, hasThinking);
6909
+ if (cm) {
6910
+ filteredPayload.context_management = cm;
6911
+ consola.debug("[DirectAnthropic] Added context_management:", JSON.stringify(cm));
6912
+ }
6913
+ }
6636
6914
  consola.debug("Sending direct Anthropic request to Copilot /v1/messages");
6637
6915
  const response = await fetch(`${copilotBaseUrl(state)}/v1/messages`, {
6638
6916
  method: "POST",
@@ -6656,96 +6934,21 @@ async function createAnthropicMessages(payload, options) {
6656
6934
  if (payload.stream) return events(response);
6657
6935
  return await response.json();
6658
6936
  }
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
6937
  /**
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.
6938
+ * Strip server-side tools from the tools array when configured.
6939
+ * When state.stripServerTools is enabled, server tools are removed from the request.
6940
+ * When disabled (default), server tools are passed through unchanged.
6725
6941
  */
6726
- function convertServerToolsToCustom(tools) {
6727
- if (!tools) return;
6942
+ function stripServerToolsFromPayload(tools) {
6943
+ if (!tools) return void 0;
6944
+ if (!state.stripServerTools) return tools;
6728
6945
  const result = [];
6729
6946
  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);
6947
+ if (isServerToolType(tool.type)) {
6948
+ consola.warn(`[DirectAnthropic] Stripping server tool: ${tool.name} (type: ${tool.type})`);
6949
+ continue;
6950
+ }
6951
+ result.push(tool);
6749
6952
  }
6750
6953
  return result.length > 0 ? result : void 0;
6751
6954
  }
@@ -6840,9 +7043,6 @@ function createAnthropicStreamAccumulator() {
6840
7043
  currentToolCall: null
6841
7044
  };
6842
7045
  }
6843
- function isServerToolResultType(type) {
6844
- return type !== "tool_result" && type.endsWith("_tool_result");
6845
- }
6846
7046
  function processAnthropicEvent(event, acc) {
6847
7047
  switch (event.type) {
6848
7048
  case "content_block_delta":
@@ -7491,6 +7691,8 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
7491
7691
  });
7492
7692
  let finalResponse = response;
7493
7693
  if (state.verbose && truncateResult?.wasCompacted) finalResponse = prependMarkerToAnthropicResponse$1(response, createTruncationMarker$1(truncateResult));
7694
+ logServerToolBlocks(finalResponse.content);
7695
+ finalResponse = filterServerToolBlocksFromResponse(finalResponse);
7494
7696
  return c.json(finalResponse);
7495
7697
  }
7496
7698
  /**
@@ -7522,6 +7724,7 @@ async function handleDirectAnthropicStreamingResponse(opts) {
7522
7724
  const { stream, response, anthropicPayload, ctx } = opts;
7523
7725
  const acc = createAnthropicStreamAccumulator();
7524
7726
  const checkRepetition = createStreamRepetitionChecker(`anthropic:${anthropicPayload.model}`);
7727
+ const serverToolFilter = createServerToolBlockFilter();
7525
7728
  try {
7526
7729
  for await (const rawEvent of response) {
7527
7730
  consola.debug("Direct Anthropic raw stream event:", JSON.stringify(rawEvent));
@@ -7535,10 +7738,13 @@ async function handleDirectAnthropicStreamingResponse(opts) {
7535
7738
  continue;
7536
7739
  }
7537
7740
  processAnthropicEvent(event, acc);
7741
+ if (event.type === "content_block_start") logServerToolBlock(event.content_block);
7538
7742
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") checkRepetition(event.delta.text);
7743
+ const forwardData = serverToolFilter.rewriteEvent(event, rawEvent.data);
7744
+ if (forwardData === null) continue;
7539
7745
  await stream.writeSSE({
7540
7746
  event: rawEvent.event || event.type,
7541
- data: rawEvent.data
7747
+ data: forwardData
7542
7748
  });
7543
7749
  }
7544
7750
  recordAnthropicStreamingResponse(acc, anthropicPayload.model, ctx);
@@ -8056,6 +8262,38 @@ const handleItemId = (parsed, tracker) => {
8056
8262
 
8057
8263
  //#endregion
8058
8264
  //#region src/routes/responses/utils.ts
8265
+ const CALL_PREFIX = "call_";
8266
+ const FC_PREFIX = "fc_";
8267
+ /**
8268
+ * Normalize function call IDs in Responses API input.
8269
+ * Converts Chat Completions format `call_xxx` IDs to Responses format `fc_xxx` IDs
8270
+ * on function_call and function_call_output items.
8271
+ */
8272
+ function normalizeCallIds(payload) {
8273
+ if (typeof payload.input === "string") return payload;
8274
+ let count = 0;
8275
+ const input = payload.input.map((item) => {
8276
+ if ("call_id" in item && typeof item.call_id === "string") {
8277
+ const callId = item.call_id;
8278
+ if (callId.startsWith(CALL_PREFIX)) {
8279
+ count++;
8280
+ return {
8281
+ ...item,
8282
+ call_id: FC_PREFIX + callId.slice(5)
8283
+ };
8284
+ }
8285
+ }
8286
+ return item;
8287
+ });
8288
+ if (count > 0) {
8289
+ consola.debug(`[Responses] Normalized ${count} call IDs (call_ → fc_)`);
8290
+ return {
8291
+ ...payload,
8292
+ input
8293
+ };
8294
+ }
8295
+ return payload;
8296
+ }
8059
8297
  const getResponsesRequestOptions = (payload) => {
8060
8298
  return {
8061
8299
  vision: hasVisionInput(payload),
@@ -8206,7 +8444,8 @@ const TERMINAL_EVENTS = new Set([
8206
8444
  "error"
8207
8445
  ]);
8208
8446
  const handleResponses = async (c) => {
8209
- const payload = await c.req.json();
8447
+ let payload = await c.req.json();
8448
+ if (state.normalizeResponsesCallIds) payload = normalizeCallIds(payload);
8210
8449
  consola.debug("Responses request payload:", JSON.stringify(payload));
8211
8450
  const trackingId = c.get("trackingId");
8212
8451
  const startTime = (trackingId ? requestTracker.getRequest(trackingId) : void 0)?.startTime ?? Date.now();
@@ -8485,7 +8724,8 @@ async function runServer(options) {
8485
8724
  state.autoTruncate = options.autoTruncate;
8486
8725
  state.compressToolResults = options.compressToolResults;
8487
8726
  state.redirectAnthropic = options.redirectAnthropic;
8488
- state.rewriteAnthropicTools = options.rewriteAnthropicTools;
8727
+ state.stripServerTools = options.stripServerTools;
8728
+ state.contextEditingMode = options.contextEditing;
8489
8729
  state.timezoneOffset = options.timezoneOffset;
8490
8730
  if (options.rateLimit) initAdaptiveRateLimiter({
8491
8731
  baseRetryIntervalSeconds: options.retryInterval,
@@ -8497,11 +8737,13 @@ async function runServer(options) {
8497
8737
  if (!options.autoTruncate) consola.info("Auto-truncate disabled");
8498
8738
  if (options.compressToolResults) consola.info("Tool result compression enabled");
8499
8739
  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)");
8740
+ if (options.stripServerTools) consola.info("Server-side tools will be stripped from requests");
8741
+ if (options.contextEditing !== "off") consola.info(`Context editing mode: ${options.contextEditing}`);
8501
8742
  initHistory(options.history, options.historyLimit);
8502
8743
  if (options.history) {
8503
8744
  const limitText = options.historyLimit === 0 ? "unlimited" : `max ${options.historyLimit}`;
8504
8745
  consola.info(`History recording enabled (${limitText} entries)`);
8746
+ startMemoryPressureMonitor();
8505
8747
  }
8506
8748
  if (options.posthogKey) {
8507
8749
  initPostHog(options.posthogKey);
@@ -8672,10 +8914,15 @@ const start = defineCommand({
8672
8914
  default: false,
8673
8915
  description: "Redirect Anthropic models through OpenAI translation (instead of direct API)"
8674
8916
  },
8675
- "no-rewrite-anthropic-tools": {
8917
+ "strip-server-tools": {
8676
8918
  type: "boolean",
8677
8919
  default: false,
8678
- description: "Don't rewrite Anthropic server-side tools (web_search, etc.) to custom tool format"
8920
+ description: "Strip Anthropic server-side tools (web_search, etc.) from requests"
8921
+ },
8922
+ "context-editing": {
8923
+ type: "string",
8924
+ default: "off",
8925
+ description: "Context editing mode: off, clear-thinking, clear-tooluse, clear-both"
8679
8926
  },
8680
8927
  "timezone-offset": {
8681
8928
  type: "string",
@@ -8708,7 +8955,8 @@ const start = defineCommand({
8708
8955
  autoTruncate: !args["no-auto-truncate"],
8709
8956
  compressToolResults: args["compress-tool-results"],
8710
8957
  redirectAnthropic: args["redirect-anthropic"],
8711
- rewriteAnthropicTools: !args["no-rewrite-anthropic-tools"],
8958
+ stripServerTools: args["strip-server-tools"],
8959
+ contextEditing: args["context-editing"],
8712
8960
  timezoneOffset: parseTimezoneOffset(args["timezone-offset"]),
8713
8961
  posthogKey: args["posthog-key"]
8714
8962
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!",
5
5
  "author": "dianshuv",
6
6
  "type": "module",