@hsupu/copilot-api 0.7.20 → 0.7.21

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.
@@ -56,7 +56,7 @@ model_overrides:
56
56
  stream_idle_timeout: 300 # Max seconds between SSE events (0 = no timeout).
57
57
  # Applies to all streaming paths (Anthropic, Chat Completions, Responses).
58
58
 
59
- fetch_timeout: 300 # Seconds: request start → HTTP response headers (0 = no timeout).
59
+ fetch_timeout: 600 # Seconds: request start → HTTP response headers (0 = no timeout).
60
60
  # Applies to all upstream API clients.
61
61
 
62
62
  stale_request_max_age: 600 # Max seconds an active request can live before the stale reaper
@@ -69,8 +69,8 @@ stale_request_max_age: 600 # Max seconds an active request can live before t
69
69
  # Control graceful shutdown timing.
70
70
 
71
71
  shutdown:
72
- graceful_wait: 60 # Phase 2: seconds to wait for in-flight requests to complete naturally (default: 60)
73
- abort_wait: 120 # Phase 3: seconds to wait after abort signal for handlers to wrap up (default: 120)
72
+ graceful_wait: 300 # Phase 2: seconds to wait for in-flight requests to complete naturally (default: 60)
73
+ abort_wait: 600 # Phase 3: seconds to wait after abort signal for handlers to wrap up (default: 120)
74
74
 
75
75
  # ============================================================================
76
76
  # History
@@ -87,13 +87,21 @@ history:
87
87
  # Settings for Anthropic API tool handling and timeouts.
88
88
 
89
89
  anthropic:
90
- strip_server_tools: false # Strip server-side tools (web_search, etc.) from requests
91
- dedup_tool_calls: false # false | "input" | "result" (true = "input" for compat)
92
- # "input": dedup by (name, input); "result": also require identical result
93
- strip_read_tool_result_tags: false # Strip <system-reminder> tags from Read tool results
94
- # rewrite_system_reminders: false # false = keep all (default), true = remove all
95
- rewrite_system_reminders: # Or provide rewrite rules (first match wins, top-down).
96
- # Note: `model` field is NOT supported here (only in system_prompt_overrides).
90
+ strip_server_tools: false # Strip server-side tools (web_search, etc.) from requests
91
+ dedup_tool_calls: false # false | "input" | "result" (true = "input" for compat)
92
+ # "input": dedup by (name, input); "result": also require identical result
93
+ strip_read_tool_result_tags: false # Strip <system-reminder> tags from Read tool results
94
+ context_editing: off # off | clear-thinking | clear-tooluse | clear-both
95
+ # Server-side context editing mode. Controls how Anthropic's
96
+ # context_management trims older context when input grows large.
97
+ # - off: disabled (default). No context_management sent.
98
+ # - clear-thinking: clear old thinking blocks.
99
+ # - clear-tooluse: clear old tool_use/result pairs.
100
+ # - clear-both: apply both clear-thinking and clear-tooluse.
101
+ # Only effective for models supporting context editing.
102
+ # rewrite_system_reminders: false # false = keep all (default), true = remove all
103
+ rewrite_system_reminders: # Or provide rewrite rules (first match wins, top-down).
104
+ # Note: `model` field is NOT supported here (only in system_prompt_overrides).
97
105
  - from: "^Whenever you read a file, you should consider whether it would be considered malware"
98
106
  to: "" # Empty = remove the tag
99
107
  # - from: ".*" # Catch-all: keep unchanged (gms flags are automatic)
