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/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-04-01";
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 = errorText;
109
+ errorJson = void 0;
109
110
  }
110
- consola.error("HTTP error:", errorJson);
111
+ const message = resolveErrorMessage(errorJson, errorText);
112
+ consola.error("HTTP error:", errorJson ?? errorText);
111
113
  return c.json({ error: {
112
- message: errorText,
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
- while (true) {
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
- else await sleep(sleepDuration);
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
- if (execSync(`wmic process get ParentProcessId,Name | findstr "${ppid}"`, { stdio: "pipe" }).toString().toLowerCase().includes("powershell.exe")) return "powershell";
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
- consola.error("Failed to create chat completions", response);
780
- throw new HTTPError("Failed to create chat completions", response);
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.debug("Request payload:", JSON.stringify(payload).slice(-400));
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$2(response)) {
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$2 = (response) => Object.hasOwn(response, "choices");
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/routes/messages/utils.ts
862
- function mapOpenAIStopReasonToAnthropic(finishReason) {
863
- if (finishReason === null) return null;
864
- return {
865
- stop: "end_turn",
866
- length: "max_tokens",
867
- tool_calls: "tool_use",
868
- content_filter: "end_turn"
869
- }[finishReason];
870
- }
871
-
872
- //#endregion
873
- //#region src/routes/messages/non-stream-translation.ts
874
- function translateToOpenAI(payload) {
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
- model: translateModelName(payload.model),
877
- messages: translateAnthropicMessagesToOpenAI(payload.messages, payload.system),
878
- max_tokens: payload.max_tokens,
879
- stop: payload.stop_sequences,
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
- function translateModelName(model) {
889
- if (model.startsWith("claude-sonnet-4-")) return model.replace(/^claude-sonnet-4-.*/, "claude-sonnet-4");
890
- else if (model.startsWith("claude-opus-")) return model.replace(/^claude-opus-4-.*/, "claude-opus-4");
891
- return model;
892
- }
893
- function translateAnthropicMessagesToOpenAI(anthropicMessages, system) {
894
- const systemMessages = handleSystemPrompt(system);
895
- const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message));
896
- return [...systemMessages, ...otherMessages];
897
- }
898
- function handleSystemPrompt(system) {
899
- if (!system) return [];
900
- if (typeof system === "string") return [{
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
- return newMessages;
928
- }
929
- function handleAssistantMessage(message) {
930
- if (!Array.isArray(message.content)) return [{
931
- role: "assistant",
932
- content: mapContent(message.content)
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
- function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice) {
993
- if (!anthropicToolChoice) return;
994
- switch (anthropicToolChoice.type) {
995
- case "auto": return "auto";
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
- function translateToAnthropic(response) {
1008
- const allTextBlocks = [];
1009
- const allToolUseBlocks = [];
1010
- let stopReason = null;
1011
- stopReason = response.choices[0]?.finish_reason ?? stopReason;
1012
- for (const choice of response.choices) {
1013
- const textBlocks = getAnthropicTextBlocks(choice.message.content);
1014
- const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls);
1015
- allTextBlocks.push(...textBlocks);
1016
- allToolUseBlocks.push(...toolUseBlocks);
1017
- if (choice.finish_reason === "tool_calls" || stopReason === "stop") stopReason = choice.finish_reason;
1018
- }
1019
- return {
1020
- id: response.id,
1021
- type: "message",
1022
- role: "assistant",
1023
- model: response.model,
1024
- content: [...allTextBlocks, ...allToolUseBlocks],
1025
- stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
1026
- stop_sequence: null,
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
- function getAnthropicTextBlocks(messageContent) {
1035
- if (typeof messageContent === "string") return [{
1036
- type: "text",
1037
- text: messageContent
1038
- }];
1039
- if (Array.isArray(messageContent)) return messageContent.filter((part) => part.type === "text").map((part) => ({
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
- * Handles token counting for Anthropic messages
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
- try {
1062
- const anthropicBeta = c.req.header("anthropic-beta");
1063
- const anthropicPayload = await c.req.json();
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 anthropicPayload = await c.req.json();
1219
- consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
1220
- const openAIPayload = translateToOpenAI(anthropicPayload);
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 createChatCompletions(openAIPayload);
1224
- if (isNonStreaming$1(response)) {
1225
- consola.debug("Non-streaming response from Copilot:", JSON.stringify(response).slice(-400));
1226
- const anthropicResponse = translateToAnthropic(response);
1227
- consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
1228
- return c.json(anthropicResponse);
1229
- }
1230
- consola.debug("Streaming response from Copilot");
1231
- return streamSSE(c, async (stream) => {
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
- //#endregion
1352
- //#region src/services/copilot/web-search.ts
1353
- const MAX_SEARCHES_PER_SECOND = 3;
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.debug("Responses request payload:", JSON.stringify(payload).slice(-400));
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: state.models.data.map((model) => model.id)
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: state.models.data.map((model) => model.id)
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 defaultCodexModel = state.models.data.find((model) => model.id === "gpt5.2-codex");
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: state.models.data.map((model) => model.id)
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 ${selectedModel}`);
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
- const rateLimit = rateLimitRaw === void 0 ? void 0 : Number.parseInt(rateLimitRaw, 10);
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: Number.parseInt(args.port, 10),
1559
+ port,
1704
1560
  verbose: args.verbose,
1705
- accountType: args["account-type"],
1561
+ accountType,
1706
1562
  manual: args.manual,
1707
1563
  rateLimit,
1708
- rateLimitWait: args.wait,
1709
- githubToken: args["github-token"],
1564
+ rateLimitWait,
1565
+ githubToken,
1710
1566
  claudeCode: args["claude-code"],
1711
1567
  codex: args.codex,
1712
1568
  showToken: args["show-token"],