github-router 0.3.0 → 0.3.2
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 +3 -1
- package/dist/main.js +235 -457
- 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,8 +800,19 @@ 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();
|
|
@@ -837,7 +872,7 @@ async function searchWeb(query) {
|
|
|
837
872
|
consola.info(`Web search: "${query.slice(0, 80)}"`);
|
|
838
873
|
const response = await sendThreadMessage(await createThread(), query);
|
|
839
874
|
const references = [];
|
|
840
|
-
for (const ref of response.message.references) if (ref.results) {
|
|
875
|
+
for (const ref of response.message.references ?? []) if (ref.results) {
|
|
841
876
|
for (const result of ref.results) if (result.url && result.reference_type !== "bing_search") references.push({
|
|
842
877
|
title: result.title,
|
|
843
878
|
url: result.url
|
|
@@ -855,8 +890,10 @@ async function searchWeb(query) {
|
|
|
855
890
|
async function handleCompletion$1(c) {
|
|
856
891
|
await checkRateLimit(state);
|
|
857
892
|
let payload = await c.req.json();
|
|
858
|
-
consola.
|
|
859
|
-
|
|
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);
|
|
860
897
|
const selectedModel = state.models?.data.find((model) => model.id === payload.model);
|
|
861
898
|
try {
|
|
862
899
|
if (selectedModel) {
|
|
@@ -866,34 +903,31 @@ async function handleCompletion$1(c) {
|
|
|
866
903
|
} catch (error) {
|
|
867
904
|
consola.warn("Failed to calculate token count:", error);
|
|
868
905
|
}
|
|
869
|
-
if (state.manualApprove) await awaitApproval();
|
|
870
906
|
if (isNullish(payload.max_tokens)) {
|
|
871
907
|
payload = {
|
|
872
908
|
...payload,
|
|
873
909
|
max_tokens: selectedModel?.capabilities.limits.max_output_tokens
|
|
874
910
|
};
|
|
875
|
-
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));
|
|
876
912
|
}
|
|
877
913
|
const response = await createChatCompletions(payload);
|
|
878
|
-
if (isNonStreaming$
|
|
879
|
-
consola.debug("Non-streaming response:", JSON.stringify(response));
|
|
914
|
+
if (isNonStreaming$1(response)) {
|
|
915
|
+
if (debugEnabled) consola.debug("Non-streaming response:", JSON.stringify(response));
|
|
880
916
|
return c.json(response);
|
|
881
917
|
}
|
|
882
918
|
consola.debug("Streaming response");
|
|
883
919
|
return streamSSE(c, async (stream) => {
|
|
884
920
|
for await (const chunk of response) {
|
|
885
|
-
consola.debug("Streaming chunk:", JSON.stringify(chunk));
|
|
921
|
+
if (debugEnabled) consola.debug("Streaming chunk:", JSON.stringify(chunk));
|
|
886
922
|
await stream.writeSSE(chunk);
|
|
887
923
|
}
|
|
888
924
|
});
|
|
889
925
|
}
|
|
890
|
-
const isNonStreaming$
|
|
891
|
-
async function injectWebSearchIfNeeded$
|
|
926
|
+
const isNonStreaming$1 = (response) => Object.hasOwn(response, "choices");
|
|
927
|
+
async function injectWebSearchIfNeeded$1(payload) {
|
|
892
928
|
if (!payload.tools?.some((t) => "type" in t && t.type === "web_search" || t.function?.name === "web_search")) return;
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
if (!query) return;
|
|
896
|
-
try {
|
|
929
|
+
const query = payload.messages.some((msg) => msg.role === "tool") ? void 0 : extractUserQuery$1(payload.messages);
|
|
930
|
+
if (query) try {
|
|
897
931
|
const results = await searchWeb(query);
|
|
898
932
|
const searchContext = [
|
|
899
933
|
"[Web Search Results]",
|
|
@@ -913,8 +947,13 @@ async function injectWebSearchIfNeeded$2(payload) {
|
|
|
913
947
|
}
|
|
914
948
|
payload.tools = payload.tools?.filter((t) => !("type" in t && t.type === "web_search" || t.function?.name === "web_search"));
|
|
915
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
|
+
}
|
|
916
955
|
}
|
|
917
|
-
function extractUserQuery$
|
|
956
|
+
function extractUserQuery$1(messages) {
|
|
918
957
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
919
958
|
const msg = messages[i];
|
|
920
959
|
if (msg.role === "user") {
|
|
@@ -964,437 +1003,127 @@ embeddingRoutes.post("/", async (c) => {
|
|
|
964
1003
|
});
|
|
965
1004
|
|
|
966
1005
|
//#endregion
|
|
967
|
-
//#region src/
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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() {
|
|
981
1024
|
return {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
stream: payload.stream,
|
|
987
|
-
temperature: payload.temperature,
|
|
988
|
-
top_p: payload.top_p,
|
|
989
|
-
user: payload.metadata?.user_id,
|
|
990
|
-
tools: translateAnthropicToolsToOpenAI(payload.tools),
|
|
991
|
-
tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice)
|
|
1025
|
+
...copilotHeaders(state),
|
|
1026
|
+
"X-Initiator": "agent",
|
|
1027
|
+
"anthropic-version": "2023-06-01",
|
|
1028
|
+
"X-Interaction-Id": randomUUID()
|
|
992
1029
|
};
|
|
993
1030
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
const
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
role: "system",
|
|
1008
|
-
content: system
|
|
1009
|
-
}];
|
|
1010
|
-
else return [{
|
|
1011
|
-
role: "system",
|
|
1012
|
-
content: system.map((block) => block.text).join("\n\n")
|
|
1013
|
-
}];
|
|
1014
|
-
}
|
|
1015
|
-
function handleUserMessage(message) {
|
|
1016
|
-
const newMessages = [];
|
|
1017
|
-
if (Array.isArray(message.content)) {
|
|
1018
|
-
const toolResultBlocks = message.content.filter((block) => block.type === "tool_result");
|
|
1019
|
-
const otherBlocks = message.content.filter((block) => block.type !== "tool_result");
|
|
1020
|
-
for (const block of toolResultBlocks) newMessages.push({
|
|
1021
|
-
role: "tool",
|
|
1022
|
-
tool_call_id: block.tool_use_id,
|
|
1023
|
-
content: mapContent(block.content)
|
|
1024
|
-
});
|
|
1025
|
-
if (otherBlocks.length > 0) newMessages.push({
|
|
1026
|
-
role: "user",
|
|
1027
|
-
content: mapContent(otherBlocks)
|
|
1028
|
-
});
|
|
1029
|
-
} else newMessages.push({
|
|
1030
|
-
role: "user",
|
|
1031
|
-
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
|
|
1032
1044
|
});
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
}];
|
|
1040
|
-
const toolUseBlocks = message.content.filter((block) => block.type === "tool_use");
|
|
1041
|
-
const textBlocks = message.content.filter((block) => block.type === "text");
|
|
1042
|
-
const thinkingBlocks = message.content.filter((block) => block.type === "thinking");
|
|
1043
|
-
const allTextContent = [...textBlocks.map((b) => b.text), ...thinkingBlocks.map((b) => b.thinking)].join("\n\n");
|
|
1044
|
-
return toolUseBlocks.length > 0 ? [{
|
|
1045
|
-
role: "assistant",
|
|
1046
|
-
content: allTextContent || null,
|
|
1047
|
-
tool_calls: toolUseBlocks.map((toolUse) => ({
|
|
1048
|
-
id: toolUse.id,
|
|
1049
|
-
type: "function",
|
|
1050
|
-
function: {
|
|
1051
|
-
name: toolUse.name,
|
|
1052
|
-
arguments: JSON.stringify(toolUse.input)
|
|
1053
|
-
}
|
|
1054
|
-
}))
|
|
1055
|
-
}] : [{
|
|
1056
|
-
role: "assistant",
|
|
1057
|
-
content: mapContent(message.content)
|
|
1058
|
-
}];
|
|
1059
|
-
}
|
|
1060
|
-
function mapContent(content) {
|
|
1061
|
-
if (typeof content === "string") return content;
|
|
1062
|
-
if (!Array.isArray(content)) return null;
|
|
1063
|
-
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");
|
|
1064
|
-
const contentParts = [];
|
|
1065
|
-
for (const block of content) switch (block.type) {
|
|
1066
|
-
case "text":
|
|
1067
|
-
contentParts.push({
|
|
1068
|
-
type: "text",
|
|
1069
|
-
text: block.text
|
|
1070
|
-
});
|
|
1071
|
-
break;
|
|
1072
|
-
case "thinking":
|
|
1073
|
-
contentParts.push({
|
|
1074
|
-
type: "text",
|
|
1075
|
-
text: block.thinking
|
|
1076
|
-
});
|
|
1077
|
-
break;
|
|
1078
|
-
case "image":
|
|
1079
|
-
contentParts.push({
|
|
1080
|
-
type: "image_url",
|
|
1081
|
-
image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
|
|
1082
|
-
});
|
|
1083
|
-
break;
|
|
1084
|
-
}
|
|
1085
|
-
return contentParts;
|
|
1086
|
-
}
|
|
1087
|
-
function translateAnthropicToolsToOpenAI(anthropicTools) {
|
|
1088
|
-
if (!anthropicTools) return;
|
|
1089
|
-
return anthropicTools.map((tool) => ({
|
|
1090
|
-
type: "function",
|
|
1091
|
-
function: {
|
|
1092
|
-
name: tool.name,
|
|
1093
|
-
description: tool.description,
|
|
1094
|
-
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)";
|
|
1095
1051
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
case "any": return "required";
|
|
1103
|
-
case "tool":
|
|
1104
|
-
if (anthropicToolChoice.name) return {
|
|
1105
|
-
type: "function",
|
|
1106
|
-
function: { name: anthropicToolChoice.name }
|
|
1107
|
-
};
|
|
1108
|
-
return;
|
|
1109
|
-
case "none": return "none";
|
|
1110
|
-
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
|
+
}));
|
|
1111
1058
|
}
|
|
1059
|
+
return response;
|
|
1112
1060
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
usage: {
|
|
1134
|
-
input_tokens: (response.usage?.prompt_tokens ?? 0) - (response.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
1135
|
-
output_tokens: response.usage?.completion_tokens ?? 0,
|
|
1136
|
-
...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)";
|
|
1137
1081
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
type: "text",
|
|
1147
|
-
text: part.text
|
|
1148
|
-
}));
|
|
1149
|
-
return [];
|
|
1150
|
-
}
|
|
1151
|
-
function getAnthropicToolUseBlocks(toolCalls) {
|
|
1152
|
-
if (!toolCalls) return [];
|
|
1153
|
-
return toolCalls.map((toolCall) => ({
|
|
1154
|
-
type: "tool_use",
|
|
1155
|
-
id: toolCall.id,
|
|
1156
|
-
name: toolCall.function.name,
|
|
1157
|
-
input: JSON.parse(toolCall.function.arguments)
|
|
1158
|
-
}));
|
|
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;
|
|
1159
1090
|
}
|
|
1160
1091
|
|
|
1161
1092
|
//#endregion
|
|
1162
1093
|
//#region src/routes/messages/count-tokens-handler.ts
|
|
1163
1094
|
/**
|
|
1164
|
-
*
|
|
1095
|
+
* Passthrough handler for Anthropic token counting.
|
|
1096
|
+
* Forwards the request directly to Copilot's native /v1/messages/count_tokens endpoint.
|
|
1165
1097
|
*/
|
|
1166
1098
|
async function handleCountTokens(c) {
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
const openAIPayload = translateToOpenAI(anthropicPayload);
|
|
1171
|
-
const selectedModel = state.models?.data.find((model) => model.id === anthropicPayload.model);
|
|
1172
|
-
if (!selectedModel) {
|
|
1173
|
-
consola.warn("Model not found, returning default token count");
|
|
1174
|
-
return c.json({ input_tokens: 1 });
|
|
1175
|
-
}
|
|
1176
|
-
const tokenCount = await getTokenCount(openAIPayload, selectedModel);
|
|
1177
|
-
if (anthropicPayload.tools && anthropicPayload.tools.length > 0) {
|
|
1178
|
-
let mcpToolExist = false;
|
|
1179
|
-
if (anthropicBeta?.startsWith("claude-code")) mcpToolExist = anthropicPayload.tools.some((tool) => tool.name.startsWith("mcp__"));
|
|
1180
|
-
if (!mcpToolExist) {
|
|
1181
|
-
if (anthropicPayload.model.startsWith("claude")) tokenCount.input = tokenCount.input + 346;
|
|
1182
|
-
else if (anthropicPayload.model.startsWith("grok")) tokenCount.input = tokenCount.input + 480;
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
let finalTokenCount = tokenCount.input + tokenCount.output;
|
|
1186
|
-
if (anthropicPayload.model.startsWith("claude")) finalTokenCount = Math.round(finalTokenCount * 1.15);
|
|
1187
|
-
else if (anthropicPayload.model.startsWith("grok")) finalTokenCount = Math.round(finalTokenCount * 1.03);
|
|
1188
|
-
consola.info("Token count:", finalTokenCount);
|
|
1189
|
-
return c.json({ input_tokens: finalTokenCount });
|
|
1190
|
-
} catch (error) {
|
|
1191
|
-
consola.error("Error counting tokens:", error);
|
|
1192
|
-
return c.json({ input_tokens: 1 });
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
//#endregion
|
|
1197
|
-
//#region src/routes/messages/stream-translation.ts
|
|
1198
|
-
function isToolBlockOpen(state$1) {
|
|
1199
|
-
if (!state$1.contentBlockOpen) return false;
|
|
1200
|
-
return Object.values(state$1.toolCalls).some((tc) => tc.anthropicBlockIndex === state$1.contentBlockIndex);
|
|
1201
|
-
}
|
|
1202
|
-
function translateChunkToAnthropicEvents(chunk, state$1) {
|
|
1203
|
-
const events$1 = [];
|
|
1204
|
-
if (chunk.choices.length === 0) return events$1;
|
|
1205
|
-
const choice = chunk.choices[0];
|
|
1206
|
-
const { delta } = choice;
|
|
1207
|
-
if (!state$1.messageStartSent) {
|
|
1208
|
-
events$1.push({
|
|
1209
|
-
type: "message_start",
|
|
1210
|
-
message: {
|
|
1211
|
-
id: chunk.id,
|
|
1212
|
-
type: "message",
|
|
1213
|
-
role: "assistant",
|
|
1214
|
-
content: [],
|
|
1215
|
-
model: chunk.model,
|
|
1216
|
-
stop_reason: null,
|
|
1217
|
-
stop_sequence: null,
|
|
1218
|
-
usage: {
|
|
1219
|
-
input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
1220
|
-
output_tokens: 0,
|
|
1221
|
-
...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
});
|
|
1225
|
-
state$1.messageStartSent = true;
|
|
1226
|
-
}
|
|
1227
|
-
if (delta.content) {
|
|
1228
|
-
if (isToolBlockOpen(state$1)) {
|
|
1229
|
-
events$1.push({
|
|
1230
|
-
type: "content_block_stop",
|
|
1231
|
-
index: state$1.contentBlockIndex
|
|
1232
|
-
});
|
|
1233
|
-
state$1.contentBlockIndex++;
|
|
1234
|
-
state$1.contentBlockOpen = false;
|
|
1235
|
-
}
|
|
1236
|
-
if (!state$1.contentBlockOpen) {
|
|
1237
|
-
events$1.push({
|
|
1238
|
-
type: "content_block_start",
|
|
1239
|
-
index: state$1.contentBlockIndex,
|
|
1240
|
-
content_block: {
|
|
1241
|
-
type: "text",
|
|
1242
|
-
text: ""
|
|
1243
|
-
}
|
|
1244
|
-
});
|
|
1245
|
-
state$1.contentBlockOpen = true;
|
|
1246
|
-
}
|
|
1247
|
-
events$1.push({
|
|
1248
|
-
type: "content_block_delta",
|
|
1249
|
-
index: state$1.contentBlockIndex,
|
|
1250
|
-
delta: {
|
|
1251
|
-
type: "text_delta",
|
|
1252
|
-
text: delta.content
|
|
1253
|
-
}
|
|
1254
|
-
});
|
|
1255
|
-
}
|
|
1256
|
-
if (delta.tool_calls) for (const toolCall of delta.tool_calls) {
|
|
1257
|
-
if (toolCall.id && toolCall.function?.name) {
|
|
1258
|
-
if (state$1.contentBlockOpen) {
|
|
1259
|
-
events$1.push({
|
|
1260
|
-
type: "content_block_stop",
|
|
1261
|
-
index: state$1.contentBlockIndex
|
|
1262
|
-
});
|
|
1263
|
-
state$1.contentBlockIndex++;
|
|
1264
|
-
state$1.contentBlockOpen = false;
|
|
1265
|
-
}
|
|
1266
|
-
const anthropicBlockIndex = state$1.contentBlockIndex;
|
|
1267
|
-
state$1.toolCalls[toolCall.index] = {
|
|
1268
|
-
id: toolCall.id,
|
|
1269
|
-
name: toolCall.function.name,
|
|
1270
|
-
anthropicBlockIndex
|
|
1271
|
-
};
|
|
1272
|
-
events$1.push({
|
|
1273
|
-
type: "content_block_start",
|
|
1274
|
-
index: anthropicBlockIndex,
|
|
1275
|
-
content_block: {
|
|
1276
|
-
type: "tool_use",
|
|
1277
|
-
id: toolCall.id,
|
|
1278
|
-
name: toolCall.function.name,
|
|
1279
|
-
input: {}
|
|
1280
|
-
}
|
|
1281
|
-
});
|
|
1282
|
-
state$1.contentBlockOpen = true;
|
|
1283
|
-
}
|
|
1284
|
-
if (toolCall.function?.arguments) {
|
|
1285
|
-
const toolCallInfo = state$1.toolCalls[toolCall.index];
|
|
1286
|
-
if (toolCallInfo) events$1.push({
|
|
1287
|
-
type: "content_block_delta",
|
|
1288
|
-
index: toolCallInfo.anthropicBlockIndex,
|
|
1289
|
-
delta: {
|
|
1290
|
-
type: "input_json_delta",
|
|
1291
|
-
partial_json: toolCall.function.arguments
|
|
1292
|
-
}
|
|
1293
|
-
});
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
if (choice.finish_reason) {
|
|
1297
|
-
if (state$1.contentBlockOpen) {
|
|
1298
|
-
events$1.push({
|
|
1299
|
-
type: "content_block_stop",
|
|
1300
|
-
index: state$1.contentBlockIndex
|
|
1301
|
-
});
|
|
1302
|
-
state$1.contentBlockOpen = false;
|
|
1303
|
-
}
|
|
1304
|
-
events$1.push({
|
|
1305
|
-
type: "message_delta",
|
|
1306
|
-
delta: {
|
|
1307
|
-
stop_reason: mapOpenAIStopReasonToAnthropic(choice.finish_reason),
|
|
1308
|
-
stop_sequence: null
|
|
1309
|
-
},
|
|
1310
|
-
usage: {
|
|
1311
|
-
input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
1312
|
-
output_tokens: chunk.usage?.completion_tokens ?? 0,
|
|
1313
|
-
...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
|
|
1314
|
-
}
|
|
1315
|
-
}, { type: "message_stop" });
|
|
1316
|
-
}
|
|
1317
|
-
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);
|
|
1318
1102
|
}
|
|
1319
1103
|
|
|
1320
1104
|
//#endregion
|
|
1321
1105
|
//#region src/routes/messages/handler.ts
|
|
1322
1106
|
async function handleCompletion(c) {
|
|
1323
1107
|
await checkRateLimit(state);
|
|
1324
|
-
const
|
|
1325
|
-
consola.
|
|
1326
|
-
|
|
1327
|
-
const openAIPayload = translateToOpenAI(anthropicPayload);
|
|
1328
|
-
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));
|
|
1329
1111
|
if (state.manualApprove) await awaitApproval();
|
|
1330
|
-
const response = await
|
|
1331
|
-
if (
|
|
1332
|
-
consola.debug("
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
const streamState = {
|
|
1340
|
-
messageStartSent: false,
|
|
1341
|
-
contentBlockIndex: 0,
|
|
1342
|
-
contentBlockOpen: false,
|
|
1343
|
-
toolCalls: {}
|
|
1344
|
-
};
|
|
1345
|
-
for await (const rawEvent of response) {
|
|
1346
|
-
consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
|
|
1347
|
-
if (rawEvent.data === "[DONE]") break;
|
|
1348
|
-
if (!rawEvent.data) continue;
|
|
1349
|
-
const events$1 = translateChunkToAnthropicEvents(JSON.parse(rawEvent.data), streamState);
|
|
1350
|
-
for (const event of events$1) {
|
|
1351
|
-
consola.debug("Translated Anthropic event:", JSON.stringify(event));
|
|
1352
|
-
await stream.writeSSE({
|
|
1353
|
-
event: event.type,
|
|
1354
|
-
data: JSON.stringify(event)
|
|
1355
|
-
});
|
|
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"
|
|
1356
1121
|
}
|
|
1357
|
-
}
|
|
1358
|
-
});
|
|
1359
|
-
}
|
|
1360
|
-
const isNonStreaming$1 = (response) => Object.hasOwn(response, "choices");
|
|
1361
|
-
async function injectWebSearchIfNeeded$1(payload) {
|
|
1362
|
-
if (!payload.tools?.some((t) => t.name === "web_search")) return;
|
|
1363
|
-
if (payload.messages.some((msg) => msg.role === "user" && Array.isArray(msg.content) && msg.content.some((block) => "type" in block && block.type === "tool_result"))) return;
|
|
1364
|
-
const query = extractUserQuery$1(payload.messages);
|
|
1365
|
-
if (!query) return;
|
|
1366
|
-
try {
|
|
1367
|
-
const results = await searchWeb(query);
|
|
1368
|
-
const searchContext = [
|
|
1369
|
-
"[Web Search Results]",
|
|
1370
|
-
results.content,
|
|
1371
|
-
"",
|
|
1372
|
-
results.references.map((r) => `- [${r.title}](${r.url})`).join("\n"),
|
|
1373
|
-
"[End Web Search Results]"
|
|
1374
|
-
].join("\n");
|
|
1375
|
-
if (typeof payload.system === "string") payload.system = `${searchContext}\n\n${payload.system}`;
|
|
1376
|
-
else if (Array.isArray(payload.system)) payload.system = [{
|
|
1377
|
-
type: "text",
|
|
1378
|
-
text: searchContext
|
|
1379
|
-
}, ...payload.system];
|
|
1380
|
-
else payload.system = searchContext;
|
|
1381
|
-
} catch (error) {
|
|
1382
|
-
consola.warn("Web search failed, continuing without results:", error);
|
|
1383
|
-
}
|
|
1384
|
-
payload.tools = payload.tools?.filter((t) => t.name !== "web_search");
|
|
1385
|
-
if (payload.tools?.length === 0) payload.tools = void 0;
|
|
1386
|
-
}
|
|
1387
|
-
function extractUserQuery$1(messages) {
|
|
1388
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1389
|
-
const msg = messages[i];
|
|
1390
|
-
if (msg.role === "user") {
|
|
1391
|
-
if (typeof msg.content === "string") return msg.content;
|
|
1392
|
-
if (Array.isArray(msg.content)) {
|
|
1393
|
-
const text = msg.content.find((block) => "type" in block && block.type === "text");
|
|
1394
|
-
if (text && "text" in text) return text.text;
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1122
|
+
});
|
|
1397
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);
|
|
1398
1127
|
}
|
|
1399
1128
|
|
|
1400
1129
|
//#endregion
|
|
@@ -1476,7 +1205,7 @@ function detectAgentCall(input) {
|
|
|
1476
1205
|
if (!Array.isArray(input)) return false;
|
|
1477
1206
|
return input.some((item) => {
|
|
1478
1207
|
if ("role" in item && item.role === "assistant") return true;
|
|
1479
|
-
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;
|
|
1480
1209
|
return false;
|
|
1481
1210
|
});
|
|
1482
1211
|
}
|
|
@@ -1487,35 +1216,49 @@ function filterUnsupportedTools(payload) {
|
|
|
1487
1216
|
if (!isSupported) consola.debug(`Stripping unsupported tool type: ${tool.type}`);
|
|
1488
1217
|
return isSupported;
|
|
1489
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
|
+
}
|
|
1490
1226
|
return {
|
|
1491
1227
|
...payload,
|
|
1492
|
-
tools: supported.length > 0 ? supported : void 0
|
|
1228
|
+
tools: supported.length > 0 ? supported : void 0,
|
|
1229
|
+
tool_choice: toolChoice
|
|
1493
1230
|
};
|
|
1494
1231
|
}
|
|
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;
|
|
1236
|
+
}
|
|
1495
1237
|
|
|
1496
1238
|
//#endregion
|
|
1497
1239
|
//#region src/routes/responses/handler.ts
|
|
1498
1240
|
async function handleResponses(c) {
|
|
1499
1241
|
await checkRateLimit(state);
|
|
1500
1242
|
const payload = await c.req.json();
|
|
1501
|
-
consola.
|
|
1243
|
+
const debugEnabled = consola.level >= 4;
|
|
1244
|
+
if (debugEnabled) consola.debug("Responses request payload:", JSON.stringify(payload).slice(-400));
|
|
1502
1245
|
const selectedModel = state.models?.data.find((model) => model.id === payload.model);
|
|
1503
1246
|
consola.info("Token counting not yet supported for /responses endpoint");
|
|
1504
1247
|
if (state.manualApprove) await awaitApproval();
|
|
1505
1248
|
await injectWebSearchIfNeeded(payload);
|
|
1506
1249
|
if (isNullish(payload.max_output_tokens)) {
|
|
1507
1250
|
payload.max_output_tokens = selectedModel?.capabilities.limits.max_output_tokens;
|
|
1508
|
-
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));
|
|
1509
1252
|
}
|
|
1510
1253
|
const response = await createResponses(payload);
|
|
1511
1254
|
if (isNonStreaming(response)) {
|
|
1512
|
-
consola.debug("Non-streaming response:", JSON.stringify(response));
|
|
1255
|
+
if (debugEnabled) consola.debug("Non-streaming response:", JSON.stringify(response));
|
|
1513
1256
|
return c.json(response);
|
|
1514
1257
|
}
|
|
1515
1258
|
consola.debug("Streaming response");
|
|
1516
1259
|
return streamSSE(c, async (stream) => {
|
|
1517
1260
|
for await (const chunk of response) {
|
|
1518
|
-
consola.debug("Streaming chunk:", JSON.stringify(chunk));
|
|
1261
|
+
if (debugEnabled) consola.debug("Streaming chunk:", JSON.stringify(chunk));
|
|
1519
1262
|
if (chunk.data === "[DONE]") break;
|
|
1520
1263
|
if (!chunk.data) continue;
|
|
1521
1264
|
await stream.writeSSE({
|
|
@@ -1592,6 +1335,10 @@ searchRoutes.post("/", async (c) => {
|
|
|
1592
1335
|
//#region src/routes/token/route.ts
|
|
1593
1336
|
const tokenRoute = new Hono();
|
|
1594
1337
|
tokenRoute.get("/", (c) => {
|
|
1338
|
+
if (!state.showToken) return c.json({ error: {
|
|
1339
|
+
message: "Token endpoint disabled",
|
|
1340
|
+
type: "error"
|
|
1341
|
+
} }, 403);
|
|
1595
1342
|
return c.json({ token: state.copilotToken });
|
|
1596
1343
|
});
|
|
1597
1344
|
|
|
@@ -1630,18 +1377,35 @@ server.route("/v1/messages", messageRoutes);
|
|
|
1630
1377
|
|
|
1631
1378
|
//#endregion
|
|
1632
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
|
+
}
|
|
1633
1395
|
async function generateClaudeCodeCommand(serverUrl) {
|
|
1634
1396
|
invariant(state.models, "Models should be loaded by now");
|
|
1397
|
+
const supportedModels = filterModelsByEndpoint(state.models.data, "v1/messages");
|
|
1635
1398
|
const selectedModel = await consola.prompt("Select a model to use with Claude Code", {
|
|
1636
1399
|
type: "select",
|
|
1637
|
-
options:
|
|
1400
|
+
options: supportedModels.map((model) => model.id)
|
|
1638
1401
|
});
|
|
1639
1402
|
const selectedSmallModel = await consola.prompt("Select a small model to use with Claude Code", {
|
|
1640
1403
|
type: "select",
|
|
1641
|
-
options:
|
|
1404
|
+
options: supportedModels.map((model) => model.id)
|
|
1642
1405
|
});
|
|
1643
1406
|
const command = generateEnvScript({
|
|
1644
1407
|
ANTHROPIC_BASE_URL: serverUrl,
|
|
1408
|
+
ANTHROPIC_API_KEY: "dummy",
|
|
1645
1409
|
ANTHROPIC_AUTH_TOKEN: "dummy",
|
|
1646
1410
|
ANTHROPIC_MODEL: selectedModel,
|
|
1647
1411
|
ANTHROPIC_DEFAULT_SONNET_MODEL: selectedModel,
|
|
@@ -1660,15 +1424,17 @@ async function generateClaudeCodeCommand(serverUrl) {
|
|
|
1660
1424
|
}
|
|
1661
1425
|
async function generateCodexCommand(serverUrl) {
|
|
1662
1426
|
invariant(state.models, "Models should be loaded by now");
|
|
1663
|
-
const
|
|
1427
|
+
const supportedModels = filterModelsByEndpoint(state.models.data, "responses");
|
|
1428
|
+
const defaultCodexModel = supportedModels.find((model) => model.id === "gpt5.2-codex");
|
|
1664
1429
|
const selectedModel = defaultCodexModel ? defaultCodexModel.id : await consola.prompt("Select a model to use with Codex CLI", {
|
|
1665
1430
|
type: "select",
|
|
1666
|
-
options:
|
|
1431
|
+
options: supportedModels.map((model) => model.id)
|
|
1667
1432
|
});
|
|
1433
|
+
const quotedModel = JSON.stringify(selectedModel);
|
|
1668
1434
|
const command = generateEnvScript({
|
|
1669
1435
|
OPENAI_BASE_URL: `${serverUrl}/v1`,
|
|
1670
1436
|
OPENAI_API_KEY: "dummy"
|
|
1671
|
-
}, `codex -m ${
|
|
1437
|
+
}, `codex -m ${quotedModel}`);
|
|
1672
1438
|
try {
|
|
1673
1439
|
clipboard.writeSync(command);
|
|
1674
1440
|
consola.success("Copied Codex CLI command to clipboard!");
|
|
@@ -1704,6 +1470,7 @@ async function runServer(options) {
|
|
|
1704
1470
|
consola.box(`🌐 Usage Viewer: https://animeshkundu.github.io/github-router/dashboard.html?endpoint=${serverUrl}/usage`);
|
|
1705
1471
|
serve({
|
|
1706
1472
|
fetch: server.fetch,
|
|
1473
|
+
hostname: "127.0.0.1",
|
|
1707
1474
|
port: options.port
|
|
1708
1475
|
});
|
|
1709
1476
|
}
|
|
@@ -1776,15 +1543,26 @@ const start = defineCommand({
|
|
|
1776
1543
|
},
|
|
1777
1544
|
run({ args }) {
|
|
1778
1545
|
const rateLimitRaw = args["rate-limit"];
|
|
1779
|
-
|
|
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;
|
|
1780
1558
|
return runServer({
|
|
1781
|
-
port
|
|
1559
|
+
port,
|
|
1782
1560
|
verbose: args.verbose,
|
|
1783
|
-
accountType
|
|
1561
|
+
accountType,
|
|
1784
1562
|
manual: args.manual,
|
|
1785
1563
|
rateLimit,
|
|
1786
|
-
rateLimitWait
|
|
1787
|
-
githubToken
|
|
1564
|
+
rateLimitWait,
|
|
1565
|
+
githubToken,
|
|
1788
1566
|
claudeCode: args["claude-code"],
|
|
1789
1567
|
codex: args.codex,
|
|
1790
1568
|
showToken: args["show-token"],
|