@ashsec/copilot-api 0.7.0 → 0.7.4

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
@@ -15,17 +15,18 @@ import { execSync } from "node:child_process";
15
15
  import process$1 from "node:process";
16
16
  import { Hono } from "hono";
17
17
  import { cors } from "hono/cors";
18
- import { logger } from "hono/logger";
19
18
  import { streamSSE } from "hono/streaming";
20
19
 
21
20
  //#region src/lib/paths.ts
22
21
  const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api");
23
22
  const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
24
23
  const AZURE_OPENAI_CONFIG_PATH = path.join(APP_DIR, "azure_openai_config");
24
+ const REPLACEMENTS_CONFIG_PATH = path.join(APP_DIR, "replacements.json");
25
25
  const PATHS = {
26
26
  APP_DIR,
27
27
  GITHUB_TOKEN_PATH,
28
- AZURE_OPENAI_CONFIG_PATH
28
+ AZURE_OPENAI_CONFIG_PATH,
29
+ REPLACEMENTS_CONFIG_PATH
29
30
  };
30
31
  async function ensurePaths() {
31
32
  await fs.mkdir(PATHS.APP_DIR, { recursive: true });
@@ -164,15 +165,15 @@ async function loadAzureOpenAIConfig() {
164
165
  const content = await fs.readFile(PATHS.AZURE_OPENAI_CONFIG_PATH, "utf8");
165
166
  if (!content.trim()) return null;
166
167
  const decoded = Buffer.from(content.trim(), "base64").toString("utf8");
167
- const config = JSON.parse(decoded);
168
- if (!config.endpoint || !config.apiKey) return null;
169
- return config;
168
+ const config$1 = JSON.parse(decoded);
169
+ if (!config$1.endpoint || !config$1.apiKey) return null;
170
+ return config$1;
170
171
  } catch {
171
172
  return null;
172
173
  }
173
174
  }
