@hsupu/copilot-api 0.7.20 → 0.7.22
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 +82 -52
- package/dist/main.mjs +543 -403
- package/dist/main.mjs.map +1 -1
- package/package.json +1 -1
- package/ui/history-v3/dist/assets/index-CaOzq3V0.js +3 -0
- package/ui/history-v3/dist/assets/{index-CMA0Arxs.css → index-Dfh3zN1X.css} +1 -1
- package/ui/history-v3/dist/index.html +2 -2
- package/ui/history-v3/dist/assets/index-DS5mAk0y.js +0 -3
package/dist/main.mjs
CHANGED
|
@@ -21,6 +21,47 @@ import { cors } from "hono/cors";
|
|
|
21
21
|
import { trimTrailingSlash } from "hono/trailing-slash";
|
|
22
22
|
import { streamSSE } from "hono/streaming";
|
|
23
23
|
|
|
24
|
+
//#region src/lib/state.ts
|
|
25
|
+
/**
|
|
26
|
+
* Rebuild model lookup indexes from state.models.
|
|
27
|
+
* Called by cacheModels() in production; call directly in tests after setting state.models.
|
|
28
|
+
*/
|
|
29
|
+
function rebuildModelIndex() {
|
|
30
|
+
const data = state.models?.data ?? [];
|
|
31
|
+
state.modelIndex = new Map(data.map((m) => [m.id, m]));
|
|
32
|
+
state.modelIds = new Set(data.map((m) => m.id));
|
|
33
|
+
}
|
|
34
|
+
const DEFAULT_MODEL_OVERRIDES = {
|
|
35
|
+
opus: "claude-opus-4.6",
|
|
36
|
+
sonnet: "claude-sonnet-4.6",
|
|
37
|
+
haiku: "claude-haiku-4.5"
|
|
38
|
+
};
|
|
39
|
+
const state = {
|
|
40
|
+
accountType: "individual",
|
|
41
|
+
autoTruncate: true,
|
|
42
|
+
compressToolResultsBeforeTruncate: true,
|
|
43
|
+
contextEditingMode: "off",
|
|
44
|
+
stripServerTools: false,
|
|
45
|
+
dedupToolCalls: false,
|
|
46
|
+
fetchTimeout: 300,
|
|
47
|
+
historyLimit: 200,
|
|
48
|
+
historyMinEntries: 50,
|
|
49
|
+
modelIds: /* @__PURE__ */ new Set(),
|
|
50
|
+
modelIndex: /* @__PURE__ */ new Map(),
|
|
51
|
+
modelOverrides: { ...DEFAULT_MODEL_OVERRIDES },
|
|
52
|
+
rewriteSystemReminders: false,
|
|
53
|
+
showGitHubToken: false,
|
|
54
|
+
shutdownAbortWait: 120,
|
|
55
|
+
shutdownGracefulWait: 60,
|
|
56
|
+
staleRequestMaxAge: 600,
|
|
57
|
+
streamIdleTimeout: 300,
|
|
58
|
+
systemPromptOverrides: [],
|
|
59
|
+
stripReadToolResultTags: false,
|
|
60
|
+
normalizeResponsesCallIds: false,
|
|
61
|
+
verbose: false
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
24
65
|
//#region src/lib/utils.ts
|
|
25
66
|
const sleep = (ms) => new Promise((resolve) => {
|
|
26
67
|
setTimeout(resolve, ms);
|
|
@@ -408,6 +449,7 @@ function updateEntry(id, update) {
|
|
|
408
449
|
if (update.pipelineInfo) entry.pipelineInfo = update.pipelineInfo;
|
|
409
450
|
if (update.durationMs !== void 0) entry.durationMs = update.durationMs;
|
|
410
451
|
if (update.sseEvents) entry.sseEvents = update.sseEvents;
|
|
452
|
+
if (update.httpHeaders) entry.httpHeaders = update.httpHeaders;
|
|
411
453
|
if (update.response) {
|
|
412
454
|
const session = historyState.sessions.get(entry.sessionId);
|
|
413
455
|
if (session) {
|
|
@@ -619,45 +661,6 @@ function exportHistory(format = "json") {
|
|
|
619
661
|
return [headers.join(","), ...rows.map((r) => r.map((v) => escapeCsvValue(v)).join(","))].join("\n");
|
|
620
662
|
}
|
|
621
663
|
|
|
622
|
-
//#endregion
|
|
623
|
-
//#region src/lib/state.ts
|
|
624
|
-
/**
|
|
625
|
-
* Rebuild model lookup indexes from state.models.
|
|
626
|
-
* Called by cacheModels() in production; call directly in tests after setting state.models.
|
|
627
|
-
*/
|
|
628
|
-
function rebuildModelIndex() {
|
|
629
|
-
const data = state.models?.data ?? [];
|
|
630
|
-
state.modelIndex = new Map(data.map((m) => [m.id, m]));
|
|
631
|
-
state.modelIds = new Set(data.map((m) => m.id));
|
|
632
|
-
}
|
|
633
|
-
const DEFAULT_MODEL_OVERRIDES = {
|
|
634
|
-
opus: "claude-opus-4.6",
|
|
635
|
-
sonnet: "claude-sonnet-4.6",
|
|
636
|
-
haiku: "claude-haiku-4.5"
|
|
637
|
-
};
|
|
638
|
-
const state = {
|
|
639
|
-
accountType: "individual",
|
|
640
|
-
autoTruncate: true,
|
|
641
|
-
compressToolResultsBeforeTruncate: true,
|
|
642
|
-
stripServerTools: false,
|
|
643
|
-
dedupToolCalls: false,
|
|
644
|
-
fetchTimeout: 300,
|
|
645
|
-
historyLimit: 200,
|
|
646
|
-
historyMinEntries: 50,
|
|
647
|
-
modelIds: /* @__PURE__ */ new Set(),
|
|
648
|
-
modelIndex: /* @__PURE__ */ new Map(),
|
|
649
|
-
modelOverrides: { ...DEFAULT_MODEL_OVERRIDES },
|
|
650
|
-
rewriteSystemReminders: false,
|
|
651
|
-
showGitHubToken: false,
|
|
652
|
-
shutdownAbortWait: 120,
|
|
653
|
-
shutdownGracefulWait: 60,
|
|
654
|
-
staleRequestMaxAge: 600,
|
|
655
|
-
streamIdleTimeout: 300,
|
|
656
|
-
systemPromptOverrides: [],
|
|
657
|
-
stripReadToolResultTags: false,
|
|
658
|
-
verbose: false
|
|
659
|
-
};
|
|
660
|
-
|
|
661
664
|
//#endregion
|
|
662
665
|
//#region src/lib/history/memory-pressure.ts
|
|
663
666
|
/**
|
|
@@ -746,7 +749,7 @@ function startMemoryPressureMonitor() {
|
|
|
746
749
|
consola.error("[memory] Error in memory pressure check:", error);
|
|
747
750
|
});
|
|
748
751
|
}, CHECK_INTERVAL_MS);
|
|
749
|
-
if (
|
|
752
|
+
if ("unref" in timer) timer.unref();
|
|
750
753
|
}
|
|
751
754
|
/** Stop the memory pressure monitor */
|
|
752
755
|
function stopMemoryPressureMonitor() {
|
|
@@ -772,12 +775,15 @@ async function ensurePaths() {
|
|
|
772
775
|
await ensureFile(PATHS.GITHUB_TOKEN_PATH);
|
|
773
776
|
}
|
|
774
777
|
async function ensureFile(filePath) {
|
|
778
|
+
const isWindows = process.platform === "win32";
|
|
775
779
|
try {
|
|
776
780
|
await fs.access(filePath, fs.constants.W_OK);
|
|
777
|
-
if (
|
|
781
|
+
if (!isWindows) {
|
|
782
|
+
if (((await fs.stat(filePath)).mode & 511) !== 384) await fs.chmod(filePath, 384);
|
|
783
|
+
}
|
|
778
784
|
} catch {
|
|
779
785
|
await fs.writeFile(filePath, "");
|
|
780
|
-
await fs.chmod(filePath, 384);
|
|
786
|
+
if (!isWindows) await fs.chmod(filePath, 384);
|
|
781
787
|
}
|
|
782
788
|
}
|
|
783
789
|
|
|
@@ -878,6 +884,7 @@ async function applyConfigToState() {
|
|
|
878
884
|
if (a.strip_server_tools !== void 0) state.stripServerTools = a.strip_server_tools;
|
|
879
885
|
if (a.dedup_tool_calls !== void 0) state.dedupToolCalls = a.dedup_tool_calls === true ? "input" : a.dedup_tool_calls;
|
|
880
886
|
if (a.strip_read_tool_result_tags !== void 0) state.stripReadToolResultTags = a.strip_read_tool_result_tags;
|
|
887
|
+
if (a.context_editing !== void 0) state.contextEditingMode = a.context_editing;
|
|
881
888
|
if (a.rewrite_system_reminders !== void 0) {
|
|
882
889
|
if (typeof a.rewrite_system_reminders === "boolean") state.rewriteSystemReminders = a.rewrite_system_reminders;
|
|
883
890
|
else if (Array.isArray(a.rewrite_system_reminders)) state.rewriteSystemReminders = compileRewriteRules(a.rewrite_system_reminders);
|
|
@@ -905,6 +912,8 @@ async function applyConfigToState() {
|
|
|
905
912
|
if (config.fetch_timeout !== void 0) state.fetchTimeout = config.fetch_timeout;
|
|
906
913
|
if (config.stream_idle_timeout !== void 0) state.streamIdleTimeout = config.stream_idle_timeout;
|
|
907
914
|
if (config.stale_request_max_age !== void 0) state.staleRequestMaxAge = config.stale_request_max_age;
|
|
915
|
+
const responsesConfig = config["openai-responses"];
|
|
916
|
+
if (responsesConfig && responsesConfig.normalize_call_ids !== void 0) state.normalizeResponsesCallIds = responsesConfig.normalize_call_ids;
|
|
908
917
|
const currentMtime = getConfigMtimeMs();
|
|
909
918
|
if (hasApplied && currentMtime !== lastAppliedMtimeMs) consola.info("[config] Reloaded config.yaml");
|
|
910
919
|
hasApplied = true;
|
|
@@ -1076,97 +1085,6 @@ function initProxyBun(options) {
|
|
|
1076
1085
|
consola.debug(`Proxy configured (Bun env): ${formatProxyDisplay(options.url)}`);
|
|
1077
1086
|
}
|
|
1078
1087
|
|
|
1079
|
-
//#endregion
|
|
1080
|
-
//#region src/lib/copilot-api.ts
|
|
1081
|
-
const standardHeaders = () => ({
|
|
1082
|
-
"content-type": "application/json",
|
|
1083
|
-
accept: "application/json"
|
|
1084
|
-
});
|
|
1085
|
-
const COPILOT_VERSION = "0.38.0";
|
|
1086
|
-
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
|
|
1087
|
-
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
|
|
1088
|
-
/** Copilot Chat API version (for chat/completions requests) */
|
|
1089
|
-
const COPILOT_API_VERSION = "2025-05-01";
|
|
1090
|
-
/** Copilot internal API version (for token & usage endpoints) */
|
|
1091
|
-
const COPILOT_INTERNAL_API_VERSION = "2025-04-01";
|
|
1092
|
-
/** GitHub public API version (for /user, repos, etc.) */
|
|
1093
|
-
const GITHUB_API_VERSION = "2022-11-28";
|
|
1094
|
-
/**
|
|
1095
|
-
* Session-level interaction ID.
|
|
1096
|
-
* Used to correlate all requests within a single server session.
|
|
1097
|
-
* Unlike x-request-id (per-request UUID), this stays constant for the server lifetime.
|
|
1098
|
-
*/
|
|
1099
|
-
const INTERACTION_ID = randomUUID();
|
|
1100
|
-
const copilotBaseUrl = (state) => state.accountType === "individual" ? "https://api.githubcopilot.com" : `https://api.${state.accountType}.githubcopilot.com`;
|
|
1101
|
-
const copilotHeaders = (state, opts) => {
|
|
1102
|
-
const headers = {
|
|
1103
|
-
Authorization: `Bearer ${state.copilotToken}`,
|
|
1104
|
-
"content-type": standardHeaders()["content-type"],
|
|
1105
|
-
"copilot-integration-id": "vscode-chat",
|
|
1106
|
-
"editor-version": `vscode/${state.vsCodeVersion}`,
|
|
1107
|
-
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
|
|
1108
|
-
"user-agent": USER_AGENT,
|
|
1109
|
-
"openai-intent": opts?.intent ?? "conversation-panel",
|
|
1110
|
-
"x-github-api-version": COPILOT_API_VERSION,
|
|
1111
|
-
"x-request-id": randomUUID(),
|
|
1112
|
-
"X-Interaction-Id": INTERACTION_ID,
|
|
1113
|
-
"x-vscode-user-agent-library-version": "electron-fetch"
|
|
1114
|
-
};
|
|
1115
|
-
if (opts?.vision) headers["copilot-vision-request"] = "true";
|
|
1116
|
-
if (opts?.modelRequestHeaders) {
|
|
1117
|
-
const coreKeysLower = new Set(Object.keys(headers).map((k) => k.toLowerCase()));
|
|
1118
|
-
for (const [key, value] of Object.entries(opts.modelRequestHeaders)) if (!coreKeysLower.has(key.toLowerCase())) headers[key] = value;
|
|
1119
|
-
}
|
|
1120
|
-
return headers;
|
|
1121
|
-
};
|
|
1122
|
-
const GITHUB_API_BASE_URL = "https://api.github.com";
|
|
1123
|
-
const githubHeaders = (state) => ({
|
|
1124
|
-
...standardHeaders(),
|
|
1125
|
-
authorization: `token ${state.githubToken}`,
|
|
1126
|
-
"editor-version": `vscode/${state.vsCodeVersion}`,
|
|
1127
|
-
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
|
|
1128
|
-
"user-agent": USER_AGENT,
|
|
1129
|
-
"x-github-api-version": GITHUB_API_VERSION,
|
|
1130
|
-
"x-vscode-user-agent-library-version": "electron-fetch"
|
|
1131
|
-
});
|
|
1132
|
-
const GITHUB_BASE_URL = "https://github.com";
|
|
1133
|
-
const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
1134
|
-
const GITHUB_APP_SCOPES = ["read:user"].join(" ");
|
|
1135
|
-
/** Fallback VSCode version when GitHub API is unavailable */
|
|
1136
|
-
const VSCODE_VERSION_FALLBACK = "1.104.3";
|
|
1137
|
-
/** GitHub API endpoint for latest VSCode release */
|
|
1138
|
-
const VSCODE_RELEASE_URL = "https://api.github.com/repos/microsoft/vscode/releases/latest";
|
|
1139
|
-
/** Fetch the latest VSCode version and cache in global state */
|
|
1140
|
-
async function cacheVSCodeVersion() {
|
|
1141
|
-
const response = await getVSCodeVersion();
|
|
1142
|
-
state.vsCodeVersion = response;
|
|
1143
|
-
consola.info(`Using VSCode version: ${response}`);
|
|
1144
|
-
}
|
|
1145
|
-
/** Fetch the latest VSCode version from GitHub releases, falling back to a hardcoded version */
|
|
1146
|
-
async function getVSCodeVersion() {
|
|
1147
|
-
const controller = new AbortController();
|
|
1148
|
-
const timeout = setTimeout(() => {
|
|
1149
|
-
controller.abort();
|
|
1150
|
-
}, 5e3);
|
|
1151
|
-
try {
|
|
1152
|
-
const response = await fetch(VSCODE_RELEASE_URL, {
|
|
1153
|
-
signal: controller.signal,
|
|
1154
|
-
headers: {
|
|
1155
|
-
Accept: "application/vnd.github.v3+json",
|
|
1156
|
-
"User-Agent": "copilot-api"
|
|
1157
|
-
}
|
|
1158
|
-
});
|
|
1159
|
-
if (!response.ok) return VSCODE_VERSION_FALLBACK;
|
|
1160
|
-
const version = (await response.json()).tag_name;
|
|
1161
|
-
if (version && /^\d+\.\d+\.\d+$/.test(version)) return version;
|
|
1162
|
-
return VSCODE_VERSION_FALLBACK;
|
|
1163
|
-
} catch {
|
|
1164
|
-
return VSCODE_VERSION_FALLBACK;
|
|
1165
|
-
} finally {
|
|
1166
|
-
clearTimeout(timeout);
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
1088
|
//#endregion
|
|
1171
1089
|
//#region src/lib/sanitize-system-reminder.ts
|
|
1172
1090
|
/**
|
|
@@ -2009,63 +1927,162 @@ function getErrorMessage(error, fallback = "Unknown error") {
|
|
|
2009
1927
|
}
|
|
2010
1928
|
|
|
2011
1929
|
//#endregion
|
|
2012
|
-
//#region src/lib/
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
} });
|
|
2027
|
-
if (!response.ok) throw await HTTPError.fromResponse("Failed to get Copilot usage", response);
|
|
2028
|
-
return await response.json();
|
|
2029
|
-
};
|
|
2030
|
-
|
|
2031
|
-
//#endregion
|
|
2032
|
-
//#region src/lib/token/copilot-token-manager.ts
|
|
1930
|
+
//#region src/lib/copilot-api.ts
|
|
1931
|
+
const standardHeaders = () => ({
|
|
1932
|
+
"content-type": "application/json",
|
|
1933
|
+
accept: "application/json"
|
|
1934
|
+
});
|
|
1935
|
+
const COPILOT_VERSION = "0.38.0";
|
|
1936
|
+
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
|
|
1937
|
+
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
|
|
1938
|
+
/** Copilot Chat API version (for chat/completions requests) */
|
|
1939
|
+
const COPILOT_API_VERSION = "2025-05-01";
|
|
1940
|
+
/** Copilot internal API version (for token & usage endpoints) */
|
|
1941
|
+
const COPILOT_INTERNAL_API_VERSION = "2025-04-01";
|
|
1942
|
+
/** GitHub public API version (for /user, repos, etc.) */
|
|
1943
|
+
const GITHUB_API_VERSION = "2022-11-28";
|
|
2033
1944
|
/**
|
|
2034
|
-
*
|
|
2035
|
-
*
|
|
2036
|
-
*
|
|
2037
|
-
* All refresh paths (scheduled + on-demand via 401) go through `refresh()`,
|
|
2038
|
-
* which deduplicates concurrent callers and reschedules the next refresh based
|
|
2039
|
-
* on the server's `refresh_in` value.
|
|
1945
|
+
* Session-level interaction ID.
|
|
1946
|
+
* Used to correlate all requests within a single server session.
|
|
1947
|
+
* Unlike x-request-id (per-request UUID), this stays constant for the server lifetime.
|
|
2040
1948
|
*/
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
1949
|
+
const INTERACTION_ID = randomUUID();
|
|
1950
|
+
const copilotBaseUrl = (state) => state.accountType === "individual" ? "https://api.githubcopilot.com" : `https://api.${state.accountType}.githubcopilot.com`;
|
|
1951
|
+
const copilotHeaders = (state, opts) => {
|
|
1952
|
+
const headers = {
|
|
1953
|
+
Authorization: `Bearer ${state.copilotToken}`,
|
|
1954
|
+
"content-type": standardHeaders()["content-type"],
|
|
1955
|
+
"copilot-integration-id": "vscode-chat",
|
|
1956
|
+
"editor-version": `vscode/${state.vsCodeVersion}`,
|
|
1957
|
+
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
|
|
1958
|
+
"user-agent": USER_AGENT,
|
|
1959
|
+
"openai-intent": opts?.intent ?? "conversation-panel",
|
|
1960
|
+
"x-github-api-version": COPILOT_API_VERSION,
|
|
1961
|
+
"x-request-id": randomUUID(),
|
|
1962
|
+
"X-Interaction-Id": INTERACTION_ID,
|
|
1963
|
+
"x-vscode-user-agent-library-version": "electron-fetch"
|
|
1964
|
+
};
|
|
1965
|
+
if (opts?.vision) headers["copilot-vision-request"] = "true";
|
|
1966
|
+
if (opts?.modelRequestHeaders) {
|
|
1967
|
+
const coreKeysLower = new Set(Object.keys(headers).map((k) => k.toLowerCase()));
|
|
1968
|
+
for (const [key, value] of Object.entries(opts.modelRequestHeaders)) if (!coreKeysLower.has(key.toLowerCase())) headers[key] = value;
|
|
1969
|
+
}
|
|
1970
|
+
return headers;
|
|
1971
|
+
};
|
|
1972
|
+
const GITHUB_API_BASE_URL = "https://api.github.com";
|
|
1973
|
+
const githubHeaders = (state) => ({
|
|
1974
|
+
...standardHeaders(),
|
|
1975
|
+
authorization: `token ${state.githubToken}`,
|
|
1976
|
+
"editor-version": `vscode/${state.vsCodeVersion}`,
|
|
1977
|
+
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
|
|
1978
|
+
"user-agent": USER_AGENT,
|
|
1979
|
+
"x-github-api-version": GITHUB_API_VERSION,
|
|
1980
|
+
"x-vscode-user-agent-library-version": "electron-fetch"
|
|
1981
|
+
});
|
|
1982
|
+
const GITHUB_BASE_URL = "https://github.com";
|
|
1983
|
+
const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
|
|
1984
|
+
const GITHUB_APP_SCOPES = ["read:user"].join(" ");
|
|
1985
|
+
/** Fallback VSCode version when GitHub API is unavailable */
|
|
1986
|
+
const VSCODE_VERSION_FALLBACK = "1.104.3";
|
|
1987
|
+
/** GitHub API endpoint for latest VSCode release */
|
|
1988
|
+
const VSCODE_RELEASE_URL = "https://api.github.com/repos/microsoft/vscode/releases/latest";
|
|
1989
|
+
/** Fetch the latest VSCode version and cache in global state */
|
|
1990
|
+
async function cacheVSCodeVersion() {
|
|
1991
|
+
const response = await getVSCodeVersion();
|
|
1992
|
+
state.vsCodeVersion = response;
|
|
1993
|
+
consola.info(`Using VSCode version: ${response}`);
|
|
1994
|
+
}
|
|
1995
|
+
/** Fetch the latest VSCode version from GitHub releases, falling back to a hardcoded version */
|
|
1996
|
+
async function getVSCodeVersion() {
|
|
1997
|
+
const controller = new AbortController();
|
|
1998
|
+
const timeout = setTimeout(() => {
|
|
1999
|
+
controller.abort();
|
|
2000
|
+
}, 5e3);
|
|
2001
|
+
try {
|
|
2002
|
+
const response = await fetch(VSCODE_RELEASE_URL, {
|
|
2003
|
+
signal: controller.signal,
|
|
2004
|
+
headers: {
|
|
2005
|
+
Accept: "application/vnd.github.v3+json",
|
|
2006
|
+
"User-Agent": "copilot-api"
|
|
2007
|
+
}
|
|
2008
|
+
});
|
|
2009
|
+
if (!response.ok) return VSCODE_VERSION_FALLBACK;
|
|
2010
|
+
const version = (await response.json()).tag_name;
|
|
2011
|
+
if (version && /^\d+\.\d+\.\d+$/.test(version)) return version;
|
|
2012
|
+
return VSCODE_VERSION_FALLBACK;
|
|
2013
|
+
} catch {
|
|
2014
|
+
return VSCODE_VERSION_FALLBACK;
|
|
2015
|
+
} finally {
|
|
2016
|
+
clearTimeout(timeout);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
//#endregion
|
|
2021
|
+
//#region src/lib/token/copilot-client.ts
|
|
2022
|
+
/** Copilot API client — token and usage */
|
|
2023
|
+
const getCopilotToken = async () => {
|
|
2024
|
+
const response = await fetch(`${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, {
|
|
2025
|
+
headers: {
|
|
2026
|
+
...githubHeaders(state),
|
|
2027
|
+
"x-github-api-version": COPILOT_INTERNAL_API_VERSION
|
|
2028
|
+
},
|
|
2029
|
+
signal: AbortSignal.timeout(15e3)
|
|
2030
|
+
});
|
|
2031
|
+
if (!response.ok) throw await HTTPError.fromResponse("Failed to get Copilot token", response);
|
|
2032
|
+
return await response.json();
|
|
2033
|
+
};
|
|
2034
|
+
const getCopilotUsage = async () => {
|
|
2035
|
+
const response = await fetch(`${GITHUB_API_BASE_URL}/copilot_internal/user`, {
|
|
2036
|
+
headers: {
|
|
2037
|
+
...githubHeaders(state),
|
|
2038
|
+
"x-github-api-version": COPILOT_INTERNAL_API_VERSION
|
|
2039
|
+
},
|
|
2040
|
+
signal: AbortSignal.timeout(15e3)
|
|
2041
|
+
});
|
|
2042
|
+
if (!response.ok) throw await HTTPError.fromResponse("Failed to get Copilot usage", response);
|
|
2043
|
+
return await response.json();
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
//#endregion
|
|
2047
|
+
//#region src/lib/token/copilot-token-manager.ts
|
|
2048
|
+
/**
|
|
2049
|
+
* Manages Copilot token lifecycle including automatic refresh.
|
|
2050
|
+
* Depends on GitHubTokenManager for authentication.
|
|
2051
|
+
*
|
|
2052
|
+
* All refresh paths (scheduled + on-demand via 401) go through `refresh()`,
|
|
2053
|
+
* which deduplicates concurrent callers and reschedules the next refresh based
|
|
2054
|
+
* on the server's `refresh_in` value.
|
|
2055
|
+
*/
|
|
2056
|
+
var CopilotTokenManager = class {
|
|
2057
|
+
githubTokenManager;
|
|
2058
|
+
currentToken = null;
|
|
2059
|
+
refreshTimeout = null;
|
|
2060
|
+
minRefreshIntervalMs;
|
|
2061
|
+
maxRetries;
|
|
2062
|
+
/** Shared promise to prevent concurrent refresh attempts */
|
|
2063
|
+
refreshInFlight = null;
|
|
2064
|
+
/** Set when a refresh attempt fails; cleared on next success */
|
|
2065
|
+
_refreshNeeded = false;
|
|
2066
|
+
constructor(options) {
|
|
2067
|
+
this.githubTokenManager = options.githubTokenManager;
|
|
2068
|
+
this.minRefreshIntervalMs = (options.minRefreshIntervalSeconds ?? 60) * 1e3;
|
|
2069
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* Get the current Copilot token info.
|
|
2073
|
+
*/
|
|
2074
|
+
getCurrentToken() {
|
|
2075
|
+
return this.currentToken;
|
|
2076
|
+
}
|
|
2077
|
+
/**
|
|
2078
|
+
* Initialize the Copilot token and start automatic refresh.
|
|
2079
|
+
*/
|
|
2080
|
+
async initialize() {
|
|
2081
|
+
const tokenInfo = await this.fetchCopilotToken();
|
|
2082
|
+
state.copilotToken = tokenInfo.token;
|
|
2083
|
+
consola.debug("GitHub Copilot Token fetched successfully!");
|
|
2084
|
+
this.scheduleRefresh(tokenInfo.refreshIn);
|
|
2085
|
+
return tokenInfo;
|
|
2069
2086
|
}
|
|
2070
2087
|
/**
|
|
2071
2088
|
* Fetch a new Copilot token from the API.
|
|
@@ -2100,10 +2117,12 @@ var CopilotTokenManager = class {
|
|
|
2100
2117
|
}
|
|
2101
2118
|
}
|
|
2102
2119
|
const delay = Math.min(1e3 * 2 ** attempt, 3e4);
|
|
2103
|
-
|
|
2120
|
+
const reason = error instanceof Error ? formatErrorWithCause(error) : String(error);
|
|
2121
|
+
consola.warn(`Token refresh attempt ${attempt + 1}/${this.maxRetries} failed: ${reason}, retrying in ${delay}ms`);
|
|
2104
2122
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2105
2123
|
}
|
|
2106
|
-
|
|
2124
|
+
const reason = lastError instanceof Error ? formatErrorWithCause(lastError) : String(lastError);
|
|
2125
|
+
consola.error(`All token refresh attempts failed: ${reason}`);
|
|
2107
2126
|
return null;
|
|
2108
2127
|
}
|
|
2109
2128
|
/**
|
|
@@ -2166,10 +2185,12 @@ var CopilotTokenManager = class {
|
|
|
2166
2185
|
}
|
|
2167
2186
|
this.refreshInFlight = this.fetchTokenWithRetry().then((tokenInfo) => {
|
|
2168
2187
|
if (tokenInfo) {
|
|
2188
|
+
this._refreshNeeded = false;
|
|
2169
2189
|
state.copilotToken = tokenInfo.token;
|
|
2170
2190
|
this.scheduleRefresh(tokenInfo.refreshIn);
|
|
2171
2191
|
consola.verbose(`[CopilotToken] Token refreshed (next refresh_in=${tokenInfo.refreshIn}s)`);
|
|
2172
2192
|
} else {
|
|
2193
|
+
this._refreshNeeded = true;
|
|
2173
2194
|
consola.error("[CopilotToken] Token refresh failed, keeping existing token");
|
|
2174
2195
|
this.scheduleRefresh(300);
|
|
2175
2196
|
}
|
|
@@ -2180,6 +2201,16 @@ var CopilotTokenManager = class {
|
|
|
2180
2201
|
return this.refreshInFlight;
|
|
2181
2202
|
}
|
|
2182
2203
|
/**
|
|
2204
|
+
* Proactively ensure the token is valid before sending a request.
|
|
2205
|
+
* Triggers a refresh if the token is expired/expiring or the last
|
|
2206
|
+
* refresh attempt failed. Concurrent callers share the same in-flight
|
|
2207
|
+
* refresh via `refresh()`.
|
|
2208
|
+
*/
|
|
2209
|
+
async ensureValidToken() {
|
|
2210
|
+
if (!this.isExpiredOrExpiring() && !this._refreshNeeded) return;
|
|
2211
|
+
await this.refresh();
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2183
2214
|
* Check if the current token is expired or about to expire.
|
|
2184
2215
|
*/
|
|
2185
2216
|
isExpiredOrExpiring(marginSeconds = 60) {
|
|
@@ -2651,6 +2682,14 @@ function getCopilotTokenManager() {
|
|
|
2651
2682
|
function stopTokenRefresh() {
|
|
2652
2683
|
copilotTokenManager?.stopAutoRefresh();
|
|
2653
2684
|
}
|
|
2685
|
+
/**
|
|
2686
|
+
* Proactively ensure the Copilot token is valid.
|
|
2687
|
+
* Triggers a refresh if the token is expired/expiring or the last
|
|
2688
|
+
* background refresh failed. No-op if the manager is not initialized.
|
|
2689
|
+
*/
|
|
2690
|
+
async function ensureValidCopilotToken() {
|
|
2691
|
+
await copilotTokenManager?.ensureValidToken();
|
|
2692
|
+
}
|
|
2654
2693
|
|
|
2655
2694
|
//#endregion
|
|
2656
2695
|
//#region src/auth.ts
|
|
@@ -2760,6 +2799,15 @@ const checkUsage = defineCommand({
|
|
|
2760
2799
|
function createFetchSignal() {
|
|
2761
2800
|
return state.fetchTimeout > 0 ? AbortSignal.timeout(state.fetchTimeout * 1e3) : void 0;
|
|
2762
2801
|
}
|
|
2802
|
+
/**
|
|
2803
|
+
* Populate a HeadersCapture object with request and response headers.
|
|
2804
|
+
* Should be called immediately after fetch(), before !response.ok check,
|
|
2805
|
+
* so headers are captured even for error responses.
|
|
2806
|
+
*/
|
|
2807
|
+
function captureHttpHeaders(capture, requestHeaders, response) {
|
|
2808
|
+
capture.request = { ...requestHeaders };
|
|
2809
|
+
capture.response = Object.fromEntries(response.headers.entries());
|
|
2810
|
+
}
|
|
2763
2811
|
|
|
2764
2812
|
//#endregion
|
|
2765
2813
|
//#region src/lib/models/client.ts
|
|
@@ -3444,6 +3492,7 @@ function createRequestContext(opts) {
|
|
|
3444
3492
|
let _pipelineInfo = null;
|
|
3445
3493
|
let _preprocessInfo = null;
|
|
3446
3494
|
let _sseEvents = null;
|
|
3495
|
+
let _httpHeaders = null;
|
|
3447
3496
|
const _sanitizationHistory = [];
|
|
3448
3497
|
let _queueWaitMs = 0;
|
|
3449
3498
|
const _attempts = [];
|
|
@@ -3480,6 +3529,9 @@ function createRequestContext(opts) {
|
|
|
3480
3529
|
get preprocessInfo() {
|
|
3481
3530
|
return _preprocessInfo;
|
|
3482
3531
|
},
|
|
3532
|
+
get httpHeaders() {
|
|
3533
|
+
return _httpHeaders;
|
|
3534
|
+
},
|
|
3483
3535
|
get attempts() {
|
|
3484
3536
|
return _attempts;
|
|
3485
3537
|
},
|
|
@@ -3518,6 +3570,12 @@ function createRequestContext(opts) {
|
|
|
3518
3570
|
setSseEvents(events) {
|
|
3519
3571
|
_sseEvents = events.length > 0 ? events : null;
|
|
3520
3572
|
},
|
|
3573
|
+
setHttpHeaders(capture) {
|
|
3574
|
+
if (capture.request && capture.response) _httpHeaders = {
|
|
3575
|
+
request: capture.request,
|
|
3576
|
+
response: capture.response
|
|
3577
|
+
};
|
|
3578
|
+
},
|
|
3521
3579
|
beginAttempt(attemptOpts) {
|
|
3522
3580
|
const attempt = {
|
|
3523
3581
|
index: _attempts.length,
|
|
@@ -3648,6 +3706,7 @@ function createRequestContext(opts) {
|
|
|
3648
3706
|
if (lastTruncation) entry.truncation = lastTruncation;
|
|
3649
3707
|
if (_pipelineInfo) entry.pipelineInfo = _pipelineInfo;
|
|
3650
3708
|
if (_sseEvents) entry.sseEvents = _sseEvents;
|
|
3709
|
+
if (_httpHeaders) entry.httpHeaders = _httpHeaders;
|
|
3651
3710
|
if (_attempts.length > 1) entry.attempts = _attempts.map((a) => ({
|
|
3652
3711
|
index: a.index,
|
|
3653
3712
|
strategy: a.strategy,
|
|
@@ -4723,7 +4782,7 @@ const setupClaudeCode = defineCommand({
|
|
|
4723
4782
|
|
|
4724
4783
|
//#endregion
|
|
4725
4784
|
//#region package.json
|
|
4726
|
-
var version = "0.7.
|
|
4785
|
+
var version = "0.7.22";
|
|
4727
4786
|
|
|
4728
4787
|
//#endregion
|
|
4729
4788
|
//#region src/lib/context/error-persistence.ts
|
|
@@ -4840,7 +4899,8 @@ function handleHistoryEvent(event) {
|
|
|
4840
4899
|
updateEntry(entryData.id, {
|
|
4841
4900
|
response,
|
|
4842
4901
|
durationMs: entryData.durationMs,
|
|
4843
|
-
sseEvents: entryData.sseEvents
|
|
4902
|
+
sseEvents: entryData.sseEvents,
|
|
4903
|
+
httpHeaders: entryData.httpHeaders
|
|
4844
4904
|
});
|
|
4845
4905
|
break;
|
|
4846
4906
|
}
|
|
@@ -4957,6 +5017,22 @@ function isEndpointSupported(model, endpoint) {
|
|
|
4957
5017
|
return model.supported_endpoints.includes(endpoint);
|
|
4958
5018
|
}
|
|
4959
5019
|
|
|
5020
|
+
//#endregion
|
|
5021
|
+
//#region src/lib/ws.ts
|
|
5022
|
+
/** Create a shared WebSocket adapter for the given Hono app */
|
|
5023
|
+
async function createWebSocketAdapter(app) {
|
|
5024
|
+
if (typeof globalThis.Bun !== "undefined") {
|
|
5025
|
+
const { upgradeWebSocket } = await import("hono/bun");
|
|
5026
|
+
return { upgradeWebSocket };
|
|
5027
|
+
}
|
|
5028
|
+
const { createNodeWebSocket } = await import("@hono/node-ws");
|
|
5029
|
+
const nodeWs = createNodeWebSocket({ app });
|
|
5030
|
+
return {
|
|
5031
|
+
upgradeWebSocket: nodeWs.upgradeWebSocket,
|
|
5032
|
+
injectWebSocket: (server) => nodeWs.injectWebSocket(server)
|
|
5033
|
+
};
|
|
5034
|
+
}
|
|
5035
|
+
|
|
4960
5036
|
//#endregion
|
|
4961
5037
|
//#region src/routes/history/api.ts
|
|
4962
5038
|
function handleGetEntries(c) {
|
|
@@ -5066,25 +5142,12 @@ historyRoutes.get("/api/sessions/:id", handleGetSession);
|
|
|
5066
5142
|
historyRoutes.delete("/api/sessions/:id", handleDeleteSession);
|
|
5067
5143
|
/**
|
|
5068
5144
|
* 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).
|
|
5145
|
+
* Registers the /history/ws route on the root app using the shared WebSocket adapter.
|
|
5071
5146
|
*
|
|
5072
|
-
* @param rootApp - The root Hono app instance
|
|
5073
|
-
* @
|
|
5074
|
-
* after the server is created. Returns `undefined` under Bun (no injection needed).
|
|
5147
|
+
* @param rootApp - The root Hono app instance
|
|
5148
|
+
* @param upgradeWs - Shared WebSocket upgrade function from createWebSocketAdapter
|
|
5075
5149
|
*/
|
|
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
|
-
}
|
|
5150
|
+
function initHistoryWebSocket(rootApp, upgradeWs) {
|
|
5088
5151
|
rootApp.get("/history/ws", upgradeWs(() => ({
|
|
5089
5152
|
onOpen(_event, ws) {
|
|
5090
5153
|
addClient(ws.raw);
|
|
@@ -5098,7 +5161,6 @@ async function initHistoryWebSocket(rootApp) {
|
|
|
5098
5161
|
removeClient(ws.raw);
|
|
5099
5162
|
}
|
|
5100
5163
|
})));
|
|
5101
|
-
return injectFn;
|
|
5102
5164
|
}
|
|
5103
5165
|
/**
|
|
5104
5166
|
* Resolve a UI directory that exists at runtime.
|
|
@@ -5831,6 +5893,7 @@ const createResponses = async (payload, opts) => {
|
|
|
5831
5893
|
body: JSON.stringify(payload),
|
|
5832
5894
|
signal: fetchSignal
|
|
5833
5895
|
});
|
|
5896
|
+
if (opts?.headersCapture) captureHttpHeaders(opts.headersCapture, headers, response);
|
|
5834
5897
|
if (!response.ok) {
|
|
5835
5898
|
consola.error("Failed to create responses", response);
|
|
5836
5899
|
throw await HTTPError.fromResponse("Failed to create responses", response, payload.model);
|
|
@@ -5947,15 +6010,20 @@ function createTokenRefreshStrategy() {
|
|
|
5947
6010
|
* centralizes that configuration to avoid duplication.
|
|
5948
6011
|
*/
|
|
5949
6012
|
/** Create the FormatAdapter for Responses API pipeline execution */
|
|
5950
|
-
function createResponsesAdapter(selectedModel) {
|
|
6013
|
+
function createResponsesAdapter(selectedModel, headersCapture) {
|
|
5951
6014
|
return {
|
|
5952
6015
|
format: "openai-responses",
|
|
5953
|
-
sanitize: (p) =>
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
6016
|
+
sanitize: (p) => {
|
|
6017
|
+
return {
|
|
6018
|
+
payload: state.normalizeResponsesCallIds ? normalizeCallIds(p) : p,
|
|
6019
|
+
blocksRemoved: 0,
|
|
6020
|
+
systemReminderRemovals: 0
|
|
6021
|
+
};
|
|
6022
|
+
},
|
|
6023
|
+
execute: (p) => executeWithAdaptiveRateLimit(() => createResponses(p, {
|
|
6024
|
+
resolvedModel: selectedModel,
|
|
6025
|
+
headersCapture
|
|
6026
|
+
})),
|
|
5959
6027
|
logPayloadSize: (p) => {
|
|
5960
6028
|
const count = typeof p.input === "string" ? 1 : p.input.length;
|
|
5961
6029
|
consola.debug(`Responses payload: ${count} input item(s), model: ${p.model}`);
|
|
@@ -5966,6 +6034,36 @@ function createResponsesAdapter(selectedModel) {
|
|
|
5966
6034
|
function createResponsesStrategies() {
|
|
5967
6035
|
return [createNetworkRetryStrategy(), createTokenRefreshStrategy()];
|
|
5968
6036
|
}
|
|
6037
|
+
const CALL_PREFIX = "call_";
|
|
6038
|
+
const FC_PREFIX = "fc_";
|
|
6039
|
+
/**
|
|
6040
|
+
* Normalize function call IDs in Responses API input.
|
|
6041
|
+
* Converts Chat Completions format `call_xxx` IDs to Responses format `fc_xxx` IDs
|
|
6042
|
+
* on `function_call` and `function_call_output` items.
|
|
6043
|
+
*/
|
|
6044
|
+
function normalizeCallIds(payload) {
|
|
6045
|
+
if (typeof payload.input === "string") return payload;
|
|
6046
|
+
let count = 0;
|
|
6047
|
+
const normalizedInput = payload.input.map((item) => {
|
|
6048
|
+
if (item.type !== "function_call" && item.type !== "function_call_output") return item;
|
|
6049
|
+
const newItem = { ...item };
|
|
6050
|
+
if (newItem.id?.startsWith(CALL_PREFIX)) {
|
|
6051
|
+
newItem.id = FC_PREFIX + newItem.id.slice(5);
|
|
6052
|
+
count++;
|
|
6053
|
+
}
|
|
6054
|
+
if (newItem.call_id?.startsWith(CALL_PREFIX)) {
|
|
6055
|
+
newItem.call_id = FC_PREFIX + newItem.call_id.slice(5);
|
|
6056
|
+
count++;
|
|
6057
|
+
}
|
|
6058
|
+
return newItem;
|
|
6059
|
+
});
|
|
6060
|
+
if (count === 0) return payload;
|
|
6061
|
+
consola.debug(`[responses] Normalized ${count} call ID(s) (call_ → fc_)`);
|
|
6062
|
+
return {
|
|
6063
|
+
...payload,
|
|
6064
|
+
input: normalizedInput
|
|
6065
|
+
};
|
|
6066
|
+
}
|
|
5969
6067
|
|
|
5970
6068
|
//#endregion
|
|
5971
6069
|
//#region src/routes/responses/ws.ts
|
|
@@ -6047,17 +6145,20 @@ async function handleResponseCreate(ws, payload) {
|
|
|
6047
6145
|
model: resolvedModel,
|
|
6048
6146
|
clientModel: requestedModel
|
|
6049
6147
|
});
|
|
6050
|
-
const
|
|
6148
|
+
const headersCapture = {};
|
|
6149
|
+
const adapter = createResponsesAdapter(selectedModel, headersCapture);
|
|
6051
6150
|
const strategies = createResponsesStrategies();
|
|
6052
6151
|
try {
|
|
6053
|
-
const
|
|
6152
|
+
const pipelineResult = await executeRequestPipeline({
|
|
6054
6153
|
adapter,
|
|
6055
6154
|
strategies,
|
|
6056
6155
|
payload,
|
|
6057
6156
|
originalPayload: payload,
|
|
6058
6157
|
model: selectedModel,
|
|
6059
6158
|
maxRetries: 1
|
|
6060
|
-
})
|
|
6159
|
+
});
|
|
6160
|
+
reqCtx.setHttpHeaders(headersCapture);
|
|
6161
|
+
const iterator = pipelineResult.response[Symbol.asyncIterator]();
|
|
6061
6162
|
const acc = createResponsesStreamAccumulator();
|
|
6062
6163
|
const idleTimeoutMs = state.streamIdleTimeout > 0 ? state.streamIdleTimeout * 1e3 : 0;
|
|
6063
6164
|
const shutdownSignal = getShutdownSignal();
|
|
@@ -6085,6 +6186,7 @@ async function handleResponseCreate(ws, payload) {
|
|
|
6085
6186
|
reqCtx.complete(responseData);
|
|
6086
6187
|
ws.close(1e3, "done");
|
|
6087
6188
|
} catch (error) {
|
|
6189
|
+
reqCtx.setHttpHeaders(headersCapture);
|
|
6088
6190
|
reqCtx.fail(resolvedModel, error);
|
|
6089
6191
|
const message = error instanceof Error ? error.message : String(error);
|
|
6090
6192
|
consola.error(`[WS] Responses API error: ${message}`);
|
|
@@ -6095,25 +6197,13 @@ async function handleResponseCreate(ws, payload) {
|
|
|
6095
6197
|
* Initialize WebSocket routes for the Responses API.
|
|
6096
6198
|
*
|
|
6097
6199
|
* Registers GET /v1/responses and GET /responses on the root Hono app
|
|
6098
|
-
* with WebSocket upgrade handling.
|
|
6099
|
-
*
|
|
6200
|
+
* with WebSocket upgrade handling. Uses the shared WebSocket adapter
|
|
6201
|
+
* to avoid multiple upgrade listeners on the same HTTP server.
|
|
6100
6202
|
*
|
|
6101
|
-
* @
|
|
6203
|
+
* @param rootApp - The root Hono app instance
|
|
6204
|
+
* @param upgradeWs - Shared WebSocket upgrade function from createWebSocketAdapter
|
|
6102
6205
|
*/
|
|
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
|
-
}
|
|
6206
|
+
function initResponsesWebSocket(rootApp, upgradeWs) {
|
|
6117
6207
|
const wsHandler = upgradeWs(() => ({
|
|
6118
6208
|
onOpen(_event, _ws) {
|
|
6119
6209
|
consola.debug("[WS] Responses API WebSocket connected");
|
|
@@ -6147,7 +6237,6 @@ async function initResponsesWebSocket(rootApp) {
|
|
|
6147
6237
|
rootApp.get("/v1/responses", wsHandler);
|
|
6148
6238
|
rootApp.get("/responses", wsHandler);
|
|
6149
6239
|
consola.debug("[WS] Responses API WebSocket routes registered");
|
|
6150
|
-
return injectFn;
|
|
6151
6240
|
}
|
|
6152
6241
|
|
|
6153
6242
|
//#endregion
|
|
@@ -6885,6 +6974,7 @@ const createChatCompletions = async (payload, opts) => {
|
|
|
6885
6974
|
body: JSON.stringify(payload),
|
|
6886
6975
|
signal: fetchSignal
|
|
6887
6976
|
});
|
|
6977
|
+
if (opts?.headersCapture) captureHttpHeaders(opts.headersCapture, headers, response);
|
|
6888
6978
|
if (!response.ok) {
|
|
6889
6979
|
consola.error("Failed to create chat completions", response);
|
|
6890
6980
|
throw await HTTPError.fromResponse("Failed to create chat completions", response, payload.model);
|
|
@@ -6984,14 +7074,14 @@ function sanitizeOpenAIMessages(payload) {
|
|
|
6984
7074
|
content: filtered
|
|
6985
7075
|
};
|
|
6986
7076
|
});
|
|
6987
|
-
const
|
|
6988
|
-
if (
|
|
7077
|
+
const blocksRemoved = originalCount - messages.length;
|
|
7078
|
+
if (blocksRemoved > 0) consola.info(`[Sanitizer:OpenAI] Filtered ${blocksRemoved} orphaned tool messages`);
|
|
6989
7079
|
return {
|
|
6990
7080
|
payload: {
|
|
6991
7081
|
...payload,
|
|
6992
7082
|
messages: allMessages
|
|
6993
7083
|
},
|
|
6994
|
-
blocksRemoved
|
|
7084
|
+
blocksRemoved,
|
|
6995
7085
|
systemReminderRemovals
|
|
6996
7086
|
};
|
|
6997
7087
|
}
|
|
@@ -7268,10 +7358,14 @@ async function handleChatCompletion(c) {
|
|
|
7268
7358
|
*/
|
|
7269
7359
|
async function executeRequest(opts) {
|
|
7270
7360
|
const { c, payload, originalPayload, selectedModel, reqCtx } = opts;
|
|
7361
|
+
const headersCapture = {};
|
|
7271
7362
|
const adapter = {
|
|
7272
7363
|
format: "openai-chat-completions",
|
|
7273
7364
|
sanitize: (p) => sanitizeOpenAIMessages(p),
|
|
7274
|
-
execute: (p) => executeWithAdaptiveRateLimit(() => createChatCompletions(p, {
|
|
7365
|
+
execute: (p) => executeWithAdaptiveRateLimit(() => createChatCompletions(p, {
|
|
7366
|
+
resolvedModel: selectedModel,
|
|
7367
|
+
headersCapture
|
|
7368
|
+
})),
|
|
7275
7369
|
logPayloadSize: (p) => logPayloadSizeInfo(p, selectedModel)
|
|
7276
7370
|
};
|
|
7277
7371
|
const strategies = [
|
|
@@ -7286,7 +7380,7 @@ async function executeRequest(opts) {
|
|
|
7286
7380
|
];
|
|
7287
7381
|
let truncateResult;
|
|
7288
7382
|
try {
|
|
7289
|
-
const
|
|
7383
|
+
const result = await executeRequestPipeline({
|
|
7290
7384
|
adapter,
|
|
7291
7385
|
strategies,
|
|
7292
7386
|
payload,
|
|
@@ -7299,7 +7393,9 @@ async function executeRequest(opts) {
|
|
|
7299
7393
|
if (retryTruncateResult) truncateResult = retryTruncateResult;
|
|
7300
7394
|
if (reqCtx.tuiLogId) tuiLogger.updateRequest(reqCtx.tuiLogId, { tags: ["truncated", `retry-${attempt + 1}`] });
|
|
7301
7395
|
}
|
|
7302
|
-
})
|
|
7396
|
+
});
|
|
7397
|
+
reqCtx.setHttpHeaders(headersCapture);
|
|
7398
|
+
const response = result.response;
|
|
7303
7399
|
if (isNonStreaming(response)) return handleNonStreamingResponse(c, response, reqCtx, truncateResult);
|
|
7304
7400
|
consola.debug("Streaming response");
|
|
7305
7401
|
reqCtx.transition("streaming");
|
|
@@ -7316,6 +7412,7 @@ async function executeRequest(opts) {
|
|
|
7316
7412
|
});
|
|
7317
7413
|
});
|
|
7318
7414
|
} catch (error) {
|
|
7415
|
+
reqCtx.setHttpHeaders(headersCapture);
|
|
7319
7416
|
reqCtx.fail(payload.model, error);
|
|
7320
7417
|
throw error;
|
|
7321
7418
|
}
|
|
@@ -8718,6 +8815,14 @@ function modelSupportsContextEditing(modelId) {
|
|
|
8718
8815
|
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
8816
|
}
|
|
8720
8817
|
/**
|
|
8818
|
+
* Check if context editing is enabled for a model.
|
|
8819
|
+
* Requires both model support AND config mode != 'off'.
|
|
8820
|
+
* Mirrors VSCode Copilot Chat's isAnthropicContextEditingEnabled().
|
|
8821
|
+
*/
|
|
8822
|
+
function isContextEditingEnabled(modelId) {
|
|
8823
|
+
return modelSupportsContextEditing(modelId) && state.contextEditingMode !== "off";
|
|
8824
|
+
}
|
|
8825
|
+
/**
|
|
8721
8826
|
* Tool search is supported by:
|
|
8722
8827
|
* - Claude Opus 4.5/4.6
|
|
8723
8828
|
*/
|
|
@@ -8756,7 +8861,7 @@ function buildAnthropicBetaHeaders(modelId, resolvedModel) {
|
|
|
8756
8861
|
const headers = {};
|
|
8757
8862
|
const betaFeatures = [];
|
|
8758
8863
|
if (!modelHasAdaptiveThinking(resolvedModel)) betaFeatures.push("interleaved-thinking-2025-05-14");
|
|
8759
|
-
if (
|
|
8864
|
+
if (isContextEditingEnabled(modelId)) betaFeatures.push("context-management-2025-06-27");
|
|
8760
8865
|
if (modelSupportsToolSearch(modelId)) betaFeatures.push("advanced-tool-use-2025-11-20");
|
|
8761
8866
|
if (betaFeatures.length > 0) headers["anthropic-beta"] = betaFeatures.join(",");
|
|
8762
8867
|
return headers;
|
|
@@ -8767,22 +8872,28 @@ function buildAnthropicBetaHeaders(modelId, resolvedModel) {
|
|
|
8767
8872
|
* From anthropic.ts:270-329 (buildContextManagement + getContextManagementFromConfig):
|
|
8768
8873
|
* - clear_thinking: keep last N thinking turns
|
|
8769
8874
|
* - clear_tool_uses: triggered by input_tokens threshold, keep last N tool uses
|
|
8875
|
+
*
|
|
8876
|
+
* Only builds edits matching the requested mode:
|
|
8877
|
+
* - "off" → undefined (no context management)
|
|
8878
|
+
* - "clear-thinking" → clear_thinking only (if thinking is enabled)
|
|
8879
|
+
* - "clear-tooluse" → clear_tool_uses only
|
|
8880
|
+
* - "clear-both" → both edits
|
|
8770
8881
|
*/
|
|
8771
|
-
function buildContextManagement(
|
|
8772
|
-
if (
|
|
8882
|
+
function buildContextManagement(mode, hasThinking) {
|
|
8883
|
+
if (mode === "off") return;
|
|
8773
8884
|
const triggerType = "input_tokens";
|
|
8774
8885
|
const triggerValue = 1e5;
|
|
8775
8886
|
const keepCount = 3;
|
|
8776
8887
|
const thinkingKeepTurns = 1;
|
|
8777
8888
|
const edits = [];
|
|
8778
|
-
if (hasThinking) edits.push({
|
|
8889
|
+
if ((mode === "clear-thinking" || mode === "clear-both") && hasThinking) edits.push({
|
|
8779
8890
|
type: "clear_thinking_20251015",
|
|
8780
8891
|
keep: {
|
|
8781
8892
|
type: "thinking_turns",
|
|
8782
8893
|
value: Math.max(1, thinkingKeepTurns)
|
|
8783
8894
|
}
|
|
8784
8895
|
});
|
|
8785
|
-
edits.push({
|
|
8896
|
+
if (mode === "clear-tooluse" || mode === "clear-both") edits.push({
|
|
8786
8897
|
type: "clear_tool_uses_20250919",
|
|
8787
8898
|
trigger: {
|
|
8788
8899
|
type: triggerType,
|
|
@@ -8793,7 +8904,7 @@ function buildContextManagement(modelId, hasThinking) {
|
|
|
8793
8904
|
value: keepCount
|
|
8794
8905
|
}
|
|
8795
8906
|
});
|
|
8796
|
-
return { edits };
|
|
8907
|
+
return edits.length > 0 ? { edits } : void 0;
|
|
8797
8908
|
}
|
|
8798
8909
|
|
|
8799
8910
|
//#endregion
|
|
@@ -9108,8 +9219,9 @@ async function createAnthropicMessages(payload, opts) {
|
|
|
9108
9219
|
"anthropic-version": "2023-06-01",
|
|
9109
9220
|
...buildAnthropicBetaHeaders(model, opts?.resolvedModel)
|
|
9110
9221
|
};
|
|
9111
|
-
if (!wire.context_management) {
|
|
9112
|
-
const
|
|
9222
|
+
if (!wire.context_management && isContextEditingEnabled(model)) {
|
|
9223
|
+
const hasThinking = Boolean(thinking && thinking.type !== "disabled");
|
|
9224
|
+
const contextManagement = buildContextManagement(state.contextEditingMode, hasThinking);
|
|
9113
9225
|
if (contextManagement) {
|
|
9114
9226
|
wire.context_management = contextManagement;
|
|
9115
9227
|
consola.debug("[DirectAnthropic] Added context_management:", JSON.stringify(contextManagement));
|
|
@@ -9123,6 +9235,7 @@ async function createAnthropicMessages(payload, opts) {
|
|
|
9123
9235
|
body: JSON.stringify(wire),
|
|
9124
9236
|
signal: fetchSignal
|
|
9125
9237
|
});
|
|
9238
|
+
if (opts?.headersCapture) captureHttpHeaders(opts.headersCapture, headers, response);
|
|
9126
9239
|
if (!response.ok) {
|
|
9127
9240
|
consola.debug("Request failed:", {
|
|
9128
9241
|
model,
|
|
@@ -9138,6 +9251,161 @@ async function createAnthropicMessages(payload, opts) {
|
|
|
9138
9251
|
return await response.json();
|
|
9139
9252
|
}
|
|
9140
9253
|
|
|
9254
|
+
//#endregion
|
|
9255
|
+
//#region src/lib/anthropic/message-mapping.ts
|
|
9256
|
+
/**
|
|
9257
|
+
* Check if two messages likely correspond to the same original message.
|
|
9258
|
+
* Used by buildMessageMapping to handle cases where sanitization removes
|
|
9259
|
+
* content blocks within a message (changing its shape) or removes entire messages.
|
|
9260
|
+
*/
|
|
9261
|
+
function messagesMatch(orig, rewritten) {
|
|
9262
|
+
if (orig.role !== rewritten.role) return false;
|
|
9263
|
+
if (typeof orig.content === "string" && typeof rewritten.content === "string") return rewritten.content.startsWith(orig.content.slice(0, 100)) || orig.content.startsWith(rewritten.content.slice(0, 100));
|
|
9264
|
+
const origBlocks = Array.isArray(orig.content) ? orig.content : [];
|
|
9265
|
+
const rwBlocks = Array.isArray(rewritten.content) ? rewritten.content : [];
|
|
9266
|
+
if (origBlocks.length === 0 || rwBlocks.length === 0) return true;
|
|
9267
|
+
const ob = origBlocks[0];
|
|
9268
|
+
const rb = rwBlocks[0];
|
|
9269
|
+
if (ob.type !== rb.type) return false;
|
|
9270
|
+
if (ob.type === "tool_use" && rb.type === "tool_use") return ob.id === rb.id;
|
|
9271
|
+
if (ob.type === "tool_result" && rb.type === "tool_result") return ob.tool_use_id === rb.tool_use_id;
|
|
9272
|
+
return true;
|
|
9273
|
+
}
|
|
9274
|
+
/**
|
|
9275
|
+
* Build messageMapping (rwIdx → origIdx) for the direct Anthropic path.
|
|
9276
|
+
* Uses a two-pointer approach since rewritten messages maintain the same relative
|
|
9277
|
+
* order as originals (all transformations are deletions, never reorderings).
|
|
9278
|
+
*/
|
|
9279
|
+
function buildMessageMapping(original, rewritten) {
|
|
9280
|
+
const mapping = [];
|
|
9281
|
+
let origIdx = 0;
|
|
9282
|
+
for (const element of rewritten) while (origIdx < original.length) {
|
|
9283
|
+
if (messagesMatch(original[origIdx], element)) {
|
|
9284
|
+
mapping.push(origIdx);
|
|
9285
|
+
origIdx++;
|
|
9286
|
+
break;
|
|
9287
|
+
}
|
|
9288
|
+
origIdx++;
|
|
9289
|
+
}
|
|
9290
|
+
while (mapping.length < rewritten.length) mapping.push(-1);
|
|
9291
|
+
return mapping;
|
|
9292
|
+
}
|
|
9293
|
+
|
|
9294
|
+
//#endregion
|
|
9295
|
+
//#region src/lib/anthropic/server-tool-filter.ts
|
|
9296
|
+
/**
|
|
9297
|
+
* Server tool block filter for Anthropic SSE streams and non-streaming responses.
|
|
9298
|
+
*
|
|
9299
|
+
* Always active — matching vscode-copilot-chat behavior, which intercepts
|
|
9300
|
+
* server_tool_use and *_tool_result blocks unconditionally. These are server-side
|
|
9301
|
+
* artifacts (e.g. tool_search injected by copilot-api, web_search) that clients
|
|
9302
|
+
* don't expect and most SDKs can't validate.
|
|
9303
|
+
*
|
|
9304
|
+
* Also provides logging for server tool blocks (called before filtering,
|
|
9305
|
+
* so information is never lost even when blocks are stripped).
|
|
9306
|
+
*/
|
|
9307
|
+
/** Check if a block type is a server-side tool result (ends with _tool_result, but not plain tool_result) */
|
|
9308
|
+
function isServerToolResultType(type) {
|
|
9309
|
+
return type !== "tool_result" && type.endsWith("_tool_result");
|
|
9310
|
+
}
|
|
9311
|
+
/**
|
|
9312
|
+
* Check if a content block is a server-side tool block.
|
|
9313
|
+
* Matches `server_tool_use` (any name) and all server tool result types
|
|
9314
|
+
* (web_search_tool_result, tool_search_tool_result, code_execution_tool_result, etc.).
|
|
9315
|
+
*/
|
|
9316
|
+
function isServerToolBlock(block) {
|
|
9317
|
+
if (block.type === "server_tool_use") return true;
|
|
9318
|
+
return isServerToolResultType(block.type);
|
|
9319
|
+
}
|
|
9320
|
+
/**
|
|
9321
|
+
* Log a single server tool block (server_tool_use or *_tool_result).
|
|
9322
|
+
* No-op for non-server-tool blocks — safe to call unconditionally.
|
|
9323
|
+
*
|
|
9324
|
+
* Called before filtering, so information is never lost even when blocks are stripped.
|
|
9325
|
+
*/
|
|
9326
|
+
function logServerToolBlock(block) {
|
|
9327
|
+
if (block.type === "server_tool_use") {
|
|
9328
|
+
consola.debug(`[ServerTool] server_tool_use: ${block.name}`);
|
|
9329
|
+
return;
|
|
9330
|
+
}
|
|
9331
|
+
if (!isServerToolResultType(block.type)) return;
|
|
9332
|
+
const content = block.content;
|
|
9333
|
+
if (!content) return;
|
|
9334
|
+
const contentType = content.type;
|
|
9335
|
+
if (contentType === "tool_search_tool_search_result") {
|
|
9336
|
+
const toolNames = content.tool_references?.map((r) => r.tool_name).filter(Boolean) ?? [];
|
|
9337
|
+
consola.debug(`[ServerTool] tool_search result: discovered ${toolNames.length} tools${toolNames.length > 0 ? ` [${toolNames.join(", ")}]` : ""}`);
|
|
9338
|
+
} else if (contentType === "tool_search_tool_result_error") consola.warn(`[ServerTool] tool_search error: ${content.error_code}`);
|
|
9339
|
+
else consola.debug(`[ServerTool] ${block.type}: ${contentType ?? "unknown"}`);
|
|
9340
|
+
}
|
|
9341
|
+
/**
|
|
9342
|
+
* Log all server tool blocks from a non-streaming response content array.
|
|
9343
|
+
* Must be called before filterServerToolBlocksFromResponse() to preserve info.
|
|
9344
|
+
*/
|
|
9345
|
+
function logServerToolBlocks(content) {
|
|
9346
|
+
for (const block of content) logServerToolBlock(block);
|
|
9347
|
+
}
|
|
9348
|
+
/**
|
|
9349
|
+
* Filters server tool blocks from the SSE stream before forwarding to the client.
|
|
9350
|
+
* Handles index remapping so block indices remain dense/sequential after filtering.
|
|
9351
|
+
*
|
|
9352
|
+
* Always active — matching vscode-copilot-chat behavior, which intercepts
|
|
9353
|
+
* server_tool_use and *_tool_result blocks unconditionally. These are server-side
|
|
9354
|
+
* artifacts (e.g. tool_search injected by copilot-api, web_search) that clients
|
|
9355
|
+
* don't expect and most SDKs can't validate.
|
|
9356
|
+
*/
|
|
9357
|
+
function createServerToolBlockFilter() {
|
|
9358
|
+
const filteredIndices = /* @__PURE__ */ new Set();
|
|
9359
|
+
const clientIndexMap = /* @__PURE__ */ new Map();
|
|
9360
|
+
let nextClientIndex = 0;
|
|
9361
|
+
function getClientIndex(apiIndex) {
|
|
9362
|
+
let idx = clientIndexMap.get(apiIndex);
|
|
9363
|
+
if (idx === void 0) {
|
|
9364
|
+
idx = nextClientIndex++;
|
|
9365
|
+
clientIndexMap.set(apiIndex, idx);
|
|
9366
|
+
}
|
|
9367
|
+
return idx;
|
|
9368
|
+
}
|
|
9369
|
+
return { rewriteEvent(parsed, rawData) {
|
|
9370
|
+
if (!parsed) return rawData;
|
|
9371
|
+
if (parsed.type === "content_block_start") {
|
|
9372
|
+
const block = parsed.content_block;
|
|
9373
|
+
if (isServerToolBlock(block)) {
|
|
9374
|
+
filteredIndices.add(parsed.index);
|
|
9375
|
+
return null;
|
|
9376
|
+
}
|
|
9377
|
+
if (filteredIndices.size === 0) {
|
|
9378
|
+
getClientIndex(parsed.index);
|
|
9379
|
+
return rawData;
|
|
9380
|
+
}
|
|
9381
|
+
const clientIndex = getClientIndex(parsed.index);
|
|
9382
|
+
if (clientIndex === parsed.index) return rawData;
|
|
9383
|
+
const obj = JSON.parse(rawData);
|
|
9384
|
+
obj.index = clientIndex;
|
|
9385
|
+
return JSON.stringify(obj);
|
|
9386
|
+
}
|
|
9387
|
+
if (parsed.type === "content_block_delta" || parsed.type === "content_block_stop") {
|
|
9388
|
+
if (filteredIndices.has(parsed.index)) return null;
|
|
9389
|
+
if (filteredIndices.size === 0) return rawData;
|
|
9390
|
+
const clientIndex = getClientIndex(parsed.index);
|
|
9391
|
+
if (clientIndex === parsed.index) return rawData;
|
|
9392
|
+
const obj = JSON.parse(rawData);
|
|
9393
|
+
obj.index = clientIndex;
|
|
9394
|
+
return JSON.stringify(obj);
|
|
9395
|
+
}
|
|
9396
|
+
return rawData;
|
|
9397
|
+
} };
|
|
9398
|
+
}
|
|
9399
|
+
/** Filter server tool blocks from a non-streaming response */
|
|
9400
|
+
function filterServerToolBlocksFromResponse(response) {
|
|
9401
|
+
const filtered = response.content.filter((block) => !isServerToolBlock(block));
|
|
9402
|
+
if (filtered.length === response.content.length) return response;
|
|
9403
|
+
return {
|
|
9404
|
+
...response,
|
|
9405
|
+
content: filtered
|
|
9406
|
+
};
|
|
9407
|
+
}
|
|
9408
|
+
|
|
9141
9409
|
//#endregion
|
|
9142
9410
|
//#region src/lib/anthropic/stream-accumulator.ts
|
|
9143
9411
|
/**
|
|
@@ -9260,10 +9528,6 @@ function handleContentBlockStart(index, block, acc) {
|
|
|
9260
9528
|
}
|
|
9261
9529
|
acc.contentBlocks[index] = newBlock;
|
|
9262
9530
|
}
|
|
9263
|
-
/** Check if a block type is a server-side tool result (ends with _tool_result, but not plain tool_result) */
|
|
9264
|
-
function isServerToolResultType(type) {
|
|
9265
|
-
return type !== "tool_result" && type.endsWith("_tool_result");
|
|
9266
|
-
}
|
|
9267
9531
|
function handleContentBlockDelta(index, delta, acc, copilotAnnotations) {
|
|
9268
9532
|
const block = acc.contentBlocks[index];
|
|
9269
9533
|
if (!block) return;
|
|
@@ -9313,7 +9577,7 @@ function handleMessageDelta(delta, usage, acc) {
|
|
|
9313
9577
|
}
|
|
9314
9578
|
|
|
9315
9579
|
//#endregion
|
|
9316
|
-
//#region src/lib/anthropic/
|
|
9580
|
+
//#region src/lib/anthropic/sse.ts
|
|
9317
9581
|
/**
|
|
9318
9582
|
* Check if a model supports direct Anthropic API.
|
|
9319
9583
|
* Returns a decision with reason so callers can log/display the routing rationale.
|
|
@@ -9374,46 +9638,6 @@ async function* processAnthropicStream(response, acc, clientAbortSignal) {
|
|
|
9374
9638
|
}
|
|
9375
9639
|
}
|
|
9376
9640
|
|
|
9377
|
-
//#endregion
|
|
9378
|
-
//#region src/lib/anthropic/message-mapping.ts
|
|
9379
|
-
/**
|
|
9380
|
-
* Check if two messages likely correspond to the same original message.
|
|
9381
|
-
* Used by buildMessageMapping to handle cases where sanitization removes
|
|
9382
|
-
* content blocks within a message (changing its shape) or removes entire messages.
|
|
9383
|
-
*/
|
|
9384
|
-
function messagesMatch(orig, rewritten) {
|
|
9385
|
-
if (orig.role !== rewritten.role) return false;
|
|
9386
|
-
if (typeof orig.content === "string" && typeof rewritten.content === "string") return rewritten.content.startsWith(orig.content.slice(0, 100)) || orig.content.startsWith(rewritten.content.slice(0, 100));
|
|
9387
|
-
const origBlocks = Array.isArray(orig.content) ? orig.content : [];
|
|
9388
|
-
const rwBlocks = Array.isArray(rewritten.content) ? rewritten.content : [];
|
|
9389
|
-
if (origBlocks.length === 0 || rwBlocks.length === 0) return true;
|
|
9390
|
-
const ob = origBlocks[0];
|
|
9391
|
-
const rb = rwBlocks[0];
|
|
9392
|
-
if (ob.type !== rb.type) return false;
|
|
9393
|
-
if (ob.type === "tool_use" && rb.type === "tool_use") return ob.id === rb.id;
|
|
9394
|
-
if (ob.type === "tool_result" && rb.type === "tool_result") return ob.tool_use_id === rb.tool_use_id;
|
|
9395
|
-
return true;
|
|
9396
|
-
}
|
|
9397
|
-
/**
|
|
9398
|
-
* Build messageMapping (rwIdx → origIdx) for the direct Anthropic path.
|
|
9399
|
-
* Uses a two-pointer approach since rewritten messages maintain the same relative
|
|
9400
|
-
* order as originals (all transformations are deletions, never reorderings).
|
|
9401
|
-
*/
|
|
9402
|
-
function buildMessageMapping(original, rewritten) {
|
|
9403
|
-
const mapping = [];
|
|
9404
|
-
let origIdx = 0;
|
|
9405
|
-
for (const element of rewritten) while (origIdx < original.length) {
|
|
9406
|
-
if (messagesMatch(original[origIdx], element)) {
|
|
9407
|
-
mapping.push(origIdx);
|
|
9408
|
-
origIdx++;
|
|
9409
|
-
break;
|
|
9410
|
-
}
|
|
9411
|
-
origIdx++;
|
|
9412
|
-
}
|
|
9413
|
-
while (mapping.length < rewritten.length) mapping.push(-1);
|
|
9414
|
-
return mapping;
|
|
9415
|
-
}
|
|
9416
|
-
|
|
9417
9641
|
//#endregion
|
|
9418
9642
|
//#region src/lib/repetition-detector.ts
|
|
9419
9643
|
/**
|
|
@@ -9704,10 +9928,14 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, reqCtx) {
|
|
|
9704
9928
|
if (initialSanitized.thinking && initialSanitized.thinking.type !== "disabled") tags.push(`thinking:${initialSanitized.thinking.type}`);
|
|
9705
9929
|
if (tags.length > 0) tuiLogger.updateRequest(reqCtx.tuiLogId, { tags });
|
|
9706
9930
|
}
|
|
9931
|
+
const headersCapture = {};
|
|
9707
9932
|
const adapter = {
|
|
9708
9933
|
format: "anthropic-messages",
|
|
9709
9934
|
sanitize: (p) => sanitizeAnthropicMessages(preprocessTools(p)),
|
|
9710
|
-
execute: (p) => executeWithAdaptiveRateLimit(() => createAnthropicMessages(p, {
|
|
9935
|
+
execute: (p) => executeWithAdaptiveRateLimit(() => createAnthropicMessages(p, {
|
|
9936
|
+
resolvedModel: selectedModel,
|
|
9937
|
+
headersCapture
|
|
9938
|
+
})),
|
|
9711
9939
|
logPayloadSize: (p) => logPayloadSizeInfoAnthropic(p, selectedModel)
|
|
9712
9940
|
};
|
|
9713
9941
|
const strategies = [
|
|
@@ -9755,6 +9983,7 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, reqCtx) {
|
|
|
9755
9983
|
}
|
|
9756
9984
|
}
|
|
9757
9985
|
});
|
|
9986
|
+
reqCtx.setHttpHeaders(headersCapture);
|
|
9758
9987
|
const response = result.response;
|
|
9759
9988
|
const effectivePayload = result.effectivePayload;
|
|
9760
9989
|
if (Symbol.asyncIterator in response) {
|
|
@@ -9774,6 +10003,7 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, reqCtx) {
|
|
|
9774
10003
|
}
|
|
9775
10004
|
return handleDirectAnthropicNonStreamingResponse(c, response, reqCtx, truncateResult);
|
|
9776
10005
|
} catch (error) {
|
|
10006
|
+
reqCtx.setHttpHeaders(headersCapture);
|
|
9777
10007
|
reqCtx.fail(anthropicPayload.model, error);
|
|
9778
10008
|
throw error;
|
|
9779
10009
|
}
|
|
@@ -9789,7 +10019,7 @@ async function handleDirectAnthropicStreamingResponse(opts) {
|
|
|
9789
10019
|
let eventsIn = 0;
|
|
9790
10020
|
let currentBlockType = "";
|
|
9791
10021
|
let firstEventLogged = false;
|
|
9792
|
-
const serverToolFilter =
|
|
10022
|
+
const serverToolFilter = createServerToolBlockFilter();
|
|
9793
10023
|
try {
|
|
9794
10024
|
for await (const { raw: rawEvent, parsed } of processAnthropicStream(response, acc, clientAbortSignal)) {
|
|
9795
10025
|
const dataLen = rawEvent.data?.length ?? 0;
|
|
@@ -9809,8 +10039,7 @@ async function handleDirectAnthropicStreamingResponse(opts) {
|
|
|
9809
10039
|
currentBlockType = parsed.content_block.type;
|
|
9810
10040
|
consola.debug(`[Stream] Block #${parsed.index} start: ${currentBlockType} at +${Date.now() - streamStartMs}ms`);
|
|
9811
10041
|
const block = parsed.content_block;
|
|
9812
|
-
|
|
9813
|
-
else if (block.type !== "tool_result" && block.type.endsWith("_tool_result")) logServerToolResult(block);
|
|
10042
|
+
logServerToolBlock(block);
|
|
9814
10043
|
} else if (parsed?.type === "content_block_stop") {
|
|
9815
10044
|
const offset = Date.now() - streamStartMs;
|
|
9816
10045
|
consola.debug(`[Stream] Block #${parsed.index} stop (${currentBlockType}) at +${offset}ms, cumulative ↓${bytesIn}B ${eventsIn}ev`);
|
|
@@ -9825,7 +10054,7 @@ async function handleDirectAnthropicStreamingResponse(opts) {
|
|
|
9825
10054
|
const delta = parsed.delta;
|
|
9826
10055
|
if (delta.type === "text_delta" && delta.text) checkRepetition(delta.text);
|
|
9827
10056
|
}
|
|
9828
|
-
const forwardData = serverToolFilter
|
|
10057
|
+
const forwardData = serverToolFilter.rewriteEvent(parsed, rawEvent.data ?? "");
|
|
9829
10058
|
if (forwardData === null) continue;
|
|
9830
10059
|
await stream.writeSSE({
|
|
9831
10060
|
data: forwardData,
|
|
@@ -9880,31 +10109,9 @@ function handleDirectAnthropicNonStreamingResponse(c, response, reqCtx, truncate
|
|
|
9880
10109
|
let finalResponse = response;
|
|
9881
10110
|
if (state.verbose && truncateResult?.wasTruncated) finalResponse = prependMarkerToResponse(response, createTruncationMarker$1(truncateResult));
|
|
9882
10111
|
logServerToolBlocks(finalResponse.content);
|
|
9883
|
-
|
|
10112
|
+
finalResponse = filterServerToolBlocksFromResponse(finalResponse);
|
|
9884
10113
|
return c.json(finalResponse);
|
|
9885
10114
|
}
|
|
9886
|
-
/**
|
|
9887
|
-
* Log information extracted from a server tool result block.
|
|
9888
|
-
* Called before filtering, so information is never lost even when blocks are stripped.
|
|
9889
|
-
*/
|
|
9890
|
-
function logServerToolResult(block) {
|
|
9891
|
-
const content = block.content;
|
|
9892
|
-
if (!content) return;
|
|
9893
|
-
const contentType = content.type;
|
|
9894
|
-
if (contentType === "tool_search_tool_search_result") {
|
|
9895
|
-
const toolNames = content.tool_references?.map((r) => r.tool_name).filter(Boolean) ?? [];
|
|
9896
|
-
consola.debug(`[ServerTool] tool_search result: discovered ${toolNames.length} tools${toolNames.length > 0 ? ` [${toolNames.join(", ")}]` : ""}`);
|
|
9897
|
-
} else if (contentType === "tool_search_tool_result_error") consola.warn(`[ServerTool] tool_search error: ${content.error_code}`);
|
|
9898
|
-
else consola.debug(`[ServerTool] ${block.type}: ${contentType ?? "unknown"}`);
|
|
9899
|
-
}
|
|
9900
|
-
/**
|
|
9901
|
-
* Log server tool blocks from a non-streaming response.
|
|
9902
|
-
* Must be called before filterServerToolBlocksFromResponse() to preserve info.
|
|
9903
|
-
*/
|
|
9904
|
-
function logServerToolBlocks(content) {
|
|
9905
|
-
for (const block of content) if (block.type === "server_tool_use") consola.debug(`[ServerTool] server_tool_use: ${block.name}`);
|
|
9906
|
-
else if (block.type !== "tool_result" && block.type.endsWith("_tool_result")) logServerToolResult(block);
|
|
9907
|
-
}
|
|
9908
10115
|
/** Convert SanitizationStats to the format expected by rewrites */
|
|
9909
10116
|
function toSanitizationInfo(stats) {
|
|
9910
10117
|
return {
|
|
@@ -9916,75 +10123,6 @@ function toSanitizationInfo(stats) {
|
|
|
9916
10123
|
systemReminderRemovals: stats.systemReminderRemovals
|
|
9917
10124
|
};
|
|
9918
10125
|
}
|
|
9919
|
-
/**
|
|
9920
|
-
* Check if a content block is a server-side tool block.
|
|
9921
|
-
* Matches `server_tool_use` (any name) and all server tool result types
|
|
9922
|
-
* (web_search_tool_result, tool_search_tool_result, code_execution_tool_result, etc.).
|
|
9923
|
-
*/
|
|
9924
|
-
function isServerToolBlock(block) {
|
|
9925
|
-
if (block.type === "server_tool_use") return true;
|
|
9926
|
-
return block.type !== "tool_result" && block.type.endsWith("_tool_result");
|
|
9927
|
-
}
|
|
9928
|
-
/**
|
|
9929
|
-
* Filters server tool blocks from the SSE stream before forwarding to the client.
|
|
9930
|
-
* Handles index remapping so block indices remain dense/sequential after filtering.
|
|
9931
|
-
*
|
|
9932
|
-
* Only active when stripServerTools is enabled — in that mode, server tools
|
|
9933
|
-
* were stripped from the request, so any server_tool_use blocks in the response
|
|
9934
|
-
* are unexpected artifacts. When disabled (default), server tool blocks are
|
|
9935
|
-
* transparently forwarded per Anthropic protocol.
|
|
9936
|
-
*/
|
|
9937
|
-
function createServerToolBlockFilter() {
|
|
9938
|
-
const filteredIndices = /* @__PURE__ */ new Set();
|
|
9939
|
-
const clientIndexMap = /* @__PURE__ */ new Map();
|
|
9940
|
-
let nextClientIndex = 0;
|
|
9941
|
-
function getClientIndex(apiIndex) {
|
|
9942
|
-
let idx = clientIndexMap.get(apiIndex);
|
|
9943
|
-
if (idx === void 0) {
|
|
9944
|
-
idx = nextClientIndex++;
|
|
9945
|
-
clientIndexMap.set(apiIndex, idx);
|
|
9946
|
-
}
|
|
9947
|
-
return idx;
|
|
9948
|
-
}
|
|
9949
|
-
return { rewriteEvent(parsed, rawData) {
|
|
9950
|
-
if (!parsed) return rawData;
|
|
9951
|
-
if (parsed.type === "content_block_start") {
|
|
9952
|
-
const block = parsed.content_block;
|
|
9953
|
-
if (isServerToolBlock(block)) {
|
|
9954
|
-
filteredIndices.add(parsed.index);
|
|
9955
|
-
return null;
|
|
9956
|
-
}
|
|
9957
|
-
if (filteredIndices.size === 0) {
|
|
9958
|
-
getClientIndex(parsed.index);
|
|
9959
|
-
return rawData;
|
|
9960
|
-
}
|
|
9961
|
-
const clientIndex = getClientIndex(parsed.index);
|
|
9962
|
-
if (clientIndex === parsed.index) return rawData;
|
|
9963
|
-
const obj = JSON.parse(rawData);
|
|
9964
|
-
obj.index = clientIndex;
|
|
9965
|
-
return JSON.stringify(obj);
|
|
9966
|
-
}
|
|
9967
|
-
if (parsed.type === "content_block_delta" || parsed.type === "content_block_stop") {
|
|
9968
|
-
if (filteredIndices.has(parsed.index)) return null;
|
|
9969
|
-
if (filteredIndices.size === 0) return rawData;
|
|
9970
|
-
const clientIndex = getClientIndex(parsed.index);
|
|
9971
|
-
if (clientIndex === parsed.index) return rawData;
|
|
9972
|
-
const obj = JSON.parse(rawData);
|
|
9973
|
-
obj.index = clientIndex;
|
|
9974
|
-
return JSON.stringify(obj);
|
|
9975
|
-
}
|
|
9976
|
-
return rawData;
|
|
9977
|
-
} };
|
|
9978
|
-
}
|
|
9979
|
-
/** Filter server tool blocks from a non-streaming response */
|
|
9980
|
-
function filterServerToolBlocksFromResponse(response) {
|
|
9981
|
-
const filtered = response.content.filter((block) => !isServerToolBlock(block));
|
|
9982
|
-
if (filtered.length === response.content.length) return response;
|
|
9983
|
-
return {
|
|
9984
|
-
...response,
|
|
9985
|
-
content: filtered
|
|
9986
|
-
};
|
|
9987
|
-
}
|
|
9988
10126
|
|
|
9989
10127
|
//#endregion
|
|
9990
10128
|
//#region src/routes/messages/route.ts
|
|
@@ -10093,7 +10231,8 @@ async function handleResponses(c) {
|
|
|
10093
10231
|
async function handleDirectResponses(opts) {
|
|
10094
10232
|
const { c, payload, reqCtx } = opts;
|
|
10095
10233
|
const selectedModel = state.modelIndex.get(payload.model);
|
|
10096
|
-
const
|
|
10234
|
+
const headersCapture = {};
|
|
10235
|
+
const adapter = createResponsesAdapter(selectedModel, headersCapture);
|
|
10097
10236
|
const strategies = createResponsesStrategies();
|
|
10098
10237
|
try {
|
|
10099
10238
|
const pipelineResult = await executeRequestPipeline({
|
|
@@ -10105,6 +10244,7 @@ async function handleDirectResponses(opts) {
|
|
|
10105
10244
|
maxRetries: 1,
|
|
10106
10245
|
requestContext: reqCtx
|
|
10107
10246
|
});
|
|
10247
|
+
reqCtx.setHttpHeaders(headersCapture);
|
|
10108
10248
|
const response = pipelineResult.response;
|
|
10109
10249
|
reqCtx.addQueueWaitMs(pipelineResult.queueWaitMs);
|
|
10110
10250
|
if (!payload.stream) {
|
|
@@ -10177,6 +10317,7 @@ async function handleDirectResponses(opts) {
|
|
|
10177
10317
|
}
|
|
10178
10318
|
});
|
|
10179
10319
|
} catch (error) {
|
|
10320
|
+
reqCtx.setHttpHeaders(headersCapture);
|
|
10180
10321
|
reqCtx.fail(payload.model, error);
|
|
10181
10322
|
throw error;
|
|
10182
10323
|
}
|
|
@@ -10259,6 +10400,7 @@ server.notFound((c) => {
|
|
|
10259
10400
|
});
|
|
10260
10401
|
server.use(async (_c, next) => {
|
|
10261
10402
|
await applyConfigToState();
|
|
10403
|
+
await ensureValidCopilotToken();
|
|
10262
10404
|
await next();
|
|
10263
10405
|
});
|
|
10264
10406
|
server.use(tuiMiddleware());
|
|
@@ -10343,6 +10485,7 @@ async function runServer(options) {
|
|
|
10343
10485
|
state.showGitHubToken = options.showGitHubToken;
|
|
10344
10486
|
state.autoTruncate = options.autoTruncate;
|
|
10345
10487
|
await ensurePaths();
|
|
10488
|
+
consola.info(`Data directory: ${PATHS.APP_DIR}`);
|
|
10346
10489
|
const config = await applyConfigToState();
|
|
10347
10490
|
const proxyUrl = options.proxy ?? config.proxy;
|
|
10348
10491
|
initProxy({
|
|
@@ -10428,8 +10571,9 @@ async function runServer(options) {
|
|
|
10428
10571
|
if (runtime?.bun?.server) c.env = { server: runtime.bun.server };
|
|
10429
10572
|
await next();
|
|
10430
10573
|
});
|
|
10431
|
-
const
|
|
10432
|
-
|
|
10574
|
+
const wsAdapter = await createWebSocketAdapter(server);
|
|
10575
|
+
initHistoryWebSocket(server, wsAdapter.upgradeWebSocket);
|
|
10576
|
+
initResponsesWebSocket(server, wsAdapter.upgradeWebSocket);
|
|
10433
10577
|
consola.box(`Web UI:\n🌐 Usage Viewer: https://ericc-ch.github.io/copilot-api?endpoint=${serverUrl}/usage\n📜 History UI: ${serverUrl}/history`);
|
|
10434
10578
|
const bunWebSocket = typeof globalThis.Bun !== "undefined" ? (await import("hono/bun")).websocket : void 0;
|
|
10435
10579
|
let serverInstance;
|
|
@@ -10451,13 +10595,9 @@ async function runServer(options) {
|
|
|
10451
10595
|
}
|
|
10452
10596
|
setServerInstance(serverInstance);
|
|
10453
10597
|
setupShutdownHandlers();
|
|
10454
|
-
if (
|
|
10598
|
+
if (wsAdapter.injectWebSocket) {
|
|
10455
10599
|
const nodeServer = serverInstance.node?.server;
|
|
10456
|
-
if (nodeServer && "on" in nodeServer)
|
|
10457
|
-
const ns = nodeServer;
|
|
10458
|
-
injectHistoryWs?.(ns);
|
|
10459
|
-
injectResponsesWs?.(ns);
|
|
10460
|
-
}
|
|
10600
|
+
if (nodeServer && "on" in nodeServer) wsAdapter.injectWebSocket(nodeServer);
|
|
10461
10601
|
}
|
|
10462
10602
|
await waitForShutdown();
|
|
10463
10603
|
}
|