@dianshuv/copilot-api 0.6.1 → 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 +453 -181
  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.1";
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) {
@@ -4483,6 +4599,30 @@ function handleNonStreamResponse(c, response, model, ctx, payload) {
4483
4599
  return c.json(geminiResponse);
4484
4600
  }
4485
4601
 
4602
+ //#endregion
4603
+ //#region src/routes/gemini/model-alias.ts
4604
+ /**
4605
+ * Maps Gemini model names that aren't available on GitHub Copilot
4606
+ * to equivalent models that are.
4607
+ *
4608
+ * The Gemini CLI's routing classifier requests gemini-2.5-flash-lite
4609
+ * and gemini-2.5-flash, which Copilot doesn't serve. We map them to
4610
+ * the closest available flash model.
4611
+ *
4612
+ * Aliases are only applied when the requested model is absent from
4613
+ * the Copilot model list, so if Copilot adds support for these models
4614
+ * natively, requests will go through unchanged.
4615
+ */
4616
+ const GEMINI_MODEL_ALIASES = {
4617
+ "gemini-2.5-flash-lite": "gemini-3-flash-preview",
4618
+ "gemini-2.5-flash": "gemini-3-flash-preview"
4619
+ };
4620
+ function resolveGeminiModelAlias(model) {
4621
+ if (!(model in GEMINI_MODEL_ALIASES)) return model;
4622
+ if (state.models?.data.some((m) => m.id === model)) return model;
4623
+ return GEMINI_MODEL_ALIASES[model];
4624
+ }
4625
+
4486
4626
  //#endregion
4487
4627
  //#region src/routes/gemini/route.ts
4488
4628
  const geminiRoutes = new Hono();
@@ -4490,7 +4630,7 @@ geminiRoutes.post("/:modelAction", async (c) => {
4490
4630
  const modelAction = c.req.param("modelAction");
4491
4631
  const colonIndex = modelAction.lastIndexOf(":");
4492
4632
  if (colonIndex === -1) return geminiError(c, 400, "INVALID_ARGUMENT", "Missing action in URL");
4493
- const model = modelAction.slice(0, Math.max(0, colonIndex));
4633
+ const model = resolveGeminiModelAlias(modelAction.slice(0, Math.max(0, colonIndex)));
4494
4634
  const action = modelAction.slice(Math.max(0, colonIndex + 1));
4495
4635
  switch (action) {
4496
4636
  case "generateContent": return handleGeminiGenerate(c, model, false);
@@ -6526,6 +6666,156 @@ async function checkNeedsCompactionAnthropic(payload, model, config = {}) {
6526
6666
  };
6527
6667
  }
6528
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
+
6529
6819
  //#endregion
6530
6820
  //#region src/services/copilot/create-anthropic-messages.ts
6531
6821
  /**
@@ -6550,14 +6840,15 @@ const COPILOT_SUPPORTED_FIELDS = new Set([
6550
6840
  "tools",
6551
6841
  "tool_choice",
6552
6842
  "thinking",
6553
- "service_tier"
6843
+ "service_tier",
6844
+ "context_management"
6554
6845
  ]);
6555
6846
  /**
6556
6847
  * Filter payload to only include fields supported by Copilot's Anthropic API.
6557
6848
  * This prevents errors like "Extra inputs are not permitted" for unsupported
6558
6849
  * fields like `output_config`.
6559
6850
  *
6560
- * Also converts server-side tools (web_search, etc.) to custom tools.
6851
+ * Optionally strips server-side tools when state.stripServerTools is enabled.
6561
6852
  */
6562
6853
  function filterPayloadForCopilot(payload) {
6563
6854
  const filtered = {};
@@ -6565,7 +6856,7 @@ function filterPayloadForCopilot(payload) {
6565
6856
  for (const [key, value] of Object.entries(payload)) if (COPILOT_SUPPORTED_FIELDS.has(key)) filtered[key] = value;
6566
6857
  else unsupportedFields.push(key);
6567
6858
  if (unsupportedFields.length > 0) consola.debug(`[DirectAnthropic] Filtered unsupported fields: ${unsupportedFields.join(", ")}`);
6568
- if (filtered.tools) filtered.tools = convertServerToolsToCustom(filtered.tools);
6859
+ if (filtered.tools) filtered.tools = stripServerToolsFromPayload(filtered.tools);
6569
6860
  return filtered;
6570
6861
  }
6571
6862
  /**
@@ -6596,6 +6887,7 @@ async function createAnthropicMessages(payload, options) {
6596
6887
  if (!state.copilotToken) throw new Error("Copilot token not found");
6597
6888
  let filteredPayload = filterPayloadForCopilot(payload);
6598
6889
  filteredPayload = adjustMaxTokensForThinking(filteredPayload);
6890
+ const resolvedModel = state.models?.data.find((m) => m.id === filteredPayload.model);
6599
6891
  const enableVision = filteredPayload.messages.some((msg) => {
6600
6892
  if (typeof msg.content === "string") return false;
6601
6893
  return msg.content.some((block) => block.type === "image");
@@ -6609,6 +6901,16 @@ async function createAnthropicMessages(payload, options) {
6609
6901
  "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user"),
6610
6902
  "anthropic-version": "2023-06-01"
6611
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
+ }
6612
6914
  consola.debug("Sending direct Anthropic request to Copilot /v1/messages");
6613
6915
  const response = await fetch(`${copilotBaseUrl(state)}/v1/messages`, {
6614
6916
  method: "POST",
@@ -6632,96 +6934,21 @@ async function createAnthropicMessages(payload, options) {
6632
6934
  if (payload.stream) return events(response);
6633
6935
  return await response.json();
6634
6936
  }
6635
- const SERVER_TOOL_CONFIGS = {
6636
- web_search: {
6637
- 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.",
6638
- input_schema: {
6639
- type: "object",
6640
- properties: { query: {
6641
- type: "string",
6642
- description: "The search query"
6643
- } },
6644
- required: ["query"]
6645
- }
6646
- },
6647
- web_fetch: {
6648
- description: "Fetch content from a URL. NOTE: This is a client-side tool - the client must fetch the URL and return the content.",
6649
- input_schema: {
6650
- type: "object",
6651
- properties: { url: {
6652
- type: "string",
6653
- description: "The URL to fetch"
6654
- } },
6655
- required: ["url"]
6656
- }
6657
- },
6658
- code_execution: {
6659
- description: "Execute code in a sandbox. NOTE: This is a client-side tool - the client must execute the code.",
6660
- input_schema: {
6661
- type: "object",
6662
- properties: {
6663
- code: {
6664
- type: "string",
6665
- description: "The code to execute"
6666
- },
6667
- language: {
6668
- type: "string",
6669
- description: "The programming language"
6670
- }
6671
- },
6672
- required: ["code"]
6673
- }
6674
- },
6675
- computer: {
6676
- description: "Control computer desktop. NOTE: This is a client-side tool - the client must handle computer control.",
6677
- input_schema: {
6678
- type: "object",
6679
- properties: { action: {
6680
- type: "string",
6681
- description: "The action to perform"
6682
- } },
6683
- required: ["action"]
6684
- }
6685
- }
6686
- };
6687
- /**
6688
- * Check if a tool is a server-side tool that needs conversion.
6689
- */
6690
- function getServerToolPrefix(tool) {
6691
- if (tool.type) {
6692
- for (const prefix of Object.keys(SERVER_TOOL_CONFIGS)) if (tool.type.startsWith(prefix)) return prefix;
6693
- }
6694
- return null;
6695
- }
6696
6937
  /**
6697
- * Convert server-side tools to custom tools, or pass them through unchanged.
6698
- * This allows them to be passed to the API and handled by the client.
6699
- *
6700
- * 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.
6701
6941
  */
6702
- function convertServerToolsToCustom(tools) {
6703
- if (!tools) return;
6942
+ function stripServerToolsFromPayload(tools) {
6943
+ if (!tools) return void 0;
6944
+ if (!state.stripServerTools) return tools;
6704
6945
  const result = [];
6705
6946
  for (const tool of tools) {
6706
- const serverToolPrefix = getServerToolPrefix(tool);
6707
- if (serverToolPrefix) {
6708
- const config = SERVER_TOOL_CONFIGS[serverToolPrefix];
6709
- if (!state.rewriteAnthropicTools) {
6710
- consola.debug(`[DirectAnthropic] Passing ${serverToolPrefix} through unchanged (use --rewrite-anthropic-tools to convert)`);
6711
- result.push(tool);
6712
- continue;
6713
- }
6714
- if (config.remove) {
6715
- consola.warn(`[DirectAnthropic] Removing unsupported server tool: ${tool.name}. Reason: ${config.removalReason}`);
6716
- continue;
6717
- }
6718
- consola.debug(`[DirectAnthropic] Converting server tool to custom: ${tool.name} (type: ${tool.type})`);
6719
- result.push({
6720
- name: tool.name,
6721
- description: config.description,
6722
- input_schema: config.input_schema
6723
- });
6724
- } 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);
6725
6952
  }
6726
6953
  return result.length > 0 ? result : void 0;
6727
6954
  }
@@ -6816,9 +7043,6 @@ function createAnthropicStreamAccumulator() {
6816
7043
  currentToolCall: null
6817
7044
  };
6818
7045
  }
6819
- function isServerToolResultType(type) {
6820
- return type !== "tool_result" && type.endsWith("_tool_result");
6821
- }
6822
7046
  function processAnthropicEvent(event, acc) {
6823
7047
  switch (event.type) {
6824
7048
  case "content_block_delta":
@@ -7467,6 +7691,8 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
7467
7691
  });
7468
7692
  let finalResponse = response;
7469
7693
  if (state.verbose && truncateResult?.wasCompacted) finalResponse = prependMarkerToAnthropicResponse$1(response, createTruncationMarker$1(truncateResult));
7694
+ logServerToolBlocks(finalResponse.content);
7695
+ finalResponse = filterServerToolBlocksFromResponse(finalResponse);
7470
7696
  return c.json(finalResponse);
7471
7697
  }
7472
7698
  /**
@@ -7498,6 +7724,7 @@ async function handleDirectAnthropicStreamingResponse(opts) {
7498
7724
  const { stream, response, anthropicPayload, ctx } = opts;
7499
7725
  const acc = createAnthropicStreamAccumulator();
7500
7726
  const checkRepetition = createStreamRepetitionChecker(`anthropic:${anthropicPayload.model}`);
7727
+ const serverToolFilter = createServerToolBlockFilter();
7501
7728
  try {
7502
7729
  for await (const rawEvent of response) {
7503
7730
  consola.debug("Direct Anthropic raw stream event:", JSON.stringify(rawEvent));
@@ -7511,10 +7738,13 @@ async function handleDirectAnthropicStreamingResponse(opts) {
7511
7738
  continue;
7512
7739
  }
7513
7740
  processAnthropicEvent(event, acc);
7741
+ if (event.type === "content_block_start") logServerToolBlock(event.content_block);
7514
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;
7515
7745
  await stream.writeSSE({
7516
7746
  event: rawEvent.event || event.type,
7517
- data: rawEvent.data
7747
+ data: forwardData
7518
7748
  });
7519
7749
  }
7520
7750
  recordAnthropicStreamingResponse(acc, anthropicPayload.model, ctx);
@@ -8032,6 +8262,38 @@ const handleItemId = (parsed, tracker) => {
8032
8262
 
8033
8263
  //#endregion
8034
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
+ }
8035
8297
  const getResponsesRequestOptions = (payload) => {
8036
8298
  return {
8037
8299
  vision: hasVisionInput(payload),
@@ -8182,7 +8444,8 @@ const TERMINAL_EVENTS = new Set([
8182
8444
  "error"
8183
8445
  ]);
8184
8446
  const handleResponses = async (c) => {
8185
- const payload = await c.req.json();
8447
+ let payload = await c.req.json();
8448
+ if (state.normalizeResponsesCallIds) payload = normalizeCallIds(payload);
8186
8449
  consola.debug("Responses request payload:", JSON.stringify(payload));
8187
8450
  const trackingId = c.get("trackingId");
8188
8451
  const startTime = (trackingId ? requestTracker.getRequest(trackingId) : void 0)?.startTime ?? Date.now();
@@ -8461,7 +8724,8 @@ async function runServer(options) {
8461
8724
  state.autoTruncate = options.autoTruncate;
8462
8725
  state.compressToolResults = options.compressToolResults;
8463
8726
  state.redirectAnthropic = options.redirectAnthropic;
8464
- state.rewriteAnthropicTools = options.rewriteAnthropicTools;
8727
+ state.stripServerTools = options.stripServerTools;
8728
+ state.contextEditingMode = options.contextEditing;
8465
8729
  state.timezoneOffset = options.timezoneOffset;
8466
8730
  if (options.rateLimit) initAdaptiveRateLimiter({
8467
8731
  baseRetryIntervalSeconds: options.retryInterval,
@@ -8473,11 +8737,13 @@ async function runServer(options) {
8473
8737
  if (!options.autoTruncate) consola.info("Auto-truncate disabled");
8474
8738
  if (options.compressToolResults) consola.info("Tool result compression enabled");
8475
8739
  if (options.redirectAnthropic) consola.info("Anthropic API redirect enabled (using OpenAI translation)");
8476
- 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}`);
8477
8742
  initHistory(options.history, options.historyLimit);
8478
8743
  if (options.history) {
8479
8744
  const limitText = options.historyLimit === 0 ? "unlimited" : `max ${options.historyLimit}`;
8480
8745
  consola.info(`History recording enabled (${limitText} entries)`);
8746
+ startMemoryPressureMonitor();
8481
8747
  }
8482
8748
  if (options.posthogKey) {
8483
8749
  initPostHog(options.posthogKey);
@@ -8648,10 +8914,15 @@ const start = defineCommand({
8648
8914
  default: false,
8649
8915
  description: "Redirect Anthropic models through OpenAI translation (instead of direct API)"
8650
8916
  },
8651
- "no-rewrite-anthropic-tools": {
8917
+ "strip-server-tools": {
8652
8918
  type: "boolean",
8653
8919
  default: false,
8654
- 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"
8655
8926
  },
8656
8927
  "timezone-offset": {
8657
8928
  type: "string",
@@ -8684,7 +8955,8 @@ const start = defineCommand({
8684
8955
  autoTruncate: !args["no-auto-truncate"],
8685
8956
  compressToolResults: args["compress-tool-results"],
8686
8957
  redirectAnthropic: args["redirect-anthropic"],
8687
- rewriteAnthropicTools: !args["no-rewrite-anthropic-tools"],
8958
+ stripServerTools: args["strip-server-tools"],
8959
+ contextEditing: args["context-editing"],
8688
8960
  timezoneOffset: parseTimezoneOffset(args["timezone-offset"]),
8689
8961
  posthogKey: args["posthog-key"]
8690
8962
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.6.1",
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",