@ashsec/copilot-api 0.7.2 → 0.7.5

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,7 +15,6 @@ 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
@@ -97,11 +96,16 @@ const GITHUB_APP_SCOPES = ["read:user"].join(" ");
97
96
  //#region src/lib/error.ts
98
97
  var HTTPError = class extends Error {
99
98
  response;
100
- constructor(message, response) {
99
+ requestPayload;
100
+ constructor(message, response, requestPayload) {
101
101
  super(message);
102
102
  this.response = response;
103
+ this.requestPayload = requestPayload;
103
104
  }
104
105
  };
106
+ function isContentFilterError(obj) {
107
+ return typeof obj === "object" && obj !== null && "error" in obj && typeof obj.error === "object" && obj.error?.code === "content_filter";
108
+ }
105
109
  async function forwardError(c, error) {
106
110
  consola.error("Error occurred:", error);
107
111
  if (error instanceof HTTPError) {
@@ -113,6 +117,15 @@ async function forwardError(c, error) {
113
117
  errorJson = errorText;
114
118
  }
115
119
  consola.error("HTTP error:", errorJson);
120
+ if (isContentFilterError(errorJson)) {
121
+ consola.box("CONTENT FILTER TRIGGERED");
122
+ consola.error("Full error response:");
123
+ console.log(JSON.stringify(errorJson, null, 2));
124
+ if (error.requestPayload) {
125
+ consola.error("Request payload that triggered the filter:");
126
+ console.log(JSON.stringify(error.requestPayload, null, 2));
127
+ }
128
+ }
116
129
  return c.json({ error: {
117
130
  message: errorText,
118
131
  type: "error"
@@ -208,6 +221,75 @@ function getAzureDeploymentName(modelId) {
208
221
  return modelId.slice(13);
209
222
  }
210
223
 
224
+ //#endregion
225
+ //#region src/lib/retry-fetch.ts
226
+ const RETRY_DELAYS_MS = [
227
+ 100,
228
+ 200,
229
+ 300
230
+ ];
231
+ /**
232
+ * Check if an error is retryable (transient network error)
233
+ */
234
+ function isRetryableError(error) {
235
+ if (!(error instanceof Error)) return false;
236
+ const message = error.message.toLowerCase();
237
+ const causeMessage = error.cause instanceof Error ? error.cause.message.toLowerCase() : "";
238
+ return [
239
+ "fetch failed",
240
+ "other side closed",
241
+ "connection reset",
242
+ "econnreset",
243
+ "socket hang up",
244
+ "socket connection was closed unexpectedly",
245
+ "etimedout",
246
+ "econnrefused",
247
+ "network error",
248
+ "aborted",
249
+ "timeout"
250
+ ].some((pattern) => message.includes(pattern) || causeMessage.includes(pattern));
251
+ }
252
+ /**
253
+ * Check if an HTTP response status is retryable
254
+ */
255
+ function isRetryableStatus(status) {
256
+ return status === 408 || status === 429 || status >= 500 && status <= 599;
257
+ }
258
+ /**
259
+ * Fetch with automatic fast retry on transient failures
260
+ * Retries with delays: 100ms, 200ms, 300ms (max ~600ms total wait)
261
+ */
262
+ async function fetchWithRetry(input, init) {
263
+ const maxAttempts = RETRY_DELAYS_MS.length + 1;
264
+ let lastError;
265
+ let lastResponse;
266
+ for (let attempt = 0; attempt < maxAttempts; attempt++) try {
267
+ const headers = new Headers(init?.headers);
268
+ headers.set("Connection", "close");
269
+ const response = await fetch(input, {
270
+ ...init,
271
+ headers,
272
+ keepalive: false
273
+ });
274
+ if (isRetryableStatus(response.status) && attempt < maxAttempts - 1) {
275
+ lastResponse = response;
276
+ const delayMs = RETRY_DELAYS_MS[attempt];
277
+ consola.warn(`HTTP ${response.status} (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delayMs}ms`);
278
+ await sleep(delayMs);
279
+ continue;
280
+ }
281
+ return response;
282
+ } catch (error) {
283
+ lastError = error;
284
+ if (!isRetryableError(error) || attempt === maxAttempts - 1) throw error;
285
+ const delayMs = RETRY_DELAYS_MS[attempt];
286
+ consola.warn(`Fetch failed (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delayMs}ms:`, lastError.message);
287
+ await sleep(delayMs);
288
+ }
289
+ if (lastResponse) return lastResponse;
290
+ throw lastError;
291
+ }
292
+
211
293
  //#endregion
212
294
  //#region src/services/azure-openai/create-chat-completions.ts
213
295
  const AZURE_API_VERSION = "2024-10-21";
@@ -219,7 +301,7 @@ async function createAzureOpenAIChatCompletions(config$1, payload) {
219
301
  model: deploymentName,
220
302
  ...max_tokens != null && { max_completion_tokens: max_tokens }
221
303
  };
222
- const response = await fetch(`${config$1.endpoint}/openai/deployments/${deploymentName}/chat/completions?api-version=${AZURE_API_VERSION}`, {
304
+ const response = await fetchWithRetry(`${config$1.endpoint}/openai/deployments/${deploymentName}/chat/completions?api-version=${AZURE_API_VERSION}`, {
223
305
  method: "POST",
224
306
  headers: {
225
307
  "api-key": config$1.apiKey,
@@ -229,7 +311,7 @@ async function createAzureOpenAIChatCompletions(config$1, payload) {
229
311
  });
230
312
  if (!response.ok) {
231
313
  consola.error("Failed to create Azure OpenAI chat completions:", response);
232
- throw new HTTPError("Failed to create Azure OpenAI chat completions", response);
314
+ throw new HTTPError("Failed to create Azure OpenAI chat completions", response, payload);
233
315
  }
234
316
  if (payload.stream) return events(response);
235
317
  return await response.json();
@@ -240,7 +322,7 @@ async function createAzureOpenAIChatCompletions(config$1, payload) {
240
322
  const AZURE_DEPLOYMENTS_API_VERSION = "2022-12-01";
241
323
  async function getAzureOpenAIDeployments(config$1) {
242
324
  try {
243
- const response = await fetch(`${config$1.endpoint}/openai/deployments?api-version=${AZURE_DEPLOYMENTS_API_VERSION}`, { headers: {
325
+ const response = await fetchWithRetry(`${config$1.endpoint}/openai/deployments?api-version=${AZURE_DEPLOYMENTS_API_VERSION}`, { headers: {
244
326
  "api-key": config$1.apiKey,
245
327
  "Content-Type": "application/json"
246
328
  } });
@@ -268,7 +350,7 @@ async function getAzureOpenAIDeployments(config$1) {
268
350
  //#region src/services/copilot/get-models.ts
269
351
  const getModels = async () => {
270
352
  const url = `${copilotBaseUrl(state)}/models`;
271
- const response = await fetch(url, { headers: copilotHeaders(state) });
353
+ const response = await fetchWithRetry(url, { headers: copilotHeaders(state) });
272
354
  if (!response.ok) {
273
355
  const errorBody = await response.text();
274
356
  let errorDetails;
@@ -515,7 +597,8 @@ const checkUsage = defineCommand({
515
597
  //#region src/lib/auto-replace.ts
516
598
  const SYSTEM_REPLACEMENTS = [{
517
599
  id: "system-anthropic-billing",
518
- pattern: "x-anthropic-billing-header:[^\n]*\n?",
600
+ name: "Remove Anthropic billing header",
601
+ pattern: "x-anthropic-billing-header:[^\\n]*\\n?",
519
602
  replacement: "",
520
603
  isRegex: true,
521
604
  enabled: true,
@@ -529,7 +612,7 @@ let isLoaded = false;
529
612
  async function loadReplacements() {
530
613
  try {
531
614
  const data = await fs.readFile(PATHS.REPLACEMENTS_CONFIG_PATH);
532
- userReplacements = JSON.parse(data).filter((r) => !r.isSystem);
615
+ userReplacements = JSON.parse(data.toString()).filter((r) => !r.isSystem);
533
616
  isLoaded = true;
534
617
  consola.debug(`Loaded ${userReplacements.length} user replacement rules`);
535
618
  } catch {
@@ -572,10 +655,11 @@ async function getUserReplacements() {
572
655
  /**
573
656
  * Add a new user replacement rule
574
657
  */
575
- async function addReplacement(pattern, replacement, isRegex = false) {
658
+ async function addReplacement(pattern, replacement, isRegex = false, name) {
576
659
  await ensureLoaded();
577
660
  const rule = {
578
661
  id: `user-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
662
+ name,
579
663
  pattern,
580
664
  replacement,
581
665
  isRegex,
@@ -604,6 +688,26 @@ async function removeReplacement(id) {
604
688
  return true;
605
689
  }
606
690
  /**
691
+ * Update an existing user replacement rule
692
+ */
693
+ async function updateReplacement(id, updates) {
694
+ await ensureLoaded();
695
+ const rule = userReplacements.find((r) => r.id === id);
696
+ if (!rule) return null;
697
+ if (rule.isSystem) {
698
+ consola.warn("Cannot update system replacement rule");
699
+ return null;
700
+ }
701
+ if (updates.name !== void 0) rule.name = updates.name;
702
+ if (updates.pattern !== void 0) rule.pattern = updates.pattern;
703
+ if (updates.replacement !== void 0) rule.replacement = updates.replacement;
704
+ if (updates.isRegex !== void 0) rule.isRegex = updates.isRegex;
705
+ if (updates.enabled !== void 0) rule.enabled = updates.enabled;
706
+ await saveReplacements();
707
+ consola.info(`Updated replacement rule: ${rule.name || rule.id}`);
708
+ return rule;
709
+ }
710
+ /**
607
711
  * Toggle a replacement rule on/off
608
712
  */
609
713
  async function toggleReplacement(id) {
@@ -630,18 +734,32 @@ async function clearUserReplacements() {
630
734
  consola.info("Cleared all user replacement rules");
631
735
  }
632
736
  /**
633
- * Apply a single replacement rule to text
737
+ * Apply a single replacement rule to text and return info about whether it matched
634
738
  */
635
739
  function applyRule(text, rule) {
636
- if (!rule.enabled) return text;
740
+ if (!rule.enabled) return {
741
+ result: text,
742
+ matched: false
743
+ };
637
744
  if (rule.isRegex) try {
638
745
  const regex = new RegExp(rule.pattern, "g");
639
- return text.replace(regex, rule.replacement);
746
+ const result$1 = text.replace(regex, rule.replacement);
747
+ return {
748
+ result: result$1,
749
+ matched: result$1 !== text
750
+ };
640
751
  } catch {
641
752
  consola.warn(`Invalid regex pattern in rule ${rule.id}: ${rule.pattern}`);
642
- return text;
753
+ return {
754
+ result: text,
755
+ matched: false
756
+ };
643
757
  }
644
- return text.split(rule.pattern).join(rule.replacement);
758
+ const result = text.split(rule.pattern).join(rule.replacement);
759
+ return {
760
+ result,
761
+ matched: result !== text
762
+ };
645
763
  }
646
764
  /**
647
765
  * Apply all replacement rules to text
@@ -649,11 +767,15 @@ function applyRule(text, rule) {
649
767
  async function applyReplacements(text) {
650
768
  let result = text;
651
769
  const allRules = await getAllReplacements();
770
+ const appliedRules = [];
652
771
  for (const rule of allRules) {
653
- const before = result;
654
- result = applyRule(result, rule);
655
- if (before !== result) consola.debug(`Applied replacement rule: ${rule.id}`);
772
+ const { result: newResult, matched } = applyRule(result, rule);
773
+ if (matched) {
774
+ result = newResult;
775
+ appliedRules.push(rule.name || rule.id);
776
+ }
656
777
  }
778
+ if (appliedRules.length > 0) consola.info(`Replacements applied: ${appliedRules.join(", ")}`);
657
779
  return result;
658
780
  }
659
781
  /**
@@ -690,8 +812,9 @@ function formatRule(rule, index) {
690
812
  const status = rule.enabled ? "✓" : "✗";
691
813
  const type = rule.isRegex ? "regex" : "string";
692
814
  const system = rule.isSystem ? " [system]" : "";
815
+ const name = rule.name ? ` "${rule.name}"` : "";
693
816
  const replacement = rule.replacement || "(empty)";
694
- return `${index + 1}. [${status}] (${type})${system} "${rule.pattern}" → "${replacement}"`;
817
+ return `${index + 1}. [${status}] (${type})${system}${name} "${rule.pattern}" → "${replacement}"`;
695
818
  }
696
819
  async function listReplacements() {
697
820
  const all = await getAllReplacements();
@@ -704,6 +827,14 @@ async function listReplacements() {
704
827
  console.log();
705
828
  }
706
829
  async function addNewReplacement() {
830
+ const name = await consola.prompt("Name (optional, short description):", {
831
+ type: "text",
832
+ default: ""
833
+ });
834
+ if (typeof name === "symbol") {
835
+ consola.info("Cancelled.");
836
+ return;
837
+ }
707
838
  const matchType = await consola.prompt("Match type:", {
708
839
  type: "select",
709
840
  options: [{
@@ -737,8 +868,87 @@ async function addNewReplacement() {
737
868
  consola.info("Cancelled.");
738
869
  return;
739
870
  }
740
- const rule = await addReplacement(pattern, replacement, matchType === "regex");
741
- consola.success(`Added rule: ${rule.id}`);
871
+ const rule = await addReplacement(pattern, replacement, matchType === "regex", name || void 0);
872
+ consola.success(`Added rule: ${rule.name || rule.id}`);
873
+ }
874
+ async function editExistingReplacement() {
875
+ const userRules = await getUserReplacements();
876
+ if (userRules.length === 0) {
877
+ consola.info("No user rules to edit.");
878
+ return;
879
+ }
880
+ const options = userRules.map((rule$1, i) => ({
881
+ label: formatRule(rule$1, i),
882
+ value: rule$1.id
883
+ }));
884
+ const selected = await consola.prompt("Select rule to edit:", {
885
+ type: "select",
886
+ options
887
+ });
888
+ if (typeof selected === "symbol") {
889
+ consola.info("Cancelled.");
890
+ return;
891
+ }
892
+ const rule = userRules.find((r) => r.id === selected);
893
+ if (!rule) {
894
+ consola.error("Rule not found.");
895
+ return;
896
+ }
897
+ consola.info(`\nEditing rule: ${rule.name || rule.id}`);
898
+ consola.info("Press Enter to keep current value.\n");
899
+ const name = await consola.prompt("Name:", {
900
+ type: "text",
901
+ default: rule.name || ""
902
+ });
903
+ if (typeof name === "symbol") {
904
+ consola.info("Cancelled.");
905
+ return;
906
+ }
907
+ const matchType = await consola.prompt("Match type:", {
908
+ type: "select",
909
+ options: [{
910
+ label: "String (exact match)",
911
+ value: "string"
912
+ }, {
913
+ label: "Regex (regular expression)",
914
+ value: "regex"
915
+ }],
916
+ initial: rule.isRegex ? "regex" : "string"
917
+ });
918
+ if (typeof matchType === "symbol") {
919
+ consola.info("Cancelled.");
920
+ return;
921
+ }
922
+ const pattern = await consola.prompt("Pattern to match:", {
923
+ type: "text",
924
+ default: rule.pattern
925
+ });
926
+ if (typeof pattern === "symbol" || !pattern) {
927
+ consola.info("Cancelled.");
928
+ return;
929
+ }
930
+ if (matchType === "regex") try {
931
+ new RegExp(pattern);
932
+ } catch {
933
+ consola.error(`Invalid regex pattern: ${pattern}`);
934
+ return;
935
+ }
936
+ const replacement = await consola.prompt("Replacement text:", {
937
+ type: "text",
938
+ default: rule.replacement
939
+ });
940
+ if (typeof replacement === "symbol") {
941
+ consola.info("Cancelled.");
942
+ return;
943
+ }
944
+ const updated = await updateReplacement(selected, {
945
+ name: name || void 0,
946
+ pattern,
947
+ replacement,
948
+ isRegex: matchType === "regex"
949
+ });
950
+ if (updated) consola.success(`Updated rule: ${updated.name || updated.id}`);
951
+ else consola.error("Failed to update rule.");
742
952
  }
743
953
  async function removeExistingReplacement() {
744
954
  const userRules = await getUserReplacements();
@@ -821,6 +1031,10 @@ async function mainMenu() {
821
1031
  label: "➕ Add new rule",
822
1032
  value: "add"
823
1033
  },
1034
+ {
1035
+ label: "✏️ Edit rule",
1036
+ value: "edit"
1037
+ },
824
1038
  {
825
1039
  label: "➖ Remove rule",
826
1040
  value: "remove"
@@ -851,6 +1065,9 @@ async function mainMenu() {
851
1065
  case "add":
852
1066
  await addNewReplacement();
853
1067
  break;
1068
+ case "edit":
1069
+ await editExistingReplacement();
1070
+ break;
854
1071
  case "remove":
855
1072
  await removeExistingReplacement();
856
1073
  break;
@@ -1055,6 +1272,84 @@ function generateEnvScript(envVars, commandToRun = "") {
1055
1272
  return commandBlock || commandToRun;
1056
1273
  }
1057
1274
 
1275
+ //#endregion
1276
+ //#region src/lib/request-logger.ts
1277
+ const REQUEST_CONTEXT_KEY = "requestContext";
1278
+ const colors = {
1279
+ reset: "\x1B[0m",
1280
+ dim: "\x1B[2m",
1281
+ bold: "\x1B[1m",
1282
+ cyan: "\x1B[36m",
1283
+ green: "\x1B[32m",
1284
+ yellow: "\x1B[33m",
1285
+ red: "\x1B[31m",
1286
+ magenta: "\x1B[35m",
1287
+ blue: "\x1B[34m",
1288
+ white: "\x1B[37m",
1289
+ gray: "\x1B[90m"
1290
+ };
1291
+ /**
1292
+ * Get the current time formatted as HH:MM:SS
1293
+ */
1294
+ function getTimeString() {
1295
+ return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", {
1296
+ hour12: false,
1297
+ hour: "2-digit",
1298
+ minute: "2-digit",
1299
+ second: "2-digit"
1300
+ });
1301
+ }
1302
+ /**
1303
+ * Get status color based on HTTP status code
1304
+ */
1305
+ function getStatusColor(status) {
1306
+ if (status >= 500) return colors.red;
1307
+ if (status >= 400) return colors.yellow;
1308
+ if (status >= 300) return colors.cyan;
1309
+ return colors.green;
1310
+ }
1311
+ /**
1312
+ * Set request context for logging
1313
+ */
1314
+ function setRequestContext(c, ctx) {
1315
+ const existing = c.get(REQUEST_CONTEXT_KEY);
1316
+ if (existing) c.set(REQUEST_CONTEXT_KEY, {
1317
+ ...existing,
1318
+ ...ctx
1319
+ });
1320
+ }
1321
+ /**
1322
+ * Custom request logger middleware
1323
+ */
1324
+ async function requestLogger(c, next) {
1325
+ const startTime = Date.now();
1326
+ const method = c.req.method;
1327
+ const path$1 = c.req.path + (c.req.raw.url.includes("?") ? "?" + c.req.raw.url.split("?")[1] : "");
1328
+ c.set(REQUEST_CONTEXT_KEY, { startTime });
1329
+ await next();
1330
+ const ctx = c.get(REQUEST_CONTEXT_KEY);
1331
+ const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
1332
+ const status = c.res.status;
1333
+ const statusColor = getStatusColor(status);
1334
+ const lines = [];
1335
+ lines.push(`${colors.dim}${"─".repeat(60)}${colors.reset}`);
1336
+ const statusBadge = `${statusColor}${status}${colors.reset}`;
1337
+ const durationStr = `${colors.cyan}${duration}s${colors.reset}`;
1338
+ lines.push(`${colors.bold}${method}${colors.reset} ${path$1} ${statusBadge} ${durationStr}`);
1339
+ if (ctx?.provider && ctx?.model) {
1340
+ const providerColor = ctx.provider === "Azure OpenAI" ? colors.blue : colors.magenta;
1341
+ lines.push(` ${colors.gray}Provider:${colors.reset} ${providerColor}${ctx.provider}${colors.reset} ${colors.gray}->${colors.reset} ${colors.white}${ctx.model}${colors.reset}`);
1342
+ }
1343
+ if (ctx?.inputTokens !== void 0 || ctx?.outputTokens !== void 0) {
1344
+ const tokenParts = [];
1345
+ if (ctx.inputTokens !== void 0) tokenParts.push(`${colors.gray}Input:${colors.reset} ${colors.yellow}${ctx.inputTokens.toLocaleString()}${colors.reset}`);
1346
+ if (ctx.outputTokens !== void 0) tokenParts.push(`${colors.gray}Output:${colors.reset} ${colors.green}${ctx.outputTokens.toLocaleString()}${colors.reset}`);
1347
+ lines.push(` ${tokenParts.join(" ")}`);
1348
+ }
1349
+ lines.push(` ${colors.dim}${getTimeString()}${colors.reset}`);
1350
+ console.log(lines.join("\n"));
1351
+ }
1352
+
1058
1353
  //#endregion
1059
1354
  //#region src/lib/approval.ts
1060
1355
  const awaitApproval = async () => {
@@ -1294,7 +1589,7 @@ const createChatCompletions = async (payload) => {
1294
1589
  ...copilotHeaders(state, enableVision),
1295
1590
  "X-Initiator": isAgentCall ? "agent" : "user"
1296
1591
  };
1297
- const response = await fetch(`${copilotBaseUrl(state)}/chat/completions`, {
1592
+ const response = await fetchWithRetry(`${copilotBaseUrl(state)}/chat/completions`, {
1298
1593
  method: "POST",
1299
1594
  headers,
1300
1595
  body: JSON.stringify(payload)
@@ -1316,28 +1611,45 @@ async function handleCompletion$1(c) {
1316
1611
  consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
1317
1612
  if (isAzureOpenAIModel(payload.model)) {
1318
1613
  if (!state.azureOpenAIConfig) return c.json({ error: "Azure OpenAI not configured" }, 500);
1319
- consola.info(`Routing to Azure OpenAI -> ${payload.model}`);
1614
+ setRequestContext(c, {
1615
+ provider: "Azure OpenAI",
1616
+ model: payload.model
1617
+ });
1320
1618
  if (state.manualApprove) await awaitApproval();
1321
1619
  const response$1 = await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, payload);
1322
- if (isNonStreaming$1(response$1)) {
1620
+ if (isNonStreaming(response$1)) {
1323
1621
  consola.debug("Non-streaming response:", JSON.stringify(response$1));
1622
+ if (response$1.usage) setRequestContext(c, {
1623
+ inputTokens: response$1.usage.prompt_tokens,
1624
+ outputTokens: response$1.usage.completion_tokens
1625
+ });
1324
1626
  return c.json(response$1);
1325
1627
  }
1326
1628
  consola.debug("Streaming response");
1327
1629
  return streamSSE(c, async (stream) => {
1328
1630
  for await (const chunk of response$1) {
1329
1631
  consola.debug("Streaming chunk:", JSON.stringify(chunk));
1632
+ if (chunk.data && chunk.data !== "[DONE]") {
1633
+ const parsed = JSON.parse(chunk.data);
1634
+ if (parsed.usage) setRequestContext(c, {
1635
+ inputTokens: parsed.usage.prompt_tokens,
1636
+ outputTokens: parsed.usage.completion_tokens
1637
+ });
1638
+ }
1330
1639
  await stream.writeSSE(chunk);
1331
1640
  }
1332
1641
  });
1333
1642
  }
1334
- consola.info(`Routing to Copilot -> ${payload.model}`);
1643
+ setRequestContext(c, {
1644
+ provider: "Copilot",
1645
+ model: payload.model
1646
+ });
1335
1647
  const selectedModel = state.models?.data.find((model) => model.id === payload.model);
1336
1648
  try {
1337
1649
  if (selectedModel) {
1338
1650
  const tokenCount = await getTokenCount(payload, selectedModel);
1339
- consola.info("Current token count:", tokenCount);
1340
- } else consola.warn("No model selected, skipping token count calculation");
1651
+ setRequestContext(c, { inputTokens: tokenCount.input });
1652
+ }
1341
1653
  } catch (error) {
1342
1654
  consola.warn("Failed to calculate token count:", error);
1343
1655
  }
@@ -1350,19 +1662,30 @@ async function handleCompletion$1(c) {
1350
1662
  consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
1351
1663
  }
1352
1664
  const response = await createChatCompletions(payload);
1353
- if (isNonStreaming$1(response)) {
1665
+ if (isNonStreaming(response)) {
1354
1666
  consola.debug("Non-streaming response:", JSON.stringify(response));
1667
+ if (response.usage) setRequestContext(c, {
1668
+ inputTokens: response.usage.prompt_tokens,
1669
+ outputTokens: response.usage.completion_tokens
1670
+ });
1355
1671
  return c.json(response);
1356
1672
  }
1357
1673
  consola.debug("Streaming response");
1358
1674
  return streamSSE(c, async (stream) => {
1359
1675
  for await (const chunk of response) {
1360
1676
  consola.debug("Streaming chunk:", JSON.stringify(chunk));
1677
+ if (chunk.data && chunk.data !== "[DONE]") {
1678
+ const parsed = JSON.parse(chunk.data);
1679
+ if (parsed.usage) setRequestContext(c, {
1680
+ inputTokens: parsed.usage.prompt_tokens,
1681
+ outputTokens: parsed.usage.completion_tokens
1682
+ });
1683
+ }
1361
1684
  await stream.writeSSE(chunk);
1362
1685
  }
1363
1686
  });
1364
1687
  }
1365
- const isNonStreaming$1 = (response) => Object.hasOwn(response, "choices");
1688
+ const isNonStreaming = (response) => Object.hasOwn(response, "choices");
1366
1689
 
1367
1690
  //#endregion
1368
1691
  //#region src/routes/chat-completions/route.ts
@@ -1379,7 +1702,7 @@ completionRoutes.post("/", async (c) => {
1379
1702
  //#region src/services/copilot/create-embeddings.ts
1380
1703
  const createEmbeddings = async (payload) => {
1381
1704
  if (!state.copilotToken) throw new Error("Copilot token not found");
1382
- const response = await fetch(`${copilotBaseUrl(state)}/embeddings`, {
1705
+ const response = await fetchWithRetry(`${copilotBaseUrl(state)}/embeddings`, {
1383
1706
  method: "POST",
1384
1707
  headers: copilotHeaders(state),
1385
1708
  body: JSON.stringify(payload)
@@ -1652,12 +1975,7 @@ function translateChunkToAnthropicEvents(chunk, state$1) {
1652
1975
  content: [],
1653
1976
  model: chunk.model,
1654
1977
  stop_reason: null,
1655
- stop_sequence: null,
1656
- usage: {
1657
- input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
1658
- output_tokens: 0,
1659
- ...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
1660
- }
1978
+ stop_sequence: null
1661
1979
  }
1662
1980
  });
1663
1981
  state$1.messageStartSent = true;
@@ -1739,6 +2057,9 @@ function translateChunkToAnthropicEvents(chunk, state$1) {
1739
2057
  });
1740
2058
  state$1.contentBlockOpen = false;
1741
2059
  }
2060
+ const inputTokens = chunk.usage?.prompt_tokens ?? 0;
2061
+ const outputTokens = chunk.usage?.completion_tokens ?? 0;
2062
+ const cachedTokens = chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0;
1742
2063
  events$1.push({
1743
2064
  type: "message_delta",
1744
2065
  delta: {
@@ -1746,9 +2067,10 @@ function translateChunkToAnthropicEvents(chunk, state$1) {
1746
2067
  stop_sequence: null
1747
2068
  },
1748
2069
  usage: {
1749
- input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
1750
- output_tokens: chunk.usage?.completion_tokens ?? 0,
1751
- ...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
2070
+ input_tokens: inputTokens,
2071
+ output_tokens: outputTokens,
2072
+ cache_creation_input_tokens: 0,
2073
+ cache_read_input_tokens: cachedTokens
1752
2074
  }
1753
2075
  }, { type: "message_stop" });
1754
2076
  }
@@ -1765,73 +2087,68 @@ async function handleCompletion(c) {
1765
2087
  const openAIPayload = await applyReplacementsToPayload(translatedPayload);
1766
2088
  consola.debug("Translated OpenAI request payload:", JSON.stringify(openAIPayload));
1767
2089
  if (state.manualApprove) await awaitApproval();
1768
- if (isAzureOpenAIModel(openAIPayload.model)) {
2090
+ const isAzureModel = isAzureOpenAIModel(openAIPayload.model);
2091
+ if (isAzureModel) {
1769
2092
  if (!state.azureOpenAIConfig) return c.json({ error: "Azure OpenAI not configured" }, 500);
1770
- consola.info(`Routing to Azure OpenAI -> ${openAIPayload.model}`);
1771
- const response$1 = await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, openAIPayload);
1772
- if (isNonStreaming(response$1)) {
1773
- consola.debug("Non-streaming response from Azure OpenAI:", JSON.stringify(response$1).slice(-400));
1774
- const anthropicResponse = translateToAnthropic(response$1);
1775
- consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
1776
- return c.json(anthropicResponse);
1777
- }
1778
- consola.debug("Streaming response from Azure OpenAI");
2093
+ setRequestContext(c, {
2094
+ provider: "Azure OpenAI",
2095
+ model: openAIPayload.model
2096
+ });
2097
+ } else setRequestContext(c, {
2098
+ provider: "Copilot",
2099
+ model: openAIPayload.model
2100
+ });
2101
+ if (anthropicPayload.stream) {
2102
+ const streamPayload = {
2103
+ ...openAIPayload,
2104
+ stream: true,
2105
+ stream_options: { include_usage: true }
2106
+ };
2107
+ const eventStream = isAzureModel ? await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, streamPayload) : await createChatCompletions(streamPayload);
1779
2108
  return streamSSE(c, async (stream) => {
1780
2109
  const streamState = {
1781
2110
  messageStartSent: false,
1782
- contentBlockIndex: 0,
1783
2111
  contentBlockOpen: false,
2112
+ contentBlockIndex: 0,
1784
2113
  toolCalls: {}
1785
2114
  };
1786
- for await (const rawEvent of response$1) {
1787
- consola.debug("Azure OpenAI raw stream event:", JSON.stringify(rawEvent));
1788
- if (rawEvent.data === "[DONE]") break;
1789
- if (!rawEvent.data) continue;
1790
- const chunk = JSON.parse(rawEvent.data);
1791
- const events$1 = translateChunkToAnthropicEvents(chunk, streamState);
1792
- for (const event of events$1) {
1793
- consola.debug("Translated Anthropic event:", JSON.stringify(event));
1794
- await stream.writeSSE({
1795
- event: event.type,
1796
- data: JSON.stringify(event)
2115
+ for await (const event of eventStream) {
2116
+ if (!event.data || event.data === "[DONE]") continue;
2117
+ try {
2118
+ const chunk = JSON.parse(event.data);
2119
+ consola.debug("OpenAI chunk:", JSON.stringify(chunk));
2120
+ const anthropicEvents = translateChunkToAnthropicEvents(chunk, streamState);
2121
+ for (const anthropicEvent of anthropicEvents) {
2122
+ consola.debug("Anthropic event:", JSON.stringify(anthropicEvent));
2123
+ await stream.writeSSE({
2124
+ event: anthropicEvent.type,
2125
+ data: JSON.stringify(anthropicEvent)
2126
+ });
2127
+ }
2128
+ if (chunk.usage) setRequestContext(c, {
2129
+ inputTokens: chunk.usage.prompt_tokens,
2130
+ outputTokens: chunk.usage.completion_tokens
1797
2131
  });
2132
+ } catch (error) {
2133
+ consola.error("Failed to parse chunk:", error, event.data);
1798
2134
  }
1799
2135
  }
1800
2136
  });
1801
2137
  }
1802
- consola.info(`Routing to Copilot -> ${openAIPayload.model}`);
1803
- const response = await createChatCompletions(openAIPayload);
1804
- if (isNonStreaming(response)) {
1805
- consola.debug("Non-streaming response from Copilot:", JSON.stringify(response).slice(-400));
1806
- const anthropicResponse = translateToAnthropic(response);
1807
- consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
1808
- return c.json(anthropicResponse);
1809
- }
1810
- consola.debug("Streaming response from Copilot");
1811
- return streamSSE(c, async (stream) => {
1812
- const streamState = {
1813
- messageStartSent: false,
1814
- contentBlockIndex: 0,
1815
- contentBlockOpen: false,
1816
- toolCalls: {}
1817
- };
1818
- for await (const rawEvent of response) {
1819
- consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
1820
- if (rawEvent.data === "[DONE]") break;
1821
- if (!rawEvent.data) continue;
1822
- const chunk = JSON.parse(rawEvent.data);
1823
- const events$1 = translateChunkToAnthropicEvents(chunk, streamState);
1824
- for (const event of events$1) {
1825
- consola.debug("Translated Anthropic event:", JSON.stringify(event));
1826
- await stream.writeSSE({
1827
- event: event.type,
1828
- data: JSON.stringify(event)
1829
- });
1830
- }
1831
- }
2138
+ const nonStreamPayload = {
2139
+ ...openAIPayload,
2140
+ stream: false
2141
+ };
2142
+ const response = isAzureModel ? await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, nonStreamPayload) : await createChatCompletions(nonStreamPayload);
2143
+ consola.debug("Response from upstream:", JSON.stringify(response).slice(-400));
2144
+ if (response.usage) setRequestContext(c, {
2145
+ inputTokens: response.usage.prompt_tokens,
2146
+ outputTokens: response.usage.completion_tokens
1832
2147
  });
2148
+ const anthropicResponse = translateToAnthropic(response);
2149
+ consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
2150
+ return c.json(anthropicResponse);
1833
2151
  }
1834
- const isNonStreaming = (response) => Object.hasOwn(response, "choices");
1835
2152
 
1836
2153
  //#endregion
1837
2154
  //#region src/routes/messages/route.ts
@@ -1898,7 +2215,7 @@ replacementsRoute.get("/", async (c) => {
1898
2215
  replacementsRoute.post("/", async (c) => {
1899
2216
  const body = await c.req.json();
1900
2217
  if (!body.pattern) return c.json({ error: "Pattern is required" }, 400);
1901
- const rule = await addReplacement(body.pattern, body.replacement ?? "", body.isRegex ?? false);
2218
+ const rule = await addReplacement(body.pattern, body.replacement ?? "", body.isRegex ?? false, body.name);
1902
2219
  return c.json(rule, 201);
1903
2220
  });
1904
2221
  replacementsRoute.delete("/:id", async (c) => {
@@ -1906,6 +2223,13 @@ replacementsRoute.delete("/:id", async (c) => {
1906
2223
  if (!await removeReplacement(id)) return c.json({ error: "Replacement not found or is a system rule" }, 404);
1907
2224
  return c.json({ success: true });
1908
2225
  });
2226
+ replacementsRoute.patch("/:id", async (c) => {
2227
+ const id = c.req.param("id");
2228
+ const body = await c.req.json();
2229
+ const rule = await updateReplacement(id, body);
2230
+ if (!rule) return c.json({ error: "Replacement not found or is a system rule" }, 404);
2231
+ return c.json(rule);
2232
+ });
1909
2233
  replacementsRoute.patch("/:id/toggle", async (c) => {
1910
2234
  const id = c.req.param("id");
1911
2235
  const rule = await toggleReplacement(id);
@@ -1948,7 +2272,7 @@ usageRoute.get("/", async (c) => {
1948
2272
  //#endregion
1949
2273
  //#region src/server.ts
1950
2274
  const server = new Hono();
1951
- server.use(logger());
2275
+ server.use(requestLogger);
1952
2276
  server.use(cors());
1953
2277
  server.get("/", (c) => c.text("Server running"));
1954
2278
  server.route("/chat/completions", completionRoutes);