package/dist/main.mjs CHANGED
@@ -639,6 +639,7 @@ const state = {
639
639
  accountType: "individual",
640
640
  autoTruncate: true,
641
641
  compressToolResultsBeforeTruncate: true,
642
+ contextEditingMode: "off",
642
643
  stripServerTools: false,
643
644
  dedupToolCalls: false,
644
645
  fetchTimeout: 300,
@@ -772,12 +773,15 @@ async function ensurePaths() {
772
773
  await ensureFile(PATHS.GITHUB_TOKEN_PATH);
773
774
  }
774
775
  async function ensureFile(filePath) {
776
+ const isWindows = process.platform === "win32";
775
777
  try {
776
778
  await fs.access(filePath, fs.constants.W_OK);
777
- if (((await fs.stat(filePath)).mode & 511) !== 384) await fs.chmod(filePath, 384);
779
+ if (!isWindows) {
780
+ if (((await fs.stat(filePath)).mode & 511) !== 384) await fs.chmod(filePath, 384);
781
+ }
778
782
  } catch {
779
783
  await fs.writeFile(filePath, "");
780
- await fs.chmod(filePath, 384);
784
+ if (!isWindows) await fs.chmod(filePath, 384);
781
785
  }
782
786
  }
783
787
 
@@ -878,6 +882,7 @@ async function applyConfigToState() {
878
882
  if (a.strip_server_tools !== void 0) state.stripServerTools = a.strip_server_tools;
879
883
  if (a.dedup_tool_calls !== void 0) state.dedupToolCalls = a.dedup_tool_calls === true ? "input" : a.dedup_tool_calls;
880
884
  if (a.strip_read_tool_result_tags !== void 0) state.stripReadToolResultTags = a.strip_read_tool_result_tags;
885
+ if (a.context_editing !== void 0) state.contextEditingMode = a.context_editing;
881
886
  if (a.rewrite_system_reminders !== void 0) {
882
887
  if (typeof a.rewrite_system_reminders === "boolean") state.rewriteSystemReminders = a.rewrite_system_reminders;
883
888
  else if (Array.isArray(a.rewrite_system_reminders)) state.rewriteSystemReminders = compileRewriteRules(a.rewrite_system_reminders);
@@ -4723,7 +4728,7 @@ const setupClaudeCode = defineCommand({
4723
4728
 
4724
4729
  //#endregion
4725
4730
  //#region package.json
4726
- var version = "0.7.20";
4731
+ var version = "0.7.21";
4727
4732
 
4728
4733
  //#endregion
4729
4734
  //#region src/lib/context/error-persistence.ts
@@ -4957,6 +4962,22 @@ function isEndpointSupported(model, endpoint) {
4957
4962
  return model.supported_endpoints.includes(endpoint);
4958
4963
  }
4959
4964
 
4965
+ //#endregion
4966
+ //#region src/lib/ws.ts
4967
+ /** Create a shared WebSocket adapter for the given Hono app */
4968
+ async function createWebSocketAdapter(app) {
4969
+ if (typeof globalThis.Bun !== "undefined") {
4970
+ const { upgradeWebSocket } = await import("hono/bun");
4971
+ return { upgradeWebSocket };
4972
+ }
4973
+ const { createNodeWebSocket } = await import("@hono/node-ws");
4974
+ const nodeWs = createNodeWebSocket({ app });
4975
+ return {
4976
+ upgradeWebSocket: nodeWs.upgradeWebSocket,
4977
+ injectWebSocket: (server) => nodeWs.injectWebSocket(server)
4978
+ };
4979
+ }
4980
+
4960
4981
  //#endregion
4961
4982
  //#region src/routes/history/api.ts
4962
4983
  function handleGetEntries(c) {
@@ -5066,25 +5087,12 @@ historyRoutes.get("/api/sessions/:id", handleGetSession);
5066
5087
  historyRoutes.delete("/api/sessions/:id", handleDeleteSession);
5067
5088
  /**
5068
5089
  * Initialize WebSocket support for history real-time updates.
5069
- * Registers the /ws route on historyRoutes using the appropriate WebSocket
5070
- * adapter for the current runtime (hono/bun for Bun, @hono/node-ws for Node.js).
5090
+ * Registers the /history/ws route on the root app using the shared WebSocket adapter.
5071
5091
  *
5072
- * @param rootApp - The root Hono app instance (needed by @hono/node-ws to match upgrade requests)
5073
- * @returns An `injectWebSocket` function that must be called with the Node.js HTTP server
5074
- * after the server is created. Returns `undefined` under Bun (no injection needed).
5092
+ * @param rootApp - The root Hono app instance
5093
+ * @param upgradeWs - Shared WebSocket upgrade function from createWebSocketAdapter
5075
5094
  */
5076
- async function initHistoryWebSocket(rootApp) {
5077
- let upgradeWs;
5078
- let injectFn;
5079
- if (typeof globalThis.Bun !== "undefined") {
5080
- const { upgradeWebSocket } = await import("hono/bun");
5081
- upgradeWs = upgradeWebSocket;
5082
- } else {
5083
- const { createNodeWebSocket } = await import("@hono/node-ws");
5084
- const nodeWs = createNodeWebSocket({ app: rootApp });
5085
- upgradeWs = nodeWs.upgradeWebSocket;
5086
- injectFn = (server) => nodeWs.injectWebSocket(server);
5087
- }
5095
+ function initHistoryWebSocket(rootApp, upgradeWs) {
5088
5096
  rootApp.get("/history/ws", upgradeWs(() => ({
5089
5097
  onOpen(_event, ws) {
5090
5098
  addClient(ws.raw);
@@ -5098,7 +5106,6 @@ async function initHistoryWebSocket(rootApp) {
5098
5106
  removeClient(ws.raw);
5099
5107
  }
5100
5108
  })));
5101
- return injectFn;
5102
5109
  }
5103
5110
  /**
5104
5111
  * Resolve a UI directory that exists at runtime.
@@ -6095,25 +6102,13 @@ async function handleResponseCreate(ws, payload) {
6095
6102
  * Initialize WebSocket routes for the Responses API.
6096
6103
  *
6097
6104
  * Registers GET /v1/responses and GET /responses on the root Hono app
6098
- * with WebSocket upgrade handling. Follows the same pattern as
6099
- * initHistoryWebSocket in src/routes/history/route.ts.
6105
+ * with WebSocket upgrade handling. Uses the shared WebSocket adapter
6106
+ * to avoid multiple upgrade listeners on the same HTTP server.
6100
6107
  *
6101
- * @returns An inject function for Node.js HTTP server (undefined for Bun)
6108
+ * @param rootApp - The root Hono app instance
6109
+ * @param upgradeWs - Shared WebSocket upgrade function from createWebSocketAdapter
6102
6110
  */
6103
- async function initResponsesWebSocket(rootApp) {
6104
- let upgradeWs;
6105
- let injectFn;
6106
- if (typeof globalThis.Bun !== "undefined") {
6107
- const { upgradeWebSocket } = await import("hono/bun");
6108
- upgradeWs = upgradeWebSocket;
6109
- } else {
6110
- const { createNodeWebSocket } = await import("@hono/node-ws");
6111
- const nodeWs = createNodeWebSocket({ app: rootApp });
6112
- upgradeWs = nodeWs.upgradeWebSocket;
6113
- injectFn = (server) => {
6114
- nodeWs.injectWebSocket(server);
6115
- };
6116
- }
6111
+ function initResponsesWebSocket(rootApp, upgradeWs) {
6117
6112
  const wsHandler = upgradeWs(() => ({
6118
6113
  onOpen(_event, _ws) {
6119
6114
  consola.debug("[WS] Responses API WebSocket connected");
@@ -6147,7 +6142,6 @@ async function initResponsesWebSocket(rootApp) {
6147
6142
  rootApp.get("/v1/responses", wsHandler);
6148
6143
  rootApp.get("/responses", wsHandler);
6149
6144
  consola.debug("[WS] Responses API WebSocket routes registered");
6150
- return injectFn;
6151
6145
  }
6152
6146
 
6153
6147
  //#endregion
@@ -6984,14 +6978,14 @@ function sanitizeOpenAIMessages(payload) {
6984
6978
  content: filtered
6985
6979
  };
6986
6980
  });
6987
- const removedCount = originalCount - messages.length;
6988
- if (removedCount > 0) consola.info(`[Sanitizer:OpenAI] Filtered ${removedCount} orphaned tool messages`);
6981
+ const blocksRemoved = originalCount - messages.length;
6982
+ if (blocksRemoved > 0) consola.info(`[Sanitizer:OpenAI] Filtered ${blocksRemoved} orphaned tool messages`);
6989
6983
  return {
6990
6984
  payload: {
6991
6985
  ...payload,
6992
6986
  messages: allMessages
6993
6987
  },
6994
- blocksRemoved: removedCount,
6988
+ blocksRemoved,
6995
6989
  systemReminderRemovals
6996
6990
  };
6997
6991
  }
@@ -8718,6 +8712,14 @@ function modelSupportsContextEditing(modelId) {
8718
8712
  return normalized.startsWith("claude-haiku-4-5") || normalized.startsWith("claude-sonnet-4-5") || normalized.startsWith("claude-sonnet-4") || normalized.startsWith("claude-opus-4-5") || normalized.startsWith("claude-opus-4-6") || normalized.startsWith("claude-opus-4-1") || normalized.startsWith("claude-opus-4");
8719
8713
  }
8720
8714
  /**
8715
+ * Check if context editing is enabled for a model.
8716
+ * Requires both model support AND config mode != 'off'.
8717
+ * Mirrors VSCode Copilot Chat's isAnthropicContextEditingEnabled().
8718
+ */
8719
+ function isContextEditingEnabled(modelId) {
8720
+ return modelSupportsContextEditing(modelId) && state.contextEditingMode !== "off";
8721
+ }
8722
+ /**
8721
8723
  * Tool search is supported by:
8722
8724
  * - Claude Opus 4.5/4.6
8723
8725
  */
@@ -8756,7 +8758,7 @@ function buildAnthropicBetaHeaders(modelId, resolvedModel) {
8756
8758
  const headers = {};
8757
8759
  const betaFeatures = [];
8758
8760
  if (!modelHasAdaptiveThinking(resolvedModel)) betaFeatures.push("interleaved-thinking-2025-05-14");
8759
- if (modelSupportsContextEditing(modelId)) betaFeatures.push("context-management-2025-06-27");
8761
+ if (isContextEditingEnabled(modelId)) betaFeatures.push("context-management-2025-06-27");
8760
8762
  if (modelSupportsToolSearch(modelId)) betaFeatures.push("advanced-tool-use-2025-11-20");
8761
8763
  if (betaFeatures.length > 0) headers["anthropic-beta"] = betaFeatures.join(",");
8762
8764
  return headers;
@@ -8767,22 +8769,28 @@ function buildAnthropicBetaHeaders(modelId, resolvedModel) {
8767
8769
  * From anthropic.ts:270-329 (buildContextManagement + getContextManagementFromConfig):
8768
8770
  * - clear_thinking: keep last N thinking turns
8769
8771
  * - clear_tool_uses: triggered by input_tokens threshold, keep last N tool uses
8772
+ *
8773
+ * Only builds edits matching the requested mode:
8774
+ * - "off" → undefined (no context management)
8775
+ * - "clear-thinking" → clear_thinking only (if thinking is enabled)
8776
+ * - "clear-tooluse" → clear_tool_uses only
8777
+ * - "clear-both" → both edits
8770
8778
  */
8771
- function buildContextManagement(modelId, hasThinking) {
8772
- if (!modelSupportsContextEditing(modelId)) return;
8779
+ function buildContextManagement(mode, hasThinking) {
8780
+ if (mode === "off") return;
8773
8781
  const triggerType = "input_tokens";
8774
8782
  const triggerValue = 1e5;
8775
8783
  const keepCount = 3;
8776
8784
  const thinkingKeepTurns = 1;
8777
8785
  const edits = [];
8778
- if (hasThinking) edits.push({
8786
+ if ((mode === "clear-thinking" || mode === "clear-both") && hasThinking) edits.push({
8779
8787
  type: "clear_thinking_20251015",
8780
8788
  keep: {
8781
8789
  type: "thinking_turns",
8782
8790
  value: Math.max(1, thinkingKeepTurns)
8783
8791
  }
8784
8792
  });
8785
- edits.push({
8793
+ if (mode === "clear-tooluse" || mode === "clear-both") edits.push({
8786
8794
  type: "clear_tool_uses_20250919",
8787
8795
  trigger: {
8788
8796
  type: triggerType,
@@ -8793,7 +8801,7 @@ function buildContextManagement(modelId, hasThinking) {
8793
8801
  value: keepCount
8794
8802
  }
8795
8803
  });
8796
- return { edits };
8804
+ return edits.length > 0 ? { edits } : void 0;
8797
8805
  }
8798
8806
 
8799
8807
  //#endregion
@@ -9108,8 +9116,9 @@ async function createAnthropicMessages(payload, opts) {
9108
9116
  "anthropic-version": "2023-06-01",
9109
9117
  ...buildAnthropicBetaHeaders(model, opts?.resolvedModel)
9110
9118
  };
9111
- if (!wire.context_management) {
9112
- const contextManagement = buildContextManagement(model, Boolean(thinking && thinking.type !== "disabled"));
9119
+ if (!wire.context_management && isContextEditingEnabled(model)) {
9120
+ const hasThinking = Boolean(thinking && thinking.type !== "disabled");
9121
+ const contextManagement = buildContextManagement(state.contextEditingMode, hasThinking);
9113
9122
  if (contextManagement) {
9114
9123
  wire.context_management = contextManagement;
9115
9124
  consola.debug("[DirectAnthropic] Added context_management:", JSON.stringify(contextManagement));
@@ -10343,6 +10352,7 @@ async function runServer(options) {
10343
10352
  state.showGitHubToken = options.showGitHubToken;
10344
10353
  state.autoTruncate = options.autoTruncate;
10345
10354
  await ensurePaths();
10355
+ consola.info(`Data directory: ${PATHS.APP_DIR}`);
10346
10356
  const config = await applyConfigToState();
10347
10357
  const proxyUrl = options.proxy ?? config.proxy;
10348
10358
  initProxy({
@@ -10428,8 +10438,9 @@ async function runServer(options) {
10428
10438
  if (runtime?.bun?.server) c.env = { server: runtime.bun.server };
10429
10439
  await next();
10430
10440
  });
10431
- const injectHistoryWs = await initHistoryWebSocket(server);
10432
- const injectResponsesWs = await initResponsesWebSocket(server);
10441
+ const wsAdapter = await createWebSocketAdapter(server);
10442
+ initHistoryWebSocket(server, wsAdapter.upgradeWebSocket);
10443
+ initResponsesWebSocket(server, wsAdapter.upgradeWebSocket);
10433
10444
  consola.box(`Web UI:\n🌐 Usage Viewer: https://ericc-ch.github.io/copilot-api?endpoint=${serverUrl}/usage\n📜 History UI: ${serverUrl}/history`);
10434
10445
  const bunWebSocket = typeof globalThis.Bun !== "undefined" ? (await import("hono/bun")).websocket : void 0;
10435
10446
  let serverInstance;
@@ -10451,13 +10462,9 @@ async function runServer(options) {
10451
10462
  }
10452
10463
  setServerInstance(serverInstance);
10453
10464
  setupShutdownHandlers();
10454
- if (injectHistoryWs || injectResponsesWs) {
10465
+ if (wsAdapter.injectWebSocket) {
10455
10466
  const nodeServer = serverInstance.node?.server;
10456
- if (nodeServer && "on" in nodeServer) {
10457
- const ns = nodeServer;
10458
- injectHistoryWs?.(ns);
10459
- injectResponsesWs?.(ns);
10460
- }
10467
+ if (nodeServer && "on" in nodeServer) wsAdapter.injectWebSocket(nodeServer);
10461
10468
  }
10462
10469
  await waitForShutdown();
10463
10470
  }