github-router 0.2.0 → 0.3.1
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/README.md +7 -5
- package/dist/main.js +333 -477
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -56,7 +56,7 @@ const standardHeaders = () => ({
|
|
|
56
56
|
const COPILOT_VERSION = "0.26.7";
|
|
57
57
|
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
|
|
58
58
|
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
|
|
59
|
-
const API_VERSION = "2025-
|
|
59
|
+
const API_VERSION = "2025-05-01";
|
|
60
60
|
const copilotBaseUrl = (state$1) => state$1.accountType === "individual" ? "https://api.githubcopilot.com" : `https://api.${state$1.accountType}.githubcopilot.com`;
|
|
61
61
|
const copilotHeaders = (state$1, vision = false, integrationId = "vscode-chat") => {
|
|
62
62
|
const headers = {
|
|
@@ -67,6 +67,7 @@ const copilotHeaders = (state$1, vision = false, integrationId = "vscode-chat")
|
|
|
67
67
|
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
|
|
68
68
|
"user-agent": USER_AGENT,
|
|
69
69
|
"openai-intent": "conversation-panel",
|
|
70
|
+
"x-interaction-type": "conversation-panel",
|
|
70
71
|
"x-github-api-version": API_VERSION,
|
|
71
72
|
"x-request-id": randomUUID(),
|
|
72
73
|
"x-vscode-user-agent-library-version": "electron-fetch"
|
|
@@ -105,19 +106,30 @@ async function forwardError(c, error) {
|
|
|
105
106
|
try {
|
|
106
107
|
errorJson = JSON.parse(errorText);
|
|
107
108
|
} catch {
|
|
108
|
-
errorJson =
|
|
109
|
+
errorJson = void 0;
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
+
const message = resolveErrorMessage(errorJson, errorText);
|
|
112
|
+
consola.error("HTTP error:", errorJson ?? errorText);
|
|
111
113
|
return c.json({ error: {
|
|
112
|
-
message
|
|
114
|
+
message,
|
|
113
115
|
type: "error"
|
|
114
116
|
} }, error.response.status);
|
|
115
117
|
}
|
|
116
118
|
return c.json({ error: {
|
|
117
|
-
message: error.message,
|
|
119
|
+
message: error instanceof Error ? error.message : String(error),
|
|
118
120
|
type: "error"
|
|
119
121
|
} }, 500);
|
|
120
122
|
}
|
|
123
|
+
function resolveErrorMessage(errorJson, fallback) {
|
|
124
|
+
if (typeof errorJson !== "object" || errorJson === null) return fallback;
|
|
125
|
+
const errorRecord = errorJson;
|
|
126
|
+
if (errorRecord.message !== void 0) return String(errorRecord.message);
|
|
127
|
+
if (typeof errorRecord.error === "object" && errorRecord.error !== null) {
|
|
128
|
+
const nestedRecord = errorRecord.error;
|
|
129
|
+
if (nestedRecord.message !== void 0) return String(nestedRecord.message);
|
|
130
|
+
}
|
|
131
|
+
return fallback;
|
|
132
|
+
}
|
|
121
133
|
|
|
122
134
|
//#endregion
|
|
123
135
|
//#region src/services/github/get-copilot-token.ts
|
|
@@ -201,7 +213,8 @@ const cacheVSCodeVersion = async () => {
|
|
|
201
213
|
async function pollAccessToken(deviceCode) {
|
|
202
214
|
const sleepDuration = (deviceCode.interval + 1) * 1e3;
|
|
203
215
|
consola.debug(`Polling access token with interval of ${sleepDuration}ms`);
|
|
204
|
-
|
|
216
|
+
const expiresAt = Date.now() + deviceCode.expires_in * 1e3;
|
|
217
|
+
while (Date.now() < expiresAt) {
|
|
205
218
|
const response = await fetch(`${GITHUB_BASE_URL}/login/oauth/access_token`, {
|
|
206
219
|
method: "POST",
|
|
207
220
|
headers: standardHeaders(),
|
|
@@ -212,16 +225,19 @@ async function pollAccessToken(deviceCode) {
|
|
|
212
225
|
})
|
|
213
226
|
});
|
|
214
227
|
if (!response.ok) {
|
|
215
|
-
await sleep(sleepDuration);
|
|
216
228
|
consola.error("Failed to poll access token:", await response.text());
|
|
229
|
+
if (Date.now() >= expiresAt) break;
|
|
230
|
+
await sleep(sleepDuration);
|
|
217
231
|
continue;
|
|
218
232
|
}
|
|
219
233
|
const json = await response.json();
|
|
220
234
|
consola.debug("Polling access token response:", json);
|
|
221
235
|
const { access_token } = json;
|
|
222
236
|
if (access_token) return access_token;
|
|
223
|
-
|
|
237
|
+
if (Date.now() >= expiresAt) break;
|
|
238
|
+
await sleep(sleepDuration);
|
|
224
239
|
}
|
|
240
|
+
throw new Error("Device code expired. Please run auth again.");
|
|
225
241
|
}
|
|
226
242
|
|
|
227
243
|
//#endregion
|
|
@@ -233,7 +249,7 @@ const setupCopilotToken = async () => {
|
|
|
233
249
|
state.copilotToken = token;
|
|
234
250
|
consola.debug("GitHub Copilot Token fetched successfully!");
|
|
235
251
|
if (state.showToken) consola.info("Copilot token:", token);
|
|
236
|
-
const refreshInterval = (refresh_in - 60) * 1e3;
|
|
252
|
+
const refreshInterval = Math.max((refresh_in - 60) * 1e3, 1e3);
|
|
237
253
|
setInterval(async () => {
|
|
238
254
|
consola.debug("Refreshing Copilot token");
|
|
239
255
|
try {
|
|
@@ -485,8 +501,10 @@ function initProxyFromEnv() {
|
|
|
485
501
|
function getShell() {
|
|
486
502
|
const { platform, ppid, env } = process$1;
|
|
487
503
|
if (platform === "win32") {
|
|
504
|
+
if (env.POWERSHELL_DISTRIBUTION_CHANNEL) return "powershell";
|
|
488
505
|
try {
|
|
489
|
-
|
|
506
|
+
const parentProcess = execSync(`wmic process get ParentProcessId,Name | findstr "${ppid}"`, { stdio: "pipe" }).toString();
|
|
507
|
+
if (parentProcess.toLowerCase().includes("powershell.exe") || parentProcess.toLowerCase().includes("pwsh.exe")) return "powershell";
|
|
490
508
|
} catch {
|
|
491
509
|
return "cmd";
|
|
492
510
|
}
|
|
@@ -501,6 +519,12 @@ function getShell() {
|
|
|
501
519
|
return "sh";
|
|
502
520
|
}
|
|
503
521
|
}
|
|
522
|
+
function quotePosixValue(value) {
|
|
523
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
524
|
+
}
|
|
525
|
+
function quotePowerShellValue(value) {
|
|
526
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
527
|
+
}
|
|
504
528
|
/**
|
|
505
529
|
* Generates a copy-pasteable script to set multiple environment variables
|
|
506
530
|
* and run a subsequent command.
|
|
@@ -514,28 +538,28 @@ function generateEnvScript(envVars, commandToRun = "") {
|
|
|
514
538
|
let commandBlock;
|
|
515
539
|
switch (shell) {
|
|
516
540
|
case "powershell":
|
|
517
|
-
commandBlock = filteredEnvVars.map(([key, value]) => `$env:${key} = ${value}`).join("; ");
|
|
541
|
+
commandBlock = filteredEnvVars.map(([key, value]) => `$env:${key} = ${quotePowerShellValue(value)}`).join("; ");
|
|
518
542
|
break;
|
|
519
543
|
case "cmd":
|
|
520
|
-
commandBlock = filteredEnvVars.map(([key, value]) => `set ${key}=${value}`).join(" & ");
|
|
544
|
+
commandBlock = filteredEnvVars.map(([key, value]) => `set "${key}=${value}"`).join(" & ");
|
|
521
545
|
break;
|
|
522
546
|
case "fish":
|
|
523
|
-
commandBlock = filteredEnvVars.map(([key, value]) => `set -gx ${key} ${value}`).join("; ");
|
|
547
|
+
commandBlock = filteredEnvVars.map(([key, value]) => `set -gx ${key} ${quotePosixValue(value)}`).join("; ");
|
|
524
548
|
break;
|
|
525
549
|
default: {
|
|
526
|
-
const assignments = filteredEnvVars.map(([key, value]) => `${key}=${value}`).join(" ");
|
|
550
|
+
const assignments = filteredEnvVars.map(([key, value]) => `${key}=${quotePosixValue(value)}`).join(" ");
|
|
527
551
|
commandBlock = filteredEnvVars.length > 0 ? `export ${assignments}` : "";
|
|
528
552
|
break;
|
|
529
553
|
}
|
|
530
554
|
}
|
|
531
|
-
if (commandBlock && commandToRun) return `${commandBlock}${shell === "cmd" ? " & " : " && "}${commandToRun}`;
|
|
555
|
+
if (commandBlock && commandToRun) return `${commandBlock}${shell === "cmd" ? " & " : shell === "powershell" ? "; " : " && "}${commandToRun}`;
|
|
532
556
|
return commandBlock || commandToRun;
|
|
533
557
|
}
|
|
534
558
|
|
|
535
559
|
//#endregion
|
|
536
560
|
//#region src/lib/approval.ts
|
|
537
561
|
const awaitApproval = async () => {
|
|
538
|
-
if (!await consola.prompt(`Accept incoming request?`, { type: "confirm" })) throw new HTTPError("Request rejected", Response.json({ message: "Request rejected" }, { status: 403 }));
|
|
562
|
+
if (!await consola.prompt(`Accept incoming request?`, { type: "confirm" })) throw new HTTPError("Request rejected by user", Response.json({ message: "Request rejected by user" }, { status: 403 }));
|
|
539
563
|
};
|
|
540
564
|
|
|
541
565
|
//#endregion
|
|
@@ -560,7 +584,7 @@ async function checkRateLimit(state$1) {
|
|
|
560
584
|
const waitTimeMs = waitTimeSeconds * 1e3;
|
|
561
585
|
consola.warn(`Rate limit reached. Waiting ${waitTimeSeconds} seconds before proceeding...`);
|
|
562
586
|
await sleep(waitTimeMs);
|
|
563
|
-
state$1.lastRequestTimestamp = now;
|
|
587
|
+
state$1.lastRequestTimestamp = Date.now();
|
|
564
588
|
consola.info("Rate limit wait completed, proceeding with request");
|
|
565
589
|
}
|
|
566
590
|
|
|
@@ -776,19 +800,100 @@ const createChatCompletions = async (payload) => {
|
|
|
776
800
|
body: JSON.stringify(payload)
|
|
777
801
|
});
|
|
778
802
|
if (!response.ok) {
|
|
779
|
-
|
|
780
|
-
|
|
803
|
+
let errorBody = "";
|
|
804
|
+
try {
|
|
805
|
+
errorBody = await response.text();
|
|
806
|
+
} catch {
|
|
807
|
+
errorBody = "(could not read error body)";
|
|
808
|
+
}
|
|
809
|
+
const claudeModels = state.models?.data.filter((m) => m.id.startsWith("claude")).map((m) => m.id).join(", ") ?? "(models not loaded)";
|
|
810
|
+
consola.error(`Copilot rejected model "${payload.model}": ${response.status} ${errorBody} (available Claude models: ${claudeModels})`);
|
|
811
|
+
throw new HTTPError("Failed to create chat completions", new Response(errorBody, {
|
|
812
|
+
status: response.status,
|
|
813
|
+
statusText: response.statusText,
|
|
814
|
+
headers: response.headers
|
|
815
|
+
}));
|
|
781
816
|
}
|
|
782
817
|
if (payload.stream) return events(response);
|
|
783
818
|
return await response.json();
|
|
784
819
|
};
|
|
785
820
|
|
|
821
|
+
//#endregion
|
|
822
|
+
//#region src/services/copilot/web-search.ts
|
|
823
|
+
const MAX_SEARCHES_PER_SECOND = 3;
|
|
824
|
+
let searchTimestamps = [];
|
|
825
|
+
async function throttleSearch() {
|
|
826
|
+
const now = Date.now();
|
|
827
|
+
searchTimestamps = searchTimestamps.filter((t) => now - t < 1e3);
|
|
828
|
+
if (searchTimestamps.length >= MAX_SEARCHES_PER_SECOND) {
|
|
829
|
+
const waitMs = 1e3 - (now - searchTimestamps[0]);
|
|
830
|
+
if (waitMs > 0) {
|
|
831
|
+
consola.debug(`Web search rate limited, waiting ${waitMs}ms`);
|
|
832
|
+
await sleep(waitMs);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
searchTimestamps.push(Date.now());
|
|
836
|
+
}
|
|
837
|
+
function threadsHeaders() {
|
|
838
|
+
return copilotHeaders(state, false, "copilot-chat");
|
|
839
|
+
}
|
|
840
|
+
async function createThread() {
|
|
841
|
+
const response = await fetch(`${copilotBaseUrl(state)}/github/chat/threads`, {
|
|
842
|
+
method: "POST",
|
|
843
|
+
headers: threadsHeaders(),
|
|
844
|
+
body: JSON.stringify({})
|
|
845
|
+
});
|
|
846
|
+
if (!response.ok) {
|
|
847
|
+
consola.error("Failed to create chat thread", response.status);
|
|
848
|
+
throw new Error(`Failed to create chat thread: ${response.status}`);
|
|
849
|
+
}
|
|
850
|
+
return (await response.json()).thread_id;
|
|
851
|
+
}
|
|
852
|
+
async function sendThreadMessage(threadId, query) {
|
|
853
|
+
const response = await fetch(`${copilotBaseUrl(state)}/github/chat/threads/${threadId}/messages`, {
|
|
854
|
+
method: "POST",
|
|
855
|
+
headers: threadsHeaders(),
|
|
856
|
+
body: JSON.stringify({
|
|
857
|
+
content: query,
|
|
858
|
+
intent: "conversation",
|
|
859
|
+
skills: ["web-search"],
|
|
860
|
+
references: []
|
|
861
|
+
})
|
|
862
|
+
});
|
|
863
|
+
if (!response.ok) {
|
|
864
|
+
consola.error("Failed to send thread message", response.status);
|
|
865
|
+
throw new Error(`Failed to send thread message: ${response.status}`);
|
|
866
|
+
}
|
|
867
|
+
return await response.json();
|
|
868
|
+
}
|
|
869
|
+
async function searchWeb(query) {
|
|
870
|
+
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
871
|
+
await throttleSearch();
|
|
872
|
+
consola.info(`Web search: "${query.slice(0, 80)}"`);
|
|
873
|
+
const response = await sendThreadMessage(await createThread(), query);
|
|
874
|
+
const references = [];
|
|
875
|
+
for (const ref of response.message.references ?? []) if (ref.results) {
|
|
876
|
+
for (const result of ref.results) if (result.url && result.reference_type !== "bing_search") references.push({
|
|
877
|
+
title: result.title,
|
|
878
|
+
url: result.url
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
consola.debug(`Web search returned ${references.length} references`);
|
|
882
|
+
return {
|
|
883
|
+
content: response.message.content,
|
|
884
|
+
references
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
786
888
|
//#endregion
|
|
787
889
|
//#region src/routes/chat-completions/handler.ts
|
|
788
890
|
async function handleCompletion$1(c) {
|
|
789
891
|
await checkRateLimit(state);
|
|
790
892
|
let payload = await c.req.json();
|
|
791
|
-
consola.
|
|
893
|
+
const debugEnabled = consola.level >= 4;
|
|
894
|
+
if (debugEnabled) consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
|
|
895
|
+
if (state.manualApprove) await awaitApproval();
|
|
896
|
+
await injectWebSearchIfNeeded$1(payload);
|
|
792
897
|
const selectedModel = state.models?.data.find((model) => model.id === payload.model);
|
|
793
898
|
try {
|
|
794
899
|
if (selectedModel) {
|
|
@@ -798,28 +903,68 @@ async function handleCompletion$1(c) {
|
|
|
798
903
|
} catch (error) {
|
|
799
904
|
consola.warn("Failed to calculate token count:", error);
|
|
800
905
|
}
|
|
801
|
-
if (state.manualApprove) await awaitApproval();
|
|
802
906
|
if (isNullish(payload.max_tokens)) {
|
|
803
907
|
payload = {
|
|
804
908
|
...payload,
|
|
805
909
|
max_tokens: selectedModel?.capabilities.limits.max_output_tokens
|
|
806
910
|
};
|
|
807
|
-
consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
|
|
911
|
+
if (debugEnabled) consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
|
|
808
912
|
}
|
|
809
913
|
const response = await createChatCompletions(payload);
|
|
810
|
-
if (isNonStreaming$
|
|
811
|
-
consola.debug("Non-streaming response:", JSON.stringify(response));
|
|
914
|
+
if (isNonStreaming$1(response)) {
|
|
915
|
+
if (debugEnabled) consola.debug("Non-streaming response:", JSON.stringify(response));
|
|
812
916
|
return c.json(response);
|
|
813
917
|
}
|
|
814
918
|
consola.debug("Streaming response");
|
|
815
919
|
return streamSSE(c, async (stream) => {
|
|
816
920
|
for await (const chunk of response) {
|
|
817
|
-
consola.debug("Streaming chunk:", JSON.stringify(chunk));
|
|
921
|
+
if (debugEnabled) consola.debug("Streaming chunk:", JSON.stringify(chunk));
|
|
818
922
|
await stream.writeSSE(chunk);
|
|
819
923
|
}
|
|
820
924
|
});
|
|
821
925
|
}
|
|
822
|
-
const isNonStreaming$
|
|
926
|
+
const isNonStreaming$1 = (response) => Object.hasOwn(response, "choices");
|
|
927
|
+
async function injectWebSearchIfNeeded$1(payload) {
|
|
928
|
+
if (!payload.tools?.some((t) => "type" in t && t.type === "web_search" || t.function?.name === "web_search")) return;
|
|
929
|
+
const query = payload.messages.some((msg) => msg.role === "tool") ? void 0 : extractUserQuery$1(payload.messages);
|
|
930
|
+
if (query) try {
|
|
931
|
+
const results = await searchWeb(query);
|
|
932
|
+
const searchContext = [
|
|
933
|
+
"[Web Search Results]",
|
|
934
|
+
results.content,
|
|
935
|
+
"",
|
|
936
|
+
results.references.map((r) => `- [${r.title}](${r.url})`).join("\n"),
|
|
937
|
+
"[End Web Search Results]"
|
|
938
|
+
].join("\n");
|
|
939
|
+
const systemMsg = payload.messages.find((msg) => msg.role === "system");
|
|
940
|
+
if (systemMsg) systemMsg.content = `${searchContext}\n\n${typeof systemMsg.content === "string" ? systemMsg.content : Array.isArray(systemMsg.content) ? systemMsg.content.filter((p) => p.type === "text").map((p) => "text" in p ? p.text : "").join("\n") : ""}`;
|
|
941
|
+
else payload.messages.unshift({
|
|
942
|
+
role: "system",
|
|
943
|
+
content: searchContext
|
|
944
|
+
});
|
|
945
|
+
} catch (error) {
|
|
946
|
+
consola.warn("Web search failed, continuing without results:", error);
|
|
947
|
+
}
|
|
948
|
+
payload.tools = payload.tools?.filter((t) => !("type" in t && t.type === "web_search" || t.function?.name === "web_search"));
|
|
949
|
+
if (payload.tools?.length === 0) payload.tools = void 0;
|
|
950
|
+
if (!payload.tools) payload.tool_choice = void 0;
|
|
951
|
+
else if (payload.tool_choice && typeof payload.tool_choice === "object" && "type" in payload.tool_choice && payload.tool_choice.type === "function") {
|
|
952
|
+
const toolChoiceName = payload.tool_choice.function?.name;
|
|
953
|
+
if (toolChoiceName && !payload.tools.some((tool) => tool.function.name === toolChoiceName)) payload.tool_choice = void 0;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function extractUserQuery$1(messages) {
|
|
957
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
958
|
+
const msg = messages[i];
|
|
959
|
+
if (msg.role === "user") {
|
|
960
|
+
if (typeof msg.content === "string") return msg.content;
|
|
961
|
+
if (Array.isArray(msg.content)) {
|
|
962
|
+
const text = msg.content.find((p) => p.type === "text");
|
|
963
|
+
if (text && "text" in text) return text.text;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
823
968
|
|
|
824
969
|
//#endregion
|
|
825
970
|
//#region src/routes/chat-completions/route.ts
|
|
@@ -858,399 +1003,128 @@ embeddingRoutes.post("/", async (c) => {
|
|
|
858
1003
|
});
|
|
859
1004
|
|
|
860
1005
|
//#endregion
|
|
861
|
-
//#region src/
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1006
|
+
//#region src/services/copilot/create-messages.ts
|
|
1007
|
+
/**
|
|
1008
|
+
* Build headers that match what VS Code Copilot Chat sends to the Copilot API.
|
|
1009
|
+
*
|
|
1010
|
+
* copilotHeaders() provides: Authorization, content-type, copilot-integration-id,
|
|
1011
|
+
* editor-version, editor-plugin-version, user-agent, openai-intent,
|
|
1012
|
+
* x-github-api-version, x-request-id, x-vscode-user-agent-library-version.
|
|
1013
|
+
*
|
|
1014
|
+
* We add the remaining headers VS Code sends for /v1/messages:
|
|
1015
|
+
* - X-Initiator (VS Code sets dynamically; "agent" is safe for CLI use)
|
|
1016
|
+
* - anthropic-version (VS Code's Anthropic SDK sends this)
|
|
1017
|
+
* - X-Interaction-Id (VS Code sends a session-scoped UUID)
|
|
1018
|
+
*
|
|
1019
|
+
* We intentionally omit copilot-vision-request — VS Code only sends it when
|
|
1020
|
+
* images are present, and the native /v1/messages endpoint handles vision
|
|
1021
|
+
* without requiring the header.
|
|
1022
|
+
*/
|
|
1023
|
+
function buildHeaders() {
|
|
875
1024
|
return {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
stream: payload.stream,
|
|
881
|
-
temperature: payload.temperature,
|
|
882
|
-
top_p: payload.top_p,
|
|
883
|
-
user: payload.metadata?.user_id,
|
|
884
|
-
tools: translateAnthropicToolsToOpenAI(payload.tools),
|
|
885
|
-
tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice)
|
|
1025
|
+
...copilotHeaders(state),
|
|
1026
|
+
"X-Initiator": "agent",
|
|
1027
|
+
"anthropic-version": "2023-06-01",
|
|
1028
|
+
"X-Interaction-Id": randomUUID()
|
|
886
1029
|
};
|
|
887
1030
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
const
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
role: "system",
|
|
902
|
-
content: system
|
|
903
|
-
}];
|
|
904
|
-
else return [{
|
|
905
|
-
role: "system",
|
|
906
|
-
content: system.map((block) => block.text).join("\n\n")
|
|
907
|
-
}];
|
|
908
|
-
}
|
|
909
|
-
function handleUserMessage(message) {
|
|
910
|
-
const newMessages = [];
|
|
911
|
-
if (Array.isArray(message.content)) {
|
|
912
|
-
const toolResultBlocks = message.content.filter((block) => block.type === "tool_result");
|
|
913
|
-
const otherBlocks = message.content.filter((block) => block.type !== "tool_result");
|
|
914
|
-
for (const block of toolResultBlocks) newMessages.push({
|
|
915
|
-
role: "tool",
|
|
916
|
-
tool_call_id: block.tool_use_id,
|
|
917
|
-
content: mapContent(block.content)
|
|
918
|
-
});
|
|
919
|
-
if (otherBlocks.length > 0) newMessages.push({
|
|
920
|
-
role: "user",
|
|
921
|
-
content: mapContent(otherBlocks)
|
|
922
|
-
});
|
|
923
|
-
} else newMessages.push({
|
|
924
|
-
role: "user",
|
|
925
|
-
content: mapContent(message.content)
|
|
1031
|
+
/**
|
|
1032
|
+
* Forward an Anthropic Messages API request to Copilot's native /v1/messages endpoint.
|
|
1033
|
+
* Returns the raw Response so callers can handle streaming vs non-streaming.
|
|
1034
|
+
*/
|
|
1035
|
+
async function createMessages(body) {
|
|
1036
|
+
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
1037
|
+
const headers = buildHeaders();
|
|
1038
|
+
const url = `${copilotBaseUrl(state)}/v1/messages`;
|
|
1039
|
+
consola.debug(`Forwarding to ${url}`);
|
|
1040
|
+
const response = await fetch(url, {
|
|
1041
|
+
method: "POST",
|
|
1042
|
+
headers,
|
|
1043
|
+
body
|
|
926
1044
|
});
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
}];
|
|
934
|
-
const toolUseBlocks = message.content.filter((block) => block.type === "tool_use");
|
|
935
|
-
const textBlocks = message.content.filter((block) => block.type === "text");
|
|
936
|
-
const thinkingBlocks = message.content.filter((block) => block.type === "thinking");
|
|
937
|
-
const allTextContent = [...textBlocks.map((b) => b.text), ...thinkingBlocks.map((b) => b.thinking)].join("\n\n");
|
|
938
|
-
return toolUseBlocks.length > 0 ? [{
|
|
939
|
-
role: "assistant",
|
|
940
|
-
content: allTextContent || null,
|
|
941
|
-
tool_calls: toolUseBlocks.map((toolUse) => ({
|
|
942
|
-
id: toolUse.id,
|
|
943
|
-
type: "function",
|
|
944
|
-
function: {
|
|
945
|
-
name: toolUse.name,
|
|
946
|
-
arguments: JSON.stringify(toolUse.input)
|
|
947
|
-
}
|
|
948
|
-
}))
|
|
949
|
-
}] : [{
|
|
950
|
-
role: "assistant",
|
|
951
|
-
content: mapContent(message.content)
|
|
952
|
-
}];
|
|
953
|
-
}
|
|
954
|
-
function mapContent(content) {
|
|
955
|
-
if (typeof content === "string") return content;
|
|
956
|
-
if (!Array.isArray(content)) return null;
|
|
957
|
-
if (!content.some((block) => block.type === "image")) return content.filter((block) => block.type === "text" || block.type === "thinking").map((block) => block.type === "text" ? block.text : block.thinking).join("\n\n");
|
|
958
|
-
const contentParts = [];
|
|
959
|
-
for (const block of content) switch (block.type) {
|
|
960
|
-
case "text":
|
|
961
|
-
contentParts.push({
|
|
962
|
-
type: "text",
|
|
963
|
-
text: block.text
|
|
964
|
-
});
|
|
965
|
-
break;
|
|
966
|
-
case "thinking":
|
|
967
|
-
contentParts.push({
|
|
968
|
-
type: "text",
|
|
969
|
-
text: block.thinking
|
|
970
|
-
});
|
|
971
|
-
break;
|
|
972
|
-
case "image":
|
|
973
|
-
contentParts.push({
|
|
974
|
-
type: "image_url",
|
|
975
|
-
image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
|
|
976
|
-
});
|
|
977
|
-
break;
|
|
978
|
-
}
|
|
979
|
-
return contentParts;
|
|
980
|
-
}
|
|
981
|
-
function translateAnthropicToolsToOpenAI(anthropicTools) {
|
|
982
|
-
if (!anthropicTools) return;
|
|
983
|
-
return anthropicTools.map((tool) => ({
|
|
984
|
-
type: "function",
|
|
985
|
-
function: {
|
|
986
|
-
name: tool.name,
|
|
987
|
-
description: tool.description,
|
|
988
|
-
parameters: tool.input_schema
|
|
1045
|
+
if (!response.ok) {
|
|
1046
|
+
let errorBody = "";
|
|
1047
|
+
try {
|
|
1048
|
+
errorBody = await response.text();
|
|
1049
|
+
} catch {
|
|
1050
|
+
errorBody = "(could not read error body)";
|
|
989
1051
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
case "any": return "required";
|
|
997
|
-
case "tool":
|
|
998
|
-
if (anthropicToolChoice.name) return {
|
|
999
|
-
type: "function",
|
|
1000
|
-
function: { name: anthropicToolChoice.name }
|
|
1001
|
-
};
|
|
1002
|
-
return;
|
|
1003
|
-
case "none": return "none";
|
|
1004
|
-
default: return;
|
|
1052
|
+
consola.error(`Copilot /v1/messages error: ${response.status} ${errorBody}`);
|
|
1053
|
+
throw new HTTPError("Copilot messages request failed", new Response(errorBody, {
|
|
1054
|
+
status: response.status,
|
|
1055
|
+
statusText: response.statusText,
|
|
1056
|
+
headers: response.headers
|
|
1057
|
+
}));
|
|
1005
1058
|
}
|
|
1059
|
+
return response;
|
|
1006
1060
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
usage: {
|
|
1028
|
-
input_tokens: (response.usage?.prompt_tokens ?? 0) - (response.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
1029
|
-
output_tokens: response.usage?.completion_tokens ?? 0,
|
|
1030
|
-
...response.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: response.usage.prompt_tokens_details.cached_tokens }
|
|
1061
|
+
/**
|
|
1062
|
+
* Forward an Anthropic count_tokens request to Copilot's native endpoint.
|
|
1063
|
+
* Returns the raw Response.
|
|
1064
|
+
*/
|
|
1065
|
+
async function countTokens(body) {
|
|
1066
|
+
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
1067
|
+
const headers = buildHeaders();
|
|
1068
|
+
const url = `${copilotBaseUrl(state)}/v1/messages/count_tokens`;
|
|
1069
|
+
consola.debug(`Forwarding to ${url}`);
|
|
1070
|
+
const response = await fetch(url, {
|
|
1071
|
+
method: "POST",
|
|
1072
|
+
headers,
|
|
1073
|
+
body
|
|
1074
|
+
});
|
|
1075
|
+
if (!response.ok) {
|
|
1076
|
+
let errorBody = "";
|
|
1077
|
+
try {
|
|
1078
|
+
errorBody = await response.text();
|
|
1079
|
+
} catch {
|
|
1080
|
+
errorBody = "(could not read error body)";
|
|
1031
1081
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
type: "text",
|
|
1041
|
-
text: part.text
|
|
1042
|
-
}));
|
|
1043
|
-
return [];
|
|
1044
|
-
}
|
|
1045
|
-
function getAnthropicToolUseBlocks(toolCalls) {
|
|
1046
|
-
if (!toolCalls) return [];
|
|
1047
|
-
return toolCalls.map((toolCall) => ({
|
|
1048
|
-
type: "tool_use",
|
|
1049
|
-
id: toolCall.id,
|
|
1050
|
-
name: toolCall.function.name,
|
|
1051
|
-
input: JSON.parse(toolCall.function.arguments)
|
|
1052
|
-
}));
|
|
1082
|
+
consola.error(`Copilot count_tokens error: ${response.status} ${errorBody}`);
|
|
1083
|
+
throw new HTTPError("Copilot count_tokens request failed", new Response(errorBody, {
|
|
1084
|
+
status: response.status,
|
|
1085
|
+
statusText: response.statusText,
|
|
1086
|
+
headers: response.headers
|
|
1087
|
+
}));
|
|
1088
|
+
}
|
|
1089
|
+
return response;
|
|
1053
1090
|
}
|
|
1054
1091
|
|
|
1055
1092
|
//#endregion
|
|
1056
1093
|
//#region src/routes/messages/count-tokens-handler.ts
|
|
1057
1094
|
/**
|
|
1058
|
-
*
|
|
1095
|
+
* Passthrough handler for Anthropic token counting.
|
|
1096
|
+
* Forwards the request directly to Copilot's native /v1/messages/count_tokens endpoint.
|
|
1059
1097
|
*/
|
|
1060
1098
|
async function handleCountTokens(c) {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
const openAIPayload = translateToOpenAI(anthropicPayload);
|
|
1065
|
-
const selectedModel = state.models?.data.find((model) => model.id === anthropicPayload.model);
|
|
1066
|
-
if (!selectedModel) {
|
|
1067
|
-
consola.warn("Model not found, returning default token count");
|
|
1068
|
-
return c.json({ input_tokens: 1 });
|
|
1069
|
-
}
|
|
1070
|
-
const tokenCount = await getTokenCount(openAIPayload, selectedModel);
|
|
1071
|
-
if (anthropicPayload.tools && anthropicPayload.tools.length > 0) {
|
|
1072
|
-
let mcpToolExist = false;
|
|
1073
|
-
if (anthropicBeta?.startsWith("claude-code")) mcpToolExist = anthropicPayload.tools.some((tool) => tool.name.startsWith("mcp__"));
|
|
1074
|
-
if (!mcpToolExist) {
|
|
1075
|
-
if (anthropicPayload.model.startsWith("claude")) tokenCount.input = tokenCount.input + 346;
|
|
1076
|
-
else if (anthropicPayload.model.startsWith("grok")) tokenCount.input = tokenCount.input + 480;
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
let finalTokenCount = tokenCount.input + tokenCount.output;
|
|
1080
|
-
if (anthropicPayload.model.startsWith("claude")) finalTokenCount = Math.round(finalTokenCount * 1.15);
|
|
1081
|
-
else if (anthropicPayload.model.startsWith("grok")) finalTokenCount = Math.round(finalTokenCount * 1.03);
|
|
1082
|
-
consola.info("Token count:", finalTokenCount);
|
|
1083
|
-
return c.json({ input_tokens: finalTokenCount });
|
|
1084
|
-
} catch (error) {
|
|
1085
|
-
consola.error("Error counting tokens:", error);
|
|
1086
|
-
return c.json({ input_tokens: 1 });
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
//#endregion
|
|
1091
|
-
//#region src/routes/messages/stream-translation.ts
|
|
1092
|
-
function isToolBlockOpen(state$1) {
|
|
1093
|
-
if (!state$1.contentBlockOpen) return false;
|
|
1094
|
-
return Object.values(state$1.toolCalls).some((tc) => tc.anthropicBlockIndex === state$1.contentBlockIndex);
|
|
1095
|
-
}
|
|
1096
|
-
function translateChunkToAnthropicEvents(chunk, state$1) {
|
|
1097
|
-
const events$1 = [];
|
|
1098
|
-
if (chunk.choices.length === 0) return events$1;
|
|
1099
|
-
const choice = chunk.choices[0];
|
|
1100
|
-
const { delta } = choice;
|
|
1101
|
-
if (!state$1.messageStartSent) {
|
|
1102
|
-
events$1.push({
|
|
1103
|
-
type: "message_start",
|
|
1104
|
-
message: {
|
|
1105
|
-
id: chunk.id,
|
|
1106
|
-
type: "message",
|
|
1107
|
-
role: "assistant",
|
|
1108
|
-
content: [],
|
|
1109
|
-
model: chunk.model,
|
|
1110
|
-
stop_reason: null,
|
|
1111
|
-
stop_sequence: null,
|
|
1112
|
-
usage: {
|
|
1113
|
-
input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
1114
|
-
output_tokens: 0,
|
|
1115
|
-
...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
});
|
|
1119
|
-
state$1.messageStartSent = true;
|
|
1120
|
-
}
|
|
1121
|
-
if (delta.content) {
|
|
1122
|
-
if (isToolBlockOpen(state$1)) {
|
|
1123
|
-
events$1.push({
|
|
1124
|
-
type: "content_block_stop",
|
|
1125
|
-
index: state$1.contentBlockIndex
|
|
1126
|
-
});
|
|
1127
|
-
state$1.contentBlockIndex++;
|
|
1128
|
-
state$1.contentBlockOpen = false;
|
|
1129
|
-
}
|
|
1130
|
-
if (!state$1.contentBlockOpen) {
|
|
1131
|
-
events$1.push({
|
|
1132
|
-
type: "content_block_start",
|
|
1133
|
-
index: state$1.contentBlockIndex,
|
|
1134
|
-
content_block: {
|
|
1135
|
-
type: "text",
|
|
1136
|
-
text: ""
|
|
1137
|
-
}
|
|
1138
|
-
});
|
|
1139
|
-
state$1.contentBlockOpen = true;
|
|
1140
|
-
}
|
|
1141
|
-
events$1.push({
|
|
1142
|
-
type: "content_block_delta",
|
|
1143
|
-
index: state$1.contentBlockIndex,
|
|
1144
|
-
delta: {
|
|
1145
|
-
type: "text_delta",
|
|
1146
|
-
text: delta.content
|
|
1147
|
-
}
|
|
1148
|
-
});
|
|
1149
|
-
}
|
|
1150
|
-
if (delta.tool_calls) for (const toolCall of delta.tool_calls) {
|
|
1151
|
-
if (toolCall.id && toolCall.function?.name) {
|
|
1152
|
-
if (state$1.contentBlockOpen) {
|
|
1153
|
-
events$1.push({
|
|
1154
|
-
type: "content_block_stop",
|
|
1155
|
-
index: state$1.contentBlockIndex
|
|
1156
|
-
});
|
|
1157
|
-
state$1.contentBlockIndex++;
|
|
1158
|
-
state$1.contentBlockOpen = false;
|
|
1159
|
-
}
|
|
1160
|
-
const anthropicBlockIndex = state$1.contentBlockIndex;
|
|
1161
|
-
state$1.toolCalls[toolCall.index] = {
|
|
1162
|
-
id: toolCall.id,
|
|
1163
|
-
name: toolCall.function.name,
|
|
1164
|
-
anthropicBlockIndex
|
|
1165
|
-
};
|
|
1166
|
-
events$1.push({
|
|
1167
|
-
type: "content_block_start",
|
|
1168
|
-
index: anthropicBlockIndex,
|
|
1169
|
-
content_block: {
|
|
1170
|
-
type: "tool_use",
|
|
1171
|
-
id: toolCall.id,
|
|
1172
|
-
name: toolCall.function.name,
|
|
1173
|
-
input: {}
|
|
1174
|
-
}
|
|
1175
|
-
});
|
|
1176
|
-
state$1.contentBlockOpen = true;
|
|
1177
|
-
}
|
|
1178
|
-
if (toolCall.function?.arguments) {
|
|
1179
|
-
const toolCallInfo = state$1.toolCalls[toolCall.index];
|
|
1180
|
-
if (toolCallInfo) events$1.push({
|
|
1181
|
-
type: "content_block_delta",
|
|
1182
|
-
index: toolCallInfo.anthropicBlockIndex,
|
|
1183
|
-
delta: {
|
|
1184
|
-
type: "input_json_delta",
|
|
1185
|
-
partial_json: toolCall.function.arguments
|
|
1186
|
-
}
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
if (choice.finish_reason) {
|
|
1191
|
-
if (state$1.contentBlockOpen) {
|
|
1192
|
-
events$1.push({
|
|
1193
|
-
type: "content_block_stop",
|
|
1194
|
-
index: state$1.contentBlockIndex
|
|
1195
|
-
});
|
|
1196
|
-
state$1.contentBlockOpen = false;
|
|
1197
|
-
}
|
|
1198
|
-
events$1.push({
|
|
1199
|
-
type: "message_delta",
|
|
1200
|
-
delta: {
|
|
1201
|
-
stop_reason: mapOpenAIStopReasonToAnthropic(choice.finish_reason),
|
|
1202
|
-
stop_sequence: null
|
|
1203
|
-
},
|
|
1204
|
-
usage: {
|
|
1205
|
-
input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
1206
|
-
output_tokens: chunk.usage?.completion_tokens ?? 0,
|
|
1207
|
-
...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
|
|
1208
|
-
}
|
|
1209
|
-
}, { type: "message_stop" });
|
|
1210
|
-
}
|
|
1211
|
-
return events$1;
|
|
1099
|
+
const body = await (await countTokens(await c.req.text())).json();
|
|
1100
|
+
consola.info("Token count:", JSON.stringify(body));
|
|
1101
|
+
return c.json(body);
|
|
1212
1102
|
}
|
|
1213
1103
|
|
|
1214
1104
|
//#endregion
|
|
1215
1105
|
//#region src/routes/messages/handler.ts
|
|
1216
1106
|
async function handleCompletion(c) {
|
|
1217
1107
|
await checkRateLimit(state);
|
|
1218
|
-
const
|
|
1219
|
-
consola.
|
|
1220
|
-
|
|
1221
|
-
consola.debug("Translated OpenAI request payload:", JSON.stringify(openAIPayload));
|
|
1108
|
+
const rawBody = await c.req.text();
|
|
1109
|
+
const debugEnabled = consola.level >= 4;
|
|
1110
|
+
if (debugEnabled) consola.debug("Anthropic request body:", rawBody.slice(0, 2e3));
|
|
1222
1111
|
if (state.manualApprove) await awaitApproval();
|
|
1223
|
-
const response = await
|
|
1224
|
-
if (
|
|
1225
|
-
consola.debug("
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
const streamState = {
|
|
1233
|
-
messageStartSent: false,
|
|
1234
|
-
contentBlockIndex: 0,
|
|
1235
|
-
contentBlockOpen: false,
|
|
1236
|
-
toolCalls: {}
|
|
1237
|
-
};
|
|
1238
|
-
for await (const rawEvent of response) {
|
|
1239
|
-
consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
|
|
1240
|
-
if (rawEvent.data === "[DONE]") break;
|
|
1241
|
-
if (!rawEvent.data) continue;
|
|
1242
|
-
const events$1 = translateChunkToAnthropicEvents(JSON.parse(rawEvent.data), streamState);
|
|
1243
|
-
for (const event of events$1) {
|
|
1244
|
-
consola.debug("Translated Anthropic event:", JSON.stringify(event));
|
|
1245
|
-
await stream.writeSSE({
|
|
1246
|
-
event: event.type,
|
|
1247
|
-
data: JSON.stringify(event)
|
|
1248
|
-
});
|
|
1112
|
+
const response = await createMessages(rawBody);
|
|
1113
|
+
if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
1114
|
+
if (debugEnabled) consola.debug("Streaming response from Copilot /v1/messages");
|
|
1115
|
+
return new Response(response.body, {
|
|
1116
|
+
status: response.status,
|
|
1117
|
+
headers: {
|
|
1118
|
+
"content-type": "text/event-stream",
|
|
1119
|
+
"cache-control": "no-cache",
|
|
1120
|
+
connection: "keep-alive"
|
|
1249
1121
|
}
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
const body = await response.json();
|
|
1125
|
+
if (debugEnabled) consola.debug("Non-streaming response from Copilot /v1/messages:", JSON.stringify(body).slice(0, 2e3));
|
|
1126
|
+
return c.json(body, response.status);
|
|
1252
1127
|
}
|
|
1253
|
-
const isNonStreaming$1 = (response) => Object.hasOwn(response, "choices");
|
|
1254
1128
|
|
|
1255
1129
|
//#endregion
|
|
1256
1130
|
//#region src/routes/messages/route.ts
|
|
@@ -1331,7 +1205,7 @@ function detectAgentCall(input) {
|
|
|
1331
1205
|
if (!Array.isArray(input)) return false;
|
|
1332
1206
|
return input.some((item) => {
|
|
1333
1207
|
if ("role" in item && item.role === "assistant") return true;
|
|
1334
|
-
if ("type" in item && item.type === "function_call_output") return true;
|
|
1208
|
+
if ("type" in item && (item.type === "function_call" || item.type === "function_call_output")) return true;
|
|
1335
1209
|
return false;
|
|
1336
1210
|
});
|
|
1337
1211
|
}
|
|
@@ -1342,77 +1216,23 @@ function filterUnsupportedTools(payload) {
|
|
|
1342
1216
|
if (!isSupported) consola.debug(`Stripping unsupported tool type: ${tool.type}`);
|
|
1343
1217
|
return isSupported;
|
|
1344
1218
|
});
|
|
1219
|
+
let toolChoice = payload.tool_choice;
|
|
1220
|
+
if (supported.length === 0) toolChoice = void 0;
|
|
1221
|
+
else if (toolChoice && typeof toolChoice === "object") {
|
|
1222
|
+
const supportedNames = new Set(supported.map((tool) => tool.name).filter(Boolean));
|
|
1223
|
+
const toolChoiceName = getToolChoiceName(toolChoice);
|
|
1224
|
+
if (toolChoiceName && !supportedNames.has(toolChoiceName)) toolChoice = void 0;
|
|
1225
|
+
}
|
|
1345
1226
|
return {
|
|
1346
1227
|
...payload,
|
|
1347
|
-
tools: supported.length > 0 ? supported : void 0
|
|
1228
|
+
tools: supported.length > 0 ? supported : void 0,
|
|
1229
|
+
tool_choice: toolChoice
|
|
1348
1230
|
};
|
|
1349
1231
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
let searchTimestamps = [];
|
|
1355
|
-
async function throttleSearch() {
|
|
1356
|
-
const now = Date.now();
|
|
1357
|
-
searchTimestamps = searchTimestamps.filter((t) => now - t < 1e3);
|
|
1358
|
-
if (searchTimestamps.length >= MAX_SEARCHES_PER_SECOND) {
|
|
1359
|
-
const waitMs = 1e3 - (now - searchTimestamps[0]);
|
|
1360
|
-
if (waitMs > 0) {
|
|
1361
|
-
consola.debug(`Web search rate limited, waiting ${waitMs}ms`);
|
|
1362
|
-
await sleep(waitMs);
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
searchTimestamps.push(Date.now());
|
|
1366
|
-
}
|
|
1367
|
-
function threadsHeaders() {
|
|
1368
|
-
return copilotHeaders(state, false, "copilot-chat");
|
|
1369
|
-
}
|
|
1370
|
-
async function createThread() {
|
|
1371
|
-
const response = await fetch(`${copilotBaseUrl(state)}/github/chat/threads`, {
|
|
1372
|
-
method: "POST",
|
|
1373
|
-
headers: threadsHeaders(),
|
|
1374
|
-
body: JSON.stringify({})
|
|
1375
|
-
});
|
|
1376
|
-
if (!response.ok) {
|
|
1377
|
-
consola.error("Failed to create chat thread", response.status);
|
|
1378
|
-
throw new Error(`Failed to create chat thread: ${response.status}`);
|
|
1379
|
-
}
|
|
1380
|
-
return (await response.json()).thread_id;
|
|
1381
|
-
}
|
|
1382
|
-
async function sendThreadMessage(threadId, query) {
|
|
1383
|
-
const response = await fetch(`${copilotBaseUrl(state)}/github/chat/threads/${threadId}/messages`, {
|
|
1384
|
-
method: "POST",
|
|
1385
|
-
headers: threadsHeaders(),
|
|
1386
|
-
body: JSON.stringify({
|
|
1387
|
-
content: query,
|
|
1388
|
-
intent: "conversation",
|
|
1389
|
-
skills: ["web-search"],
|
|
1390
|
-
references: []
|
|
1391
|
-
})
|
|
1392
|
-
});
|
|
1393
|
-
if (!response.ok) {
|
|
1394
|
-
consola.error("Failed to send thread message", response.status);
|
|
1395
|
-
throw new Error(`Failed to send thread message: ${response.status}`);
|
|
1396
|
-
}
|
|
1397
|
-
return await response.json();
|
|
1398
|
-
}
|
|
1399
|
-
async function searchWeb(query) {
|
|
1400
|
-
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
1401
|
-
await throttleSearch();
|
|
1402
|
-
consola.info(`Web search: "${query.slice(0, 80)}"`);
|
|
1403
|
-
const response = await sendThreadMessage(await createThread(), query);
|
|
1404
|
-
const references = [];
|
|
1405
|
-
for (const ref of response.message.references) if (ref.results) {
|
|
1406
|
-
for (const result of ref.results) if (result.url && result.reference_type !== "bing_search") references.push({
|
|
1407
|
-
title: result.title,
|
|
1408
|
-
url: result.url
|
|
1409
|
-
});
|
|
1410
|
-
}
|
|
1411
|
-
consola.debug(`Web search returned ${references.length} references`);
|
|
1412
|
-
return {
|
|
1413
|
-
content: response.message.content,
|
|
1414
|
-
references
|
|
1415
|
-
};
|
|
1232
|
+
function getToolChoiceName(toolChoice) {
|
|
1233
|
+
if (typeof toolChoice !== "object") return void 0;
|
|
1234
|
+
if ("function" in toolChoice && toolChoice.function && typeof toolChoice.function === "object") return toolChoice.function.name;
|
|
1235
|
+
if ("name" in toolChoice) return toolChoice.name;
|
|
1416
1236
|
}
|
|
1417
1237
|
|
|
1418
1238
|
//#endregion
|
|
@@ -1420,24 +1240,25 @@ async function searchWeb(query) {
|
|
|
1420
1240
|
async function handleResponses(c) {
|
|
1421
1241
|
await checkRateLimit(state);
|
|
1422
1242
|
const payload = await c.req.json();
|
|
1423
|
-
consola.
|
|
1243
|
+
const debugEnabled = consola.level >= 4;
|
|
1244
|
+
if (debugEnabled) consola.debug("Responses request payload:", JSON.stringify(payload).slice(-400));
|
|
1424
1245
|
const selectedModel = state.models?.data.find((model) => model.id === payload.model);
|
|
1425
1246
|
consola.info("Token counting not yet supported for /responses endpoint");
|
|
1426
1247
|
if (state.manualApprove) await awaitApproval();
|
|
1427
1248
|
await injectWebSearchIfNeeded(payload);
|
|
1428
1249
|
if (isNullish(payload.max_output_tokens)) {
|
|
1429
1250
|
payload.max_output_tokens = selectedModel?.capabilities.limits.max_output_tokens;
|
|
1430
|
-
consola.debug("Set max_output_tokens to:", JSON.stringify(payload.max_output_tokens));
|
|
1251
|
+
if (debugEnabled) consola.debug("Set max_output_tokens to:", JSON.stringify(payload.max_output_tokens));
|
|
1431
1252
|
}
|
|
1432
1253
|
const response = await createResponses(payload);
|
|
1433
1254
|
if (isNonStreaming(response)) {
|
|
1434
|
-
consola.debug("Non-streaming response:", JSON.stringify(response));
|
|
1255
|
+
if (debugEnabled) consola.debug("Non-streaming response:", JSON.stringify(response));
|
|
1435
1256
|
return c.json(response);
|
|
1436
1257
|
}
|
|
1437
1258
|
consola.debug("Streaming response");
|
|
1438
1259
|
return streamSSE(c, async (stream) => {
|
|
1439
1260
|
for await (const chunk of response) {
|
|
1440
|
-
consola.debug("Streaming chunk:", JSON.stringify(chunk));
|
|
1261
|
+
if (debugEnabled) consola.debug("Streaming chunk:", JSON.stringify(chunk));
|
|
1441
1262
|
if (chunk.data === "[DONE]") break;
|
|
1442
1263
|
if (!chunk.data) continue;
|
|
1443
1264
|
await stream.writeSSE({
|
|
@@ -1514,6 +1335,10 @@ searchRoutes.post("/", async (c) => {
|
|
|
1514
1335
|
//#region src/routes/token/route.ts
|
|
1515
1336
|
const tokenRoute = new Hono();
|
|
1516
1337
|
tokenRoute.get("/", (c) => {
|
|
1338
|
+
if (!state.showToken) return c.json({ error: {
|
|
1339
|
+
message: "Token endpoint disabled",
|
|
1340
|
+
type: "error"
|
|
1341
|
+
} }, 403);
|
|
1517
1342
|
return c.json({ token: state.copilotToken });
|
|
1518
1343
|
});
|
|
1519
1344
|
|
|
@@ -1552,18 +1377,35 @@ server.route("/v1/messages", messageRoutes);
|
|
|
1552
1377
|
|
|
1553
1378
|
//#endregion
|
|
1554
1379
|
//#region src/start.ts
|
|
1380
|
+
const allowedAccountTypes = new Set([
|
|
1381
|
+
"individual",
|
|
1382
|
+
"business",
|
|
1383
|
+
"enterprise"
|
|
1384
|
+
]);
|
|
1385
|
+
function filterModelsByEndpoint(models, endpoint) {
|
|
1386
|
+
const filtered = models.filter((model) => {
|
|
1387
|
+
const endpoints = model.supported_endpoints;
|
|
1388
|
+
if (!endpoints || endpoints.length === 0) return true;
|
|
1389
|
+
return endpoints.some((entry) => {
|
|
1390
|
+
return entry.replace(/^\/?v1\//, "").replace(/^\//, "") === endpoint;
|
|
1391
|
+
});
|
|
1392
|
+
});
|
|
1393
|
+
return filtered.length > 0 ? filtered : models;
|
|
1394
|
+
}
|
|
1555
1395
|
async function generateClaudeCodeCommand(serverUrl) {
|
|
1556
1396
|
invariant(state.models, "Models should be loaded by now");
|
|
1397
|
+
const supportedModels = filterModelsByEndpoint(state.models.data, "v1/messages");
|
|
1557
1398
|
const selectedModel = await consola.prompt("Select a model to use with Claude Code", {
|
|
1558
1399
|
type: "select",
|
|
1559
|
-
options:
|
|
1400
|
+
options: supportedModels.map((model) => model.id)
|
|
1560
1401
|
});
|
|
1561
1402
|
const selectedSmallModel = await consola.prompt("Select a small model to use with Claude Code", {
|
|
1562
1403
|
type: "select",
|
|
1563
|
-
options:
|
|
1404
|
+
options: supportedModels.map((model) => model.id)
|
|
1564
1405
|
});
|
|
1565
1406
|
const command = generateEnvScript({
|
|
1566
1407
|
ANTHROPIC_BASE_URL: serverUrl,
|
|
1408
|
+
ANTHROPIC_API_KEY: "dummy",
|
|
1567
1409
|
ANTHROPIC_AUTH_TOKEN: "dummy",
|
|
1568
1410
|
ANTHROPIC_MODEL: selectedModel,
|
|
1569
1411
|
ANTHROPIC_DEFAULT_SONNET_MODEL: selectedModel,
|
|
@@ -1582,15 +1424,17 @@ async function generateClaudeCodeCommand(serverUrl) {
|
|
|
1582
1424
|
}
|
|
1583
1425
|
async function generateCodexCommand(serverUrl) {
|
|
1584
1426
|
invariant(state.models, "Models should be loaded by now");
|
|
1585
|
-
const
|
|
1427
|
+
const supportedModels = filterModelsByEndpoint(state.models.data, "responses");
|
|
1428
|
+
const defaultCodexModel = supportedModels.find((model) => model.id === "gpt5.2-codex");
|
|
1586
1429
|
const selectedModel = defaultCodexModel ? defaultCodexModel.id : await consola.prompt("Select a model to use with Codex CLI", {
|
|
1587
1430
|
type: "select",
|
|
1588
|
-
options:
|
|
1431
|
+
options: supportedModels.map((model) => model.id)
|
|
1589
1432
|
});
|
|
1433
|
+
const quotedModel = JSON.stringify(selectedModel);
|
|
1590
1434
|
const command = generateEnvScript({
|
|
1591
1435
|
OPENAI_BASE_URL: `${serverUrl}/v1`,
|
|
1592
1436
|
OPENAI_API_KEY: "dummy"
|
|
1593
|
-
}, `codex -m ${
|
|
1437
|
+
}, `codex -m ${quotedModel}`);
|
|
1594
1438
|
try {
|
|
1595
1439
|
clipboard.writeSync(command);
|
|
1596
1440
|
consola.success("Copied Codex CLI command to clipboard!");
|
|
@@ -1626,6 +1470,7 @@ async function runServer(options) {
|
|
|
1626
1470
|
consola.box(`🌐 Usage Viewer: https://animeshkundu.github.io/github-router/dashboard.html?endpoint=${serverUrl}/usage`);
|
|
1627
1471
|
serve({
|
|
1628
1472
|
fetch: server.fetch,
|
|
1473
|
+
hostname: "127.0.0.1",
|
|
1629
1474
|
port: options.port
|
|
1630
1475
|
});
|
|
1631
1476
|
}
|
|
@@ -1698,15 +1543,26 @@ const start = defineCommand({
|
|
|
1698
1543
|
},
|
|
1699
1544
|
run({ args }) {
|
|
1700
1545
|
const rateLimitRaw = args["rate-limit"];
|
|
1701
|
-
|
|
1546
|
+
let rateLimit;
|
|
1547
|
+
if (rateLimitRaw !== void 0) {
|
|
1548
|
+
rateLimit = Number.parseInt(rateLimitRaw, 10);
|
|
1549
|
+
if (Number.isNaN(rateLimit) || rateLimit <= 0) throw new Error("Invalid rate limit. Must be a positive integer.");
|
|
1550
|
+
}
|
|
1551
|
+
const port = Number.parseInt(args.port, 10);
|
|
1552
|
+
if (Number.isNaN(port) || port <= 0 || port > 65535) throw new Error("Invalid port. Must be between 1 and 65535.");
|
|
1553
|
+
const accountType = args["account-type"];
|
|
1554
|
+
if (!allowedAccountTypes.has(accountType)) throw new Error("Invalid account type. Must be individual, business, or enterprise.");
|
|
1555
|
+
const rateLimitWait = args.wait && rateLimit !== void 0;
|
|
1556
|
+
if (args.wait && rateLimit === void 0) consola.warn("Rate limit wait ignored because no rate limit was set.");
|
|
1557
|
+
const githubToken = args["github-token"] ?? process.env.GH_TOKEN;
|
|
1702
1558
|
return runServer({
|
|
1703
|
-
port
|
|
1559
|
+
port,
|
|
1704
1560
|
verbose: args.verbose,
|
|
1705
|
-
accountType
|
|
1561
|
+
accountType,
|
|
1706
1562
|
manual: args.manual,
|
|
1707
1563
|
rateLimit,
|
|
1708
|
-
rateLimitWait
|
|
1709
|
-
githubToken
|
|
1564
|
+
rateLimitWait,
|
|
1565
|
+
githubToken,
|
|
1710
1566
|
claudeCode: args["claude-code"],
|
|
1711
1567
|
codex: args.codex,
|
|
1712
1568
|
showToken: args["show-token"],
|