@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.
- package/config.example.yaml +18 -10
- package/dist/main.mjs +65 -58
- package/dist/main.mjs.map +1 -1
- package/package.json +1 -1
package/config.example.yaml
CHANGED
|
@@ -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:
|
|
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:
|
|
73
|
-
abort_wait:
|
|
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
|
|
91
|
-
dedup_tool_calls: false
|
|
92
|
-
|
|
93
|
-
strip_read_tool_result_tags: false
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 (
|
|
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.
|
|
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
|
|
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
|
|
5073
|
-
* @
|
|
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
|
-
|
|
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.
|
|
6099
|
-
*
|
|
6105
|
+
* with WebSocket upgrade handling. Uses the shared WebSocket adapter
|
|
6106
|
+
* to avoid multiple upgrade listeners on the same HTTP server.
|
|
6100
6107
|
*
|
|
6101
|
-
* @
|
|
6108
|
+
* @param rootApp - The root Hono app instance
|
|
6109
|
+
* @param upgradeWs - Shared WebSocket upgrade function from createWebSocketAdapter
|
|
6102
6110
|
*/
|
|
6103
|
-
|
|
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
|
|
6988
|
-
if (
|
|
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
|
|
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 (
|
|
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(
|
|
8772
|
-
if (
|
|
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
|
|
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
|
|
10432
|
-
|
|
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 (
|
|
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
|
}
|