174
- async function saveAzureOpenAIConfig(config) {
175
- const encoded = Buffer.from(JSON.stringify(config)).toString("base64");
175
+ async function saveAzureOpenAIConfig(config$1) {
176
+ const encoded = Buffer.from(JSON.stringify(config$1)).toString("base64");
176
177
  await fs.writeFile(PATHS.AZURE_OPENAI_CONFIG_PATH, encoded, "utf8");
177
178
  await fs.chmod(PATHS.AZURE_OPENAI_CONFIG_PATH, 384);
178
179
  consola.success("Azure OpenAI configuration saved");
@@ -192,12 +193,12 @@ async function promptAzureOpenAISetup() {
192
193
  consola.warn("No API key provided, skipping Azure OpenAI setup");
193
194
  return null;
194
195
  }
195
- const config = {
196
+ const config$1 = {
196
197
  endpoint: endpoint.trim().replace(/\/$/, ""),
197
198
  apiKey: apiKey.trim()
198
199
  };
199
- await saveAzureOpenAIConfig(config);
200
- return config;
200
+ await saveAzureOpenAIConfig(config$1);
201
+ return config$1;
201
202
  }
202
203
  function isAzureOpenAIModel(modelId) {
203
204
  return modelId.startsWith(AZURE_OPENAI_MODEL_PREFIX);
@@ -206,10 +207,74 @@ function getAzureDeploymentName(modelId) {
206
207
  return modelId.slice(13);
207
208
  }
208
209
 
210
+ //#endregion
211
+ //#region src/lib/retry-fetch.ts
212
+ const DEFAULT_MAX_RETRIES = 3;
213
+ const DEFAULT_BASE_DELAY_MS = 1e3;
214
+ /**
215
+ * Check if an error is retryable (transient network error)
216
+ */
217
+ function isRetryableError(error) {
218
+ if (!(error instanceof Error)) return false;
219
+ const message = error.message.toLowerCase();
220
+ const causeMessage = error.cause instanceof Error ? error.cause.message.toLowerCase() : "";
221
+ return [
222
+ "fetch failed",
223
+ "other side closed",
224
+ "connection reset",
225
+ "econnreset",
226
+ "socket hang up",
227
+ "etimedout",
228
+ "econnrefused",
229
+ "network error",
230
+ "aborted",
231
+ "timeout"
232
+ ].some((pattern) => message.includes(pattern) || causeMessage.includes(pattern));
233
+ }
234
+ /**
235
+ * Check if an HTTP response status is retryable
236
+ */
237
+ function isRetryableStatus(status) {
238
+ return status === 408 || status === 429 || status >= 500 && status <= 599;
239
+ }
240
+ /**
241
+ * Fetch with automatic retry on transient failures
242
+ */
243
+ async function fetchWithRetry(input, init, options = {}) {
244
+ const { maxRetries = DEFAULT_MAX_RETRIES, baseDelayMs = DEFAULT_BASE_DELAY_MS } = options;
245
+ let lastError;
246
+ let lastResponse;
247
+ for (let attempt = 0; attempt <= maxRetries; attempt++) try {
248
+ const headers = new Headers(init?.headers);
249
+ headers.set("Connection", "close");
250
+ const response = await fetch(input, {
251
+ ...init,
252
+ headers,
253
+ keepalive: false
254
+ });
255
+ if (isRetryableStatus(response.status) && attempt < maxRetries) {
256
+ lastResponse = response;
257
+ const delayMs = baseDelayMs * 2 ** attempt;
258
+ consola.warn(`HTTP ${response.status} (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delayMs}ms`);
259
+ await sleep(delayMs);
260
+ continue;
261
+ }
262
+ return response;
263
+ } catch (error) {
264
+ lastError = error;
265
+ if (!isRetryableError(error) || attempt === maxRetries) throw error;
266
+ const delayMs = baseDelayMs * 2 ** attempt;
267
+ consola.warn(`Fetch failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delayMs}ms:`, lastError.message);
268
+ await sleep(delayMs);
269
+ }
270
+ if (lastResponse) return lastResponse;
271
+ throw lastError;
272
+ }
273
+
209
274
  //#endregion
210
275
  //#region src/services/azure-openai/create-chat-completions.ts
211
276
  const AZURE_API_VERSION = "2024-10-21";
212
- async function createAzureOpenAIChatCompletions(config, payload) {
277
+ async function createAzureOpenAIChatCompletions(config$1, payload) {
213
278
  const deploymentName = getAzureDeploymentName(payload.model);
214
279
  const { max_tokens,...restPayload } = payload;
215
280
  const azurePayload = {
@@ -217,10 +282,10 @@ async function createAzureOpenAIChatCompletions(config, payload) {
217
282
  model: deploymentName,
218
283
  ...max_tokens != null && { max_completion_tokens: max_tokens }
219
284
  };
220
- const response = await fetch(`${config.endpoint}/openai/deployments/${deploymentName}/chat/completions?api-version=${AZURE_API_VERSION}`, {
285
+ const response = await fetchWithRetry(`${config$1.endpoint}/openai/deployments/${deploymentName}/chat/completions?api-version=${AZURE_API_VERSION}`, {
221
286
  method: "POST",
222
287
  headers: {
223
- "api-key": config.apiKey,
288
+ "api-key": config$1.apiKey,
224
289
  "Content-Type": "application/json"
225
290
  },
226
291
  body: JSON.stringify(azurePayload)
@@ -236,10 +301,10 @@ async function createAzureOpenAIChatCompletions(config, payload) {
236
301
  //#endregion
237
302
  //#region src/services/azure-openai/get-models.ts
238
303
  const AZURE_DEPLOYMENTS_API_VERSION = "2022-12-01";
239
- async function getAzureOpenAIDeployments(config) {
304
+ async function getAzureOpenAIDeployments(config$1) {
240
305
  try {
241
- const response = await fetch(`${config.endpoint}/openai/deployments?api-version=${AZURE_DEPLOYMENTS_API_VERSION}`, { headers: {
242
- "api-key": config.apiKey,
306
+ const response = await fetchWithRetry(`${config$1.endpoint}/openai/deployments?api-version=${AZURE_DEPLOYMENTS_API_VERSION}`, { headers: {
307
+ "api-key": config$1.apiKey,
243
308
  "Content-Type": "application/json"
244
309
  } });
245
310
  if (!response.ok) {
@@ -265,8 +330,20 @@ async function getAzureOpenAIDeployments(config) {
265
330
  //#endregion
266
331
  //#region src/services/copilot/get-models.ts
267
332
  const getModels = async () => {
268
- const response = await fetch(`${copilotBaseUrl(state)}/models`, { headers: copilotHeaders(state) });
269
- if (!response.ok) throw new HTTPError("Failed to get models", response);
333
+ const url = `${copilotBaseUrl(state)}/models`;
334
+ const response = await fetchWithRetry(url, { headers: copilotHeaders(state) });
335
+ if (!response.ok) {
336
+ const errorBody = await response.text();
337
+ let errorDetails;
338
+ try {
339
+ const parsed = JSON.parse(errorBody);
340
+ errorDetails = JSON.stringify(parsed, null, 2);
341
+ } catch {
342
+ errorDetails = errorBody || "(empty response)";
343
+ }
344
+ consola.error(`Failed to get models from ${url}\nStatus: ${response.status} ${response.statusText}\nResponse: ${errorDetails}`);
345
+ throw new HTTPError(`Failed to get models: ${response.status} ${response.statusText}`, response);
346
+ }
270
347
  return await response.json();
271
348
  };
272
349
 
@@ -297,7 +374,16 @@ const sleep = (ms) => new Promise((resolve) => {
297
374
  });
298
375
  const isNullish = (value) => value === null || value === void 0;
299
376
  async function cacheModels() {
300
- state.models = await getModels();
377
+ try {
378
+ state.models = await getModels();
379
+ } catch (error) {
380
+ consola.error("Failed to fetch and cache models. This could be due to:");
381
+ consola.error(" - Invalid or expired Copilot token");
382
+ consola.error(" - Network connectivity issues");
383
+ consola.error(" - GitHub Copilot service unavailable");
384
+ consola.error(" - Account type mismatch (try --account-type=individual or --account-type=business)");
385
+ throw error;
386
+ }
301
387
  }
302
388
  const cacheVSCodeVersion = async () => {
303
389
  const response = await getVSCodeVersion();
@@ -305,16 +391,16 @@ const cacheVSCodeVersion = async () => {
305
391
  consola.info(`Using VSCode version: ${response}`);
306
392
  };
307
393
  async function setupAzureOpenAI() {
308
- let config = await loadAzureOpenAIConfig();
309
- if (!config) config = await promptAzureOpenAISetup();
310
- if (!config) {
394
+ let config$1 = await loadAzureOpenAIConfig();
395
+ if (!config$1) config$1 = await promptAzureOpenAISetup();
396
+ if (!config$1) {
311
397
  consola.info("Azure OpenAI not configured");
312
398
  return;
313
399
  }
314
- state.azureOpenAIConfig = config;
400
+ state.azureOpenAIConfig = config$1;
315
401
  consola.info("Azure OpenAI configuration loaded");
316
402
  try {
317
- const deployments = await getAzureOpenAIDeployments(config);
403
+ const deployments = await getAzureOpenAIDeployments(config$1);
318
404
  state.azureOpenAIDeployments = deployments;
319
405
  if (deployments.length > 0) consola.info(`Loaded ${deployments.length} Azure OpenAI deployment(s):\n${deployments.map((d) => `- ${d.id} (${d.model})`).join("\n")}`);
320
406
  else consola.warn("No Azure OpenAI deployments found");
@@ -488,6 +574,377 @@ const checkUsage = defineCommand({
488
574
  }
489
575
  });
490
576
 
577
+ //#endregion
578
+ //#region src/lib/auto-replace.ts
579
+ const SYSTEM_REPLACEMENTS = [{
580
+ id: "system-anthropic-billing",
581
+ pattern: "x-anthropic-billing-header:[^\n]*\n?",
582
+ replacement: "",
583
+ isRegex: true,
584
+ enabled: true,
585
+ isSystem: true
586
+ }];
587
+ let userReplacements = [];
588
+ let isLoaded = false;
589
+ /**
590
+ * Load user replacements from disk
591
+ */
592
+ async function loadReplacements() {
593
+ try {
594
+ const data = await fs.readFile(PATHS.REPLACEMENTS_CONFIG_PATH);
595
+ userReplacements = JSON.parse(data).filter((r) => !r.isSystem);
596
+ isLoaded = true;
597
+ consola.debug(`Loaded ${userReplacements.length} user replacement rules`);
598
+ } catch {
599
+ userReplacements = [];
600
+ isLoaded = true;
601
+ }
602
+ }
603
+ /**
604
+ * Save user replacements to disk
605
+ */
606
+ async function saveReplacements() {
607
+ try {
608
+ await fs.writeFile(PATHS.REPLACEMENTS_CONFIG_PATH, JSON.stringify(userReplacements, null, 2), "utf8");
609
+ consola.debug(`Saved ${userReplacements.length} user replacement rules`);
610
+ } catch (error) {
611
+ consola.error("Failed to save replacement rules:", error);
612
+ throw error;
613
+ }
614
+ }
615
+ /**
616
+ * Ensure replacements are loaded before accessing
617
+ */
618
+ async function ensureLoaded() {
619
+ if (!isLoaded) await loadReplacements();
620
+ }
621
+ /**
622
+ * Get all replacement rules (system + user)
623
+ */
624
+ async function getAllReplacements() {
625
+ await ensureLoaded();
626
+ return [...SYSTEM_REPLACEMENTS, ...userReplacements];
627
+ }
628
+ /**
629
+ * Get only user-configurable replacements
630
+ */
631
+ async function getUserReplacements() {
632
+ await ensureLoaded();
633
+ return userReplacements;
634
+ }
635
+ /**
636
+ * Add a new user replacement rule
637
+ */
638
+ async function addReplacement(pattern, replacement, isRegex = false) {
639
+ await ensureLoaded();
640
+ const rule = {
641
+ id: `user-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
642
+ pattern,
643
+ replacement,
644
+ isRegex,
645
+ enabled: true,
646
+ isSystem: false
647
+ };
648
+ userReplacements.push(rule);
649
+ await saveReplacements();
650
+ consola.info(`Added replacement rule: "${pattern}" -> "${replacement}"`);
651
+ return rule;
652
+ }
653
+ /**
654
+ * Remove a user replacement rule by ID
655
+ */
656
+ async function removeReplacement(id) {
657
+ await ensureLoaded();
658
+ const rule = userReplacements.find((r) => r.id === id);
659
+ if (!rule) return false;
660
+ if (rule.isSystem) {
661
+ consola.warn("Cannot remove system replacement rule");
662
+ return false;
663
+ }
664
+ userReplacements = userReplacements.filter((r) => r.id !== id);
665
+ await saveReplacements();
666
+ consola.info(`Removed replacement rule: ${id}`);
667
+ return true;
668
+ }
669
+ /**
670
+ * Toggle a replacement rule on/off
671
+ */
672
+ async function toggleReplacement(id) {
673
+ await ensureLoaded();
674
+ const userRule = userReplacements.find((r) => r.id === id);
675
+ if (userRule) {
676
+ userRule.enabled = !userRule.enabled;
677
+ await saveReplacements();
678
+ consola.info(`Toggled replacement rule ${id}: ${userRule.enabled ? "enabled" : "disabled"}`);
679
+ return userRule;
680
+ }
681
+ if (SYSTEM_REPLACEMENTS.find((r) => r.id === id)) {
682
+ consola.warn("Cannot toggle system replacement rule");
683
+ return null;
684
+ }
685
+ return null;
686
+ }
687
+ /**
688
+ * Clear all user replacements
689
+ */
690
+ async function clearUserReplacements() {
691
+ userReplacements = [];
692
+ await saveReplacements();
693
+ consola.info("Cleared all user replacement rules");
694
+ }
695
+ /**
696
+ * Apply a single replacement rule to text
697
+ */
698
+ function applyRule(text, rule) {
699
+ if (!rule.enabled) return text;
700
+ if (rule.isRegex) try {
701
+ const regex = new RegExp(rule.pattern, "g");
702
+ return text.replace(regex, rule.replacement);
703
+ } catch {
704
+ consola.warn(`Invalid regex pattern in rule ${rule.id}: ${rule.pattern}`);
705
+ return text;
706
+ }
707
+ return text.split(rule.pattern).join(rule.replacement);
708
+ }
709
+ /**
710
+ * Apply all replacement rules to text
711
+ */
712
+ async function applyReplacements(text) {
713
+ let result = text;
714
+ const allRules = await getAllReplacements();
715
+ for (const rule of allRules) {
716
+ const before = result;
717
+ result = applyRule(result, rule);
718
+ if (before !== result) consola.debug(`Applied replacement rule: ${rule.id}`);
719
+ }
720
+ return result;
721
+ }
722
+ /**
723
+ * Apply replacements to a chat completions payload
724
+ * This modifies message content in place
725
+ */
726
+ async function applyReplacementsToPayload(payload) {
727
+ const processedMessages = await Promise.all(payload.messages.map(async (message) => {
728
+ if (typeof message.content === "string") return {
729
+ ...message,
730
+ content: await applyReplacements(message.content)
731
+ };
732
+ if (Array.isArray(message.content)) return {
733
+ ...message,
734
+ content: await Promise.all(message.content.map(async (part) => {
735
+ if (typeof part === "object" && part.type === "text" && part.text) return {
736
+ ...part,
737
+ text: await applyReplacements(part.text)
738
+ };
739
+ return part;
740
+ }))
741
+ };
742
+ return message;
743
+ }));
744
+ return {
745
+ ...payload,
746
+ messages: processedMessages
747
+ };
748
+ }
749
+
750
+ //#endregion
751
+ //#region src/config.ts
752
+ function formatRule(rule, index) {
753
+ const status = rule.enabled ? "✓" : "✗";
754
+ const type = rule.isRegex ? "regex" : "string";
755
+ const system = rule.isSystem ? " [system]" : "";
756
+ const replacement = rule.replacement || "(empty)";
757
+ return `${index + 1}. [${status}] (${type})${system} "${rule.pattern}" → "${replacement}"`;
758
+ }
759
+ async function listReplacements() {
760
+ const all = await getAllReplacements();
761
+ if (all.length === 0) {
762
+ consola.info("No replacement rules configured.");
763
+ return;
764
+ }
765
+ consola.info("\n📋 Replacement Rules:\n");
766
+ for (const [i, element] of all.entries()) console.log(formatRule(element, i));
767
+ console.log();
768
+ }
769
+ async function addNewReplacement() {
770
+ const matchType = await consola.prompt("Match type:", {
771
+ type: "select",
772
+ options: [{
773
+ label: "String (exact match)",
774
+ value: "string"
775
+ }, {
776
+ label: "Regex (regular expression)",
777
+ value: "regex"
778
+ }]
779
+ });
780
+ if (typeof matchType === "symbol") {
781
+ consola.info("Cancelled.");
782
+ return;
783
+ }
784
+ const pattern = await consola.prompt("Pattern to match:", { type: "text" });
785
+ if (typeof pattern === "symbol" || !pattern) {
786
+ consola.info("Cancelled.");
787
+ return;
788
+ }
789
+ if (matchType === "regex") try {
790
+ new RegExp(pattern);
791
+ } catch {
792
+ consola.error(`Invalid regex pattern: ${pattern}`);
793
+ return;
794
+ }
795
+ const replacement = await consola.prompt("Replacement text (leave empty to delete matches):", {
796
+ type: "text",
797
+ default: ""
798
+ });
799
+ if (typeof replacement === "symbol") {
800
+ consola.info("Cancelled.");
801
+ return;
802
+ }
803
+ const rule = await addReplacement(pattern, replacement, matchType === "regex");
804
+ consola.success(`Added rule: ${rule.id}`);
805
+ }
806
+ async function removeExistingReplacement() {
807
+ const userRules = await getUserReplacements();
808
+ if (userRules.length === 0) {
809
+ consola.info("No user rules to remove.");
810
+ return;
811
+ }
812
+ const options = userRules.map((rule, i) => ({
813
+ label: formatRule(rule, i),
814
+ value: rule.id
815
+ }));
816
+ const selected = await consola.prompt("Select rule to remove:", {
817
+ type: "select",
818
+ options
819
+ });
820
+ if (typeof selected === "symbol") {
821
+ consola.info("Cancelled.");
822
+ return;
823
+ }
824
+ if (await removeReplacement(selected)) consola.success("Rule removed.");
825
+ else consola.error("Failed to remove rule.");
826
+ }
827
+ async function toggleExistingReplacement() {
828
+ const userRules = await getUserReplacements();
829
+ if (userRules.length === 0) {
830
+ consola.info("No user rules to toggle.");
831
+ return;
832
+ }
833
+ const options = userRules.map((rule$1, i) => ({
834
+ label: formatRule(rule$1, i),
835
+ value: rule$1.id
836
+ }));
837
+ const selected = await consola.prompt("Select rule to toggle:", {
838
+ type: "select",
839
+ options
840
+ });
841
+ if (typeof selected === "symbol") {
842
+ consola.info("Cancelled.");
843
+ return;
844
+ }
845
+ const rule = await toggleReplacement(selected);
846
+ if (rule) consola.success(`Rule ${rule.enabled ? "enabled" : "disabled"}.`);
847
+ else consola.error("Failed to toggle rule.");
848
+ }
849
+ async function testReplacements() {
850
+ const testText = await consola.prompt("Enter text to test replacements:", { type: "text" });
851
+ if (typeof testText === "symbol" || !testText) {
852
+ consola.info("Cancelled.");
853
+ return;
854
+ }
855
+ const result = await applyReplacements(testText);
856
+ consola.info("\n📝 Original:");
857
+ console.log(testText);
858
+ consola.info("\n✨ After replacements:");
859
+ console.log(result);
860
+ console.log();
861
+ }
862
+ async function clearAllReplacements() {
863
+ if (await consola.prompt("Are you sure you want to clear all user replacements?", {
864
+ type: "confirm",
865
+ initial: false
866
+ })) {
867
+ await clearUserReplacements();
868
+ consola.success("All user replacements cleared.");
869
+ } else consola.info("Cancelled.");
870
+ }
871
+ async function mainMenu() {
872
+ consola.info(`\n🔧 Copilot API - Replacement Configuration`);
873
+ consola.info(`Config file: ${PATHS.REPLACEMENTS_CONFIG_PATH}\n`);
874
+ let running = true;
875
+ while (running) {
876
+ const action = await consola.prompt("What would you like to do?", {
877
+ type: "select",
878
+ options: [
879
+ {
880
+ label: "📋 List all rules",
881
+ value: "list"
882
+ },
883
+ {
884
+ label: "➕ Add new rule",
885
+ value: "add"
886
+ },
887
+ {
888
+ label: "➖ Remove rule",
889
+ value: "remove"
890
+ },
891
+ {
892
+ label: "🔄 Toggle rule on/off",
893
+ value: "toggle"
894
+ },
895
+ {
896
+ label: "🧪 Test replacements",
897
+ value: "test"
898
+ },
899
+ {
900
+ label: "🗑️ Clear all user rules",
901
+ value: "clear"
902
+ },
903
+ {
904
+ label: "🚪 Exit",
905
+ value: "exit"
906
+ }
907
+ ]
908
+ });
909
+ if (typeof action === "symbol") break;
910
+ switch (action) {
911
+ case "list":
912
+ await listReplacements();
913
+ break;
914
+ case "add":
915
+ await addNewReplacement();
916
+ break;
917
+ case "remove":
918
+ await removeExistingReplacement();
919
+ break;
920
+ case "toggle":
921
+ await toggleExistingReplacement();
922
+ break;
923
+ case "test":
924
+ await testReplacements();
925
+ break;
926
+ case "clear":
927
+ await clearAllReplacements();
928
+ break;
929
+ case "exit":
930
+ running = false;
931
+ break;
932
+ default: break;
933
+ }
934
+ }
935
+ consola.info("Goodbye! 👋");
936
+ }
937
+ const config = defineCommand({
938
+ meta: {
939
+ name: "config",
940
+ description: "Configure replacement rules interactively"
941
+ },
942
+ run: async () => {
943
+ await ensurePaths();
944
+ await mainMenu();
945
+ }
946
+ });
947
+
491
948
  //#endregion
492
949
  //#region src/debug.ts
493
950
  async function getPackageVersion() {
@@ -661,6 +1118,84 @@ function generateEnvScript(envVars, commandToRun = "") {
661
1118
  return commandBlock || commandToRun;
662
1119
  }
663
1120
 
1121
+ //#endregion
1122
+ //#region src/lib/request-logger.ts
1123
+ const REQUEST_CONTEXT_KEY = "requestContext";
1124
+ const colors = {
1125
+ reset: "\x1B[0m",
1126
+ dim: "\x1B[2m",
1127
+ bold: "\x1B[1m",
1128
+ cyan: "\x1B[36m",
1129
+ green: "\x1B[32m",
1130
+ yellow: "\x1B[33m",
1131
+ red: "\x1B[31m",
1132
+ magenta: "\x1B[35m",
1133
+ blue: "\x1B[34m",
1134
+ white: "\x1B[37m",
1135
+ gray: "\x1B[90m"
1136
+ };
1137
+ /**
1138
+ * Get the current time formatted as HH:MM:SS
1139
+ */
1140
+ function getTimeString() {
1141
+ return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", {
1142
+ hour12: false,
1143
+ hour: "2-digit",
1144
+ minute: "2-digit",
1145
+ second: "2-digit"
1146
+ });
1147
+ }
1148
+ /**
1149
+ * Get status color based on HTTP status code
1150
+ */
1151
+ function getStatusColor(status) {
1152
+ if (status >= 500) return colors.red;
1153
+ if (status >= 400) return colors.yellow;
1154
+ if (status >= 300) return colors.cyan;
1155
+ return colors.green;
1156
+ }
1157
+ /**
1158
+ * Set request context for logging
1159
+ */
1160
+ function setRequestContext(c, ctx) {
1161
+ const existing = c.get(REQUEST_CONTEXT_KEY);
1162
+ if (existing) c.set(REQUEST_CONTEXT_KEY, {
1163
+ ...existing,
1164
+ ...ctx
1165
+ });
1166
+ }
1167
+ /**
1168
+ * Custom request logger middleware
1169
+ */
1170
+ async function requestLogger(c, next) {
1171
+ const startTime = Date.now();
1172
+ const method = c.req.method;
1173
+ const path$1 = c.req.path + (c.req.raw.url.includes("?") ? "?" + c.req.raw.url.split("?")[1] : "");
1174
+ c.set(REQUEST_CONTEXT_KEY, { startTime });
1175
+ await next();
1176
+ const ctx = c.get(REQUEST_CONTEXT_KEY);
1177
+ const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
1178
+ const status = c.res.status;
1179
+ const statusColor = getStatusColor(status);
1180
+ const lines = [];
1181
+ lines.push(`${colors.dim}${"─".repeat(60)}${colors.reset}`);
1182
+ const statusBadge = `${statusColor}${status}${colors.reset}`;
1183
+ const durationStr = `${colors.cyan}${duration}s${colors.reset}`;
1184
+ lines.push(`${colors.bold}${method}${colors.reset} ${path$1} ${statusBadge} ${durationStr}`);
1185
+ if (ctx?.provider && ctx?.model) {
1186
+ const providerColor = ctx.provider === "Azure OpenAI" ? colors.blue : colors.magenta;
1187
+ lines.push(` ${colors.gray}Provider:${colors.reset} ${providerColor}${ctx.provider}${colors.reset} ${colors.gray}->${colors.reset} ${colors.white}${ctx.model}${colors.reset}`);
1188
+ }
1189
+ if (ctx?.inputTokens !== void 0 || ctx?.outputTokens !== void 0) {
1190
+ const tokenParts = [];
1191
+ if (ctx.inputTokens !== void 0) tokenParts.push(`${colors.gray}Input:${colors.reset} ${colors.yellow}${ctx.inputTokens.toLocaleString()}${colors.reset}`);
1192
+ if (ctx.outputTokens !== void 0) tokenParts.push(`${colors.gray}Output:${colors.reset} ${colors.green}${ctx.outputTokens.toLocaleString()}${colors.reset}`);
1193
+ lines.push(` ${tokenParts.join(" ")}`);
1194
+ }
1195
+ lines.push(` ${colors.dim}${getTimeString()}${colors.reset}`);
1196
+ console.log(lines.join("\n"));
1197
+ }
1198
+
664
1199
  //#endregion
665
1200
  //#region src/lib/approval.ts
666
1201
  const awaitApproval = async () => {
@@ -900,7 +1435,7 @@ const createChatCompletions = async (payload) => {
900
1435
  ...copilotHeaders(state, enableVision),
901
1436
  "X-Initiator": isAgentCall ? "agent" : "user"
902
1437
  };
903
- const response = await fetch(`${copilotBaseUrl(state)}/chat/completions`, {
1438
+ const response = await fetchWithRetry(`${copilotBaseUrl(state)}/chat/completions`, {
904
1439
  method: "POST",
905
1440
  headers,
906
1441
  body: JSON.stringify(payload)
@@ -917,32 +1452,50 @@ const createChatCompletions = async (payload) => {
917
1452
  //#region src/routes/chat-completions/handler.ts
918
1453
  async function handleCompletion$1(c) {
919
1454
  await checkRateLimit(state);
920
- let payload = await c.req.json();
1455
+ const rawPayload = await c.req.json();
1456
+ let payload = await applyReplacementsToPayload(rawPayload);
921
1457
  consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
922
1458
  if (isAzureOpenAIModel(payload.model)) {
923
1459
  if (!state.azureOpenAIConfig) return c.json({ error: "Azure OpenAI not configured" }, 500);
924
- consola.info(`Routing to Azure OpenAI -> ${payload.model}`);
1460
+ setRequestContext(c, {
1461
+ provider: "Azure OpenAI",
1462
+ model: payload.model
1463
+ });
925
1464
  if (state.manualApprove) await awaitApproval();
926
1465
  const response$1 = await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, payload);
927
1466
  if (isNonStreaming$1(response$1)) {
928
1467
  consola.debug("Non-streaming response:", JSON.stringify(response$1));
1468
+ if (response$1.usage) setRequestContext(c, {
1469
+ inputTokens: response$1.usage.prompt_tokens,
1470
+ outputTokens: response$1.usage.completion_tokens
1471
+ });
929
1472
  return c.json(response$1);
930
1473
  }
931
1474
  consola.debug("Streaming response");
932
1475
  return streamSSE(c, async (stream) => {
933
1476
  for await (const chunk of response$1) {
934
1477
  consola.debug("Streaming chunk:", JSON.stringify(chunk));
1478
+ if (chunk.data && chunk.data !== "[DONE]") {
1479
+ const parsed = JSON.parse(chunk.data);
1480
+ if (parsed.usage) setRequestContext(c, {
1481
+ inputTokens: parsed.usage.prompt_tokens,
1482
+ outputTokens: parsed.usage.completion_tokens
1483
+ });
1484
+ }
935
1485
  await stream.writeSSE(chunk);
936
1486
  }
937
1487
  });
938
1488
  }
939
- consola.info(`Routing to Copilot -> ${payload.model}`);
1489
+ setRequestContext(c, {
1490
+ provider: "Copilot",
1491
+ model: payload.model
1492
+ });
940
1493
  const selectedModel = state.models?.data.find((model) => model.id === payload.model);
941
1494
  try {
942
1495
  if (selectedModel) {
943
1496
  const tokenCount = await getTokenCount(payload, selectedModel);
944
- consola.info("Current token count:", tokenCount);
945
- } else consola.warn("No model selected, skipping token count calculation");
1497
+ setRequestContext(c, { inputTokens: tokenCount });
1498
+ }
946
1499
  } catch (error) {
947
1500
  consola.warn("Failed to calculate token count:", error);
948
1501
  }
@@ -957,12 +1510,23 @@ async function handleCompletion$1(c) {
957
1510
  const response = await createChatCompletions(payload);
958
1511
  if (isNonStreaming$1(response)) {
959
1512
  consola.debug("Non-streaming response:", JSON.stringify(response));
1513
+ if (response.usage) setRequestContext(c, {
1514
+ inputTokens: response.usage.prompt_tokens,
1515
+ outputTokens: response.usage.completion_tokens
1516
+ });
960
1517
  return c.json(response);
961
1518
  }
962
1519
  consola.debug("Streaming response");
963
1520
  return streamSSE(c, async (stream) => {
964
1521
  for await (const chunk of response) {
965
1522
  consola.debug("Streaming chunk:", JSON.stringify(chunk));
1523
+ if (chunk.data && chunk.data !== "[DONE]") {
1524
+ const parsed = JSON.parse(chunk.data);
1525
+ if (parsed.usage) setRequestContext(c, {
1526
+ inputTokens: parsed.usage.prompt_tokens,
1527
+ outputTokens: parsed.usage.completion_tokens
1528
+ });
1529
+ }
966
1530
  await stream.writeSSE(chunk);
967
1531
  }
968
1532
  });
@@ -984,7 +1548,7 @@ completionRoutes.post("/", async (c) => {
984
1548
  //#region src/services/copilot/create-embeddings.ts
985
1549
  const createEmbeddings = async (payload) => {
986
1550
  if (!state.copilotToken) throw new Error("Copilot token not found");
987
- const response = await fetch(`${copilotBaseUrl(state)}/embeddings`, {
1551
+ const response = await fetchWithRetry(`${copilotBaseUrl(state)}/embeddings`, {
988
1552
  method: "POST",
989
1553
  headers: copilotHeaders(state),
990
1554
  body: JSON.stringify(payload)
@@ -1366,15 +1930,23 @@ async function handleCompletion(c) {
1366
1930
  await checkRateLimit(state);
1367
1931
  const anthropicPayload = await c.req.json();
1368
1932
  consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
1369
- const openAIPayload = translateToOpenAI(anthropicPayload);
1933
+ const translatedPayload = translateToOpenAI(anthropicPayload);
1934
+ const openAIPayload = await applyReplacementsToPayload(translatedPayload);
1370
1935
  consola.debug("Translated OpenAI request payload:", JSON.stringify(openAIPayload));
1371
1936
  if (state.manualApprove) await awaitApproval();
1372
1937
  if (isAzureOpenAIModel(openAIPayload.model)) {
1373
1938
  if (!state.azureOpenAIConfig) return c.json({ error: "Azure OpenAI not configured" }, 500);
1374
- consola.info(`Routing to Azure OpenAI -> ${openAIPayload.model}`);
1939
+ setRequestContext(c, {
1940
+ provider: "Azure OpenAI",
1941
+ model: openAIPayload.model
1942
+ });
1375
1943
  const response$1 = await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, openAIPayload);
1376
1944
  if (isNonStreaming(response$1)) {
1377
1945
  consola.debug("Non-streaming response from Azure OpenAI:", JSON.stringify(response$1).slice(-400));
1946
+ if (response$1.usage) setRequestContext(c, {
1947
+ inputTokens: response$1.usage.prompt_tokens,
1948
+ outputTokens: response$1.usage.completion_tokens
1949
+ });
1378
1950
  const anthropicResponse = translateToAnthropic(response$1);
1379
1951
  consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
1380
1952
  return c.json(anthropicResponse);
@@ -1392,6 +1964,10 @@ async function handleCompletion(c) {
1392
1964
  if (rawEvent.data === "[DONE]") break;
1393
1965
  if (!rawEvent.data) continue;
1394
1966
  const chunk = JSON.parse(rawEvent.data);
1967
+ if (chunk.usage) setRequestContext(c, {
1968
+ inputTokens: chunk.usage.prompt_tokens,
1969
+ outputTokens: chunk.usage.completion_tokens
1970
+ });
1395
1971
  const events$1 = translateChunkToAnthropicEvents(chunk, streamState);
1396
1972
  for (const event of events$1) {
1397
1973
  consola.debug("Translated Anthropic event:", JSON.stringify(event));
@@ -1403,10 +1979,17 @@ async function handleCompletion(c) {
1403
1979
  }
1404
1980
  });
1405
1981
  }
1406
- consola.info(`Routing to Copilot -> ${openAIPayload.model}`);
1982
+ setRequestContext(c, {
1983
+ provider: "Copilot",
1984
+ model: openAIPayload.model
1985
+ });
1407
1986
  const response = await createChatCompletions(openAIPayload);
1408
1987
  if (isNonStreaming(response)) {
1409
1988
  consola.debug("Non-streaming response from Copilot:", JSON.stringify(response).slice(-400));
1989
+ if (response.usage) setRequestContext(c, {
1990
+ inputTokens: response.usage.prompt_tokens,
1991
+ outputTokens: response.usage.completion_tokens
1992
+ });
1410
1993
  const anthropicResponse = translateToAnthropic(response);
1411
1994
  consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
1412
1995
  return c.json(anthropicResponse);
@@ -1424,6 +2007,10 @@ async function handleCompletion(c) {
1424
2007
  if (rawEvent.data === "[DONE]") break;
1425
2008
  if (!rawEvent.data) continue;
1426
2009
  const chunk = JSON.parse(rawEvent.data);
2010
+ if (chunk.usage) setRequestContext(c, {
2011
+ inputTokens: chunk.usage.prompt_tokens,
2012
+ outputTokens: chunk.usage.completion_tokens
2013
+ });
1427
2014
  const events$1 = translateChunkToAnthropicEvents(chunk, streamState);
1428
2015
  for (const event of events$1) {
1429
2016
  consola.debug("Translated Anthropic event:", JSON.stringify(event));
@@ -1490,6 +2077,37 @@ modelRoutes.get("/", async (c) => {
1490
2077
  }
1491
2078
  });
1492
2079
 
2080
+ //#endregion
2081
+ //#region src/routes/replacements/route.ts
2082
+ const replacementsRoute = new Hono();
2083
+ replacementsRoute.get("/", async (c) => {
2084
+ return c.json({
2085
+ all: await getAllReplacements(),
2086
+ user: await getUserReplacements()
2087
+ });
2088
+ });
2089
+ replacementsRoute.post("/", async (c) => {
2090
+ const body = await c.req.json();
2091
+ if (!body.pattern) return c.json({ error: "Pattern is required" }, 400);
2092
+ const rule = await addReplacement(body.pattern, body.replacement ?? "", body.isRegex ?? false);
2093
+ return c.json(rule, 201);
2094
+ });
2095
+ replacementsRoute.delete("/:id", async (c) => {
2096
+ const id = c.req.param("id");
2097
+ if (!await removeReplacement(id)) return c.json({ error: "Replacement not found or is a system rule" }, 404);
2098
+ return c.json({ success: true });
2099
+ });
2100
+ replacementsRoute.patch("/:id/toggle", async (c) => {
2101
+ const id = c.req.param("id");
2102
+ const rule = await toggleReplacement(id);
2103
+ if (!rule) return c.json({ error: "Replacement not found or is a system rule" }, 404);
2104
+ return c.json(rule);
2105
+ });
2106
+ replacementsRoute.delete("/", async (c) => {
2107
+ await clearUserReplacements();
2108
+ return c.json({ success: true });
2109
+ });
2110
+
1493
2111
  //#endregion
1494
2112
  //#region src/routes/token/route.ts
1495
2113
  const tokenRoute = new Hono();
@@ -1521,7 +2139,7 @@ usageRoute.get("/", async (c) => {
1521
2139
  //#endregion
1522
2140
  //#region src/server.ts
1523
2141
  const server = new Hono();
1524
- server.use(logger());
2142
+ server.use(requestLogger);
1525
2143
  server.use(cors());
1526
2144
  server.get("/", (c) => c.text("Server running"));
1527
2145
  server.route("/chat/completions", completionRoutes);
@@ -1529,6 +2147,7 @@ server.route("/models", modelRoutes);
1529
2147
  server.route("/embeddings", embeddingRoutes);
1530
2148
  server.route("/usage", usageRoute);
1531
2149
  server.route("/token", tokenRoute);
2150
+ server.route("/replacements", replacementsRoute);
1532
2151
  server.route("/v1/chat/completions", completionRoutes);
1533
2152
  server.route("/v1/models", modelRoutes);
1534
2153
  server.route("/v1/embeddings", embeddingRoutes);
@@ -1537,6 +2156,10 @@ server.route("/v1/messages", messageRoutes);
1537
2156
  //#endregion
1538
2157
  //#region src/start.ts
1539
2158
  async function runServer(options) {
2159
+ if (options.insecure) {
2160
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
2161
+ consola.warn("SSL certificate verification disabled (insecure mode)");
2162
+ }
1540
2163
  if (options.proxyEnv) initProxyFromEnv();
1541
2164
  if (options.verbose) {
1542
2165
  consola.level = 5;
@@ -1656,6 +2279,11 @@ const start = defineCommand({
1656
2279
  type: "boolean",
1657
2280
  default: false,
1658
2281
  description: "Initialize proxy from environment variables"
2282
+ },
2283
+ insecure: {
2284
+ type: "boolean",
2285
+ default: false,
2286
+ description: "Disable SSL certificate verification (for corporate proxies with self-signed certs)"
1659
2287
  }
1660
2288
  },
1661
2289
  run({ args }) {
@@ -1671,7 +2299,8 @@ const start = defineCommand({
1671
2299
  githubToken: args["github-token"],
1672
2300
  claudeCode: args["claude-code"],
1673
2301
  showToken: args["show-token"],
1674
- proxyEnv: args["proxy-env"]
2302
+ proxyEnv: args["proxy-env"],
2303
+ insecure: args.insecure
1675
2304
  });
1676
2305
  }
1677
2306
  });
@@ -1687,7 +2316,8 @@ const main = defineCommand({
1687
2316
  auth,
1688
2317
  start,
1689
2318
  "check-usage": checkUsage,
1690
- debug
2319
+ debug,
2320
+ config
1691
2321
  }
1692
2322
  });
1693
2323
  await runMain(main);