@ashsec/copilot-api 0.7.4 → 0.7.6

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
@@ -48,7 +48,8 @@ const state = {
48
48
  accountType: "individual",
49
49
  manualApprove: false,
50
50
  rateLimitWait: false,
51
- showToken: false
51
+ showToken: false,
52
+ debug: false
52
53
  };
53
54
 
54
55
  //#endregion
@@ -96,11 +97,16 @@ const GITHUB_APP_SCOPES = ["read:user"].join(" ");
96
97
  //#region src/lib/error.ts
97
98
  var HTTPError = class extends Error {
98
99
  response;
99
- constructor(message, response) {
100
+ requestPayload;
101
+ constructor(message, response, requestPayload) {
100
102
  super(message);
101
103
  this.response = response;
104
+ this.requestPayload = requestPayload;
102
105
  }
103
106
  };
107
+ function isContentFilterError(obj) {
108
+ return typeof obj === "object" && obj !== null && "error" in obj && typeof obj.error === "object" && obj.error?.code === "content_filter";
109
+ }
104
110
  async function forwardError(c, error) {
105
111
  consola.error("Error occurred:", error);
106
112
  if (error instanceof HTTPError) {
@@ -112,6 +118,15 @@ async function forwardError(c, error) {
112
118
  errorJson = errorText;
113
119
  }
114
120
  consola.error("HTTP error:", errorJson);
121
+ if (isContentFilterError(errorJson)) {
122
+ consola.box("CONTENT FILTER TRIGGERED");
123
+ consola.error("Full error response:");
124
+ console.log(JSON.stringify(errorJson, null, 2));
125
+ if (error.requestPayload) {
126
+ consola.error("Request payload that triggered the filter:");
127
+ console.log(JSON.stringify(error.requestPayload, null, 2));
128
+ }
129
+ }
115
130
  return c.json({ error: {
116
131
  message: errorText,
117
132
  type: "error"
@@ -209,8 +224,11 @@ function getAzureDeploymentName(modelId) {
209
224
 
210
225
  //#endregion
211
226
  //#region src/lib/retry-fetch.ts
212
- const DEFAULT_MAX_RETRIES = 3;
213
- const DEFAULT_BASE_DELAY_MS = 1e3;
227
+ const RETRY_DELAYS_MS = [
228
+ 100,
229
+ 200,
230
+ 300
231
+ ];
214
232
  /**
215
233
  * Check if an error is retryable (transient network error)
216
234
  */
@@ -224,6 +242,7 @@ function isRetryableError(error) {
224
242
  "connection reset",
225
243
  "econnreset",
226
244
  "socket hang up",
245
+ "socket connection was closed unexpectedly",
227
246
  "etimedout",
228
247
  "econnrefused",
229
248
  "network error",
@@ -238,13 +257,14 @@ function isRetryableStatus(status) {
238
257
  return status === 408 || status === 429 || status >= 500 && status <= 599;
239
258
  }
240
259
  /**
241
- * Fetch with automatic retry on transient failures
260
+ * Fetch with automatic fast retry on transient failures
261
+ * Retries with delays: 100ms, 200ms, 300ms (max ~600ms total wait)
242
262
  */
243
- async function fetchWithRetry(input, init, options = {}) {
244
- const { maxRetries = DEFAULT_MAX_RETRIES, baseDelayMs = DEFAULT_BASE_DELAY_MS } = options;
263
+ async function fetchWithRetry(input, init) {
264
+ const maxAttempts = RETRY_DELAYS_MS.length + 1;
245
265
  let lastError;
246
266
  let lastResponse;
247
- for (let attempt = 0; attempt <= maxRetries; attempt++) try {
267
+ for (let attempt = 0; attempt < maxAttempts; attempt++) try {
248
268
  const headers = new Headers(init?.headers);
249
269
  headers.set("Connection", "close");
250
270
  const response = await fetch(input, {
@@ -252,19 +272,19 @@ async function fetchWithRetry(input, init, options = {}) {
252
272
  headers,
253
273
  keepalive: false
254
274
  });
255
- if (isRetryableStatus(response.status) && attempt < maxRetries) {
275
+ if (isRetryableStatus(response.status) && attempt < maxAttempts - 1) {
256
276
  lastResponse = response;
257
- const delayMs = baseDelayMs * 2 ** attempt;
258
- consola.warn(`HTTP ${response.status} (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${delayMs}ms`);
277
+ const delayMs = RETRY_DELAYS_MS[attempt];
278
+ consola.warn(`HTTP ${response.status} (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delayMs}ms`);
259
279
  await sleep(delayMs);
260
280
  continue;
261
281
  }
262
282
  return response;
263
283
  } catch (error) {
264
284
  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);
285
+ if (!isRetryableError(error) || attempt === maxAttempts - 1) throw error;
286
+ const delayMs = RETRY_DELAYS_MS[attempt];
287
+ consola.warn(`Fetch failed (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delayMs}ms:`, lastError.message);
268
288
  await sleep(delayMs);
269
289
  }
270
290
  if (lastResponse) return lastResponse;
@@ -292,7 +312,7 @@ async function createAzureOpenAIChatCompletions(config$1, payload) {
292
312
  });
293
313
  if (!response.ok) {
294
314
  consola.error("Failed to create Azure OpenAI chat completions:", response);
295
- throw new HTTPError("Failed to create Azure OpenAI chat completions", response);
315
+ throw new HTTPError("Failed to create Azure OpenAI chat completions", response, payload);
296
316
  }
297
317
  if (payload.stream) return events(response);
298
318
  return await response.json();
@@ -578,7 +598,8 @@ const checkUsage = defineCommand({
578
598
  //#region src/lib/auto-replace.ts
579
599
  const SYSTEM_REPLACEMENTS = [{
580
600
  id: "system-anthropic-billing",
581
- pattern: "x-anthropic-billing-header:[^\n]*\n?",
601
+ name: "Remove Anthropic billing header",
602
+ pattern: "x-anthropic-billing-header:[^\\n]*\\n?",
582
603
  replacement: "",
583
604
  isRegex: true,
584
605
  enabled: true,
@@ -592,7 +613,7 @@ let isLoaded = false;
592
613
  async function loadReplacements() {
593
614
  try {
594
615
  const data = await fs.readFile(PATHS.REPLACEMENTS_CONFIG_PATH);
595
- userReplacements = JSON.parse(data).filter((r) => !r.isSystem);
616
+ userReplacements = JSON.parse(data.toString()).filter((r) => !r.isSystem);
596
617
  isLoaded = true;
597
618
  consola.debug(`Loaded ${userReplacements.length} user replacement rules`);
598
619
  } catch {
@@ -635,10 +656,11 @@ async function getUserReplacements() {
635
656
  /**
636
657
  * Add a new user replacement rule
637
658
  */
638
- async function addReplacement(pattern, replacement, isRegex = false) {
659
+ async function addReplacement(pattern, replacement, isRegex = false, name) {
639
660
  await ensureLoaded();
640
661
  const rule = {
641
662
  id: `user-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
663
+ name,
642
664
  pattern,
643
665
  replacement,
644
666
  isRegex,
@@ -667,6 +689,26 @@ async function removeReplacement(id) {
667
689
  return true;
668
690
  }
669
691
  /**
692
+ * Update an existing user replacement rule
693
+ */
694
+ async function updateReplacement(id, updates) {
695
+ await ensureLoaded();
696
+ const rule = userReplacements.find((r) => r.id === id);
697
+ if (!rule) return null;
698
+ if (rule.isSystem) {
699
+ consola.warn("Cannot update system replacement rule");
700
+ return null;
701
+ }
702
+ if (updates.name !== void 0) rule.name = updates.name;
703
+ if (updates.pattern !== void 0) rule.pattern = updates.pattern;
704
+ if (updates.replacement !== void 0) rule.replacement = updates.replacement;
705
+ if (updates.isRegex !== void 0) rule.isRegex = updates.isRegex;
706
+ if (updates.enabled !== void 0) rule.enabled = updates.enabled;
707
+ await saveReplacements();
708
+ consola.info(`Updated replacement rule: ${rule.name || rule.id}`);
709
+ return rule;
710
+ }
711
+ /**
670
712
  * Toggle a replacement rule on/off
671
713
  */
672
714
  async function toggleReplacement(id) {
@@ -693,18 +735,32 @@ async function clearUserReplacements() {
693
735
  consola.info("Cleared all user replacement rules");
694
736
  }
695
737
  /**
696
- * Apply a single replacement rule to text
738
+ * Apply a single replacement rule to text and return info about whether it matched
697
739
  */
698
740
  function applyRule(text, rule) {
699
- if (!rule.enabled) return text;
741
+ if (!rule.enabled) return {
742
+ result: text,
743
+ matched: false
744
+ };
700
745
  if (rule.isRegex) try {
701
746
  const regex = new RegExp(rule.pattern, "g");
702
- return text.replace(regex, rule.replacement);
747
+ const result$1 = text.replace(regex, rule.replacement);
748
+ return {
749
+ result: result$1,
750
+ matched: result$1 !== text
751
+ };
703
752
  } catch {
704
753
  consola.warn(`Invalid regex pattern in rule ${rule.id}: ${rule.pattern}`);
705
- return text;
754
+ return {
755
+ result: text,
756
+ matched: false
757
+ };
706
758
  }
707
- return text.split(rule.pattern).join(rule.replacement);
759
+ const result = text.split(rule.pattern).join(rule.replacement);
760
+ return {
761
+ result,
762
+ matched: result !== text
763
+ };
708
764
  }
709
765
  /**
710
766
  * Apply all replacement rules to text
@@ -712,11 +768,15 @@ function applyRule(text, rule) {
712
768
  async function applyReplacements(text) {
713
769
  let result = text;
714
770
  const allRules = await getAllReplacements();
771
+ const appliedRules = [];
715
772
  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}`);
773
+ const { result: newResult, matched } = applyRule(result, rule);
774
+ if (matched) {
775
+ result = newResult;
776
+ appliedRules.push(rule.name || rule.id);
777
+ }
719
778
  }
779
+ if (appliedRules.length > 0) consola.info(`Replacements applied: ${appliedRules.join(", ")}`);
720
780
  return result;
721
781
  }
722
782
  /**
@@ -753,8 +813,9 @@ function formatRule(rule, index) {
753
813
  const status = rule.enabled ? "✓" : "✗";
754
814
  const type = rule.isRegex ? "regex" : "string";
755
815
  const system = rule.isSystem ? " [system]" : "";
816
+ const name = rule.name ? ` "${rule.name}"` : "";
756
817
  const replacement = rule.replacement || "(empty)";
757
- return `${index + 1}. [${status}] (${type})${system} "${rule.pattern}" → "${replacement}"`;
818
+ return `${index + 1}. [${status}] (${type})${system}${name} "${rule.pattern}" → "${replacement}"`;
758
819
  }
759
820
  async function listReplacements() {
760
821
  const all = await getAllReplacements();
@@ -767,6 +828,14 @@ async function listReplacements() {
767
828
  console.log();
768
829
  }
769
830
  async function addNewReplacement() {
831
+ const name = await consola.prompt("Name (optional, short description):", {
832
+ type: "text",
833
+ default: ""
834
+ });
835
+ if (typeof name === "symbol") {
836
+ consola.info("Cancelled.");
837
+ return;
838
+ }
770
839
  const matchType = await consola.prompt("Match type:", {
771
840
  type: "select",
772
841
  options: [{
@@ -800,8 +869,87 @@ async function addNewReplacement() {
800
869
  consola.info("Cancelled.");
801
870
  return;
802
871
  }
803
- const rule = await addReplacement(pattern, replacement, matchType === "regex");
804
- consola.success(`Added rule: ${rule.id}`);
872
+ const rule = await addReplacement(pattern, replacement, matchType === "regex", name || void 0);
873
+ consola.success(`Added rule: ${rule.name || rule.id}`);
874
+ }
875
+ async function editExistingReplacement() {
876
+ const userRules = await getUserReplacements();
877
+ if (userRules.length === 0) {
878
+ consola.info("No user rules to edit.");
879
+ return;
880
+ }
881
+ const options = userRules.map((rule$1, i) => ({
882
+ label: formatRule(rule$1, i),
883
+ value: rule$1.id
884
+ }));
885
+ const selected = await consola.prompt("Select rule to edit:", {
886
+ type: "select",
887
+ options
888
+ });
889
+ if (typeof selected === "symbol") {
890
+ consola.info("Cancelled.");
891
+ return;
892
+ }
893
+ const rule = userRules.find((r) => r.id === selected);
894
+ if (!rule) {
895
+ consola.error("Rule not found.");
896
+ return;
897
+ }
898
+ consola.info(`\nEditing rule: ${rule.name || rule.id}`);
899
+ consola.info("Press Enter to keep current value.\n");
900
+ const name = await consola.prompt("Name:", {
901
+ type: "text",
902
+ default: rule.name || ""
903
+ });
904
+ if (typeof name === "symbol") {
905
+ consola.info("Cancelled.");
906
+ return;
907
+ }
908
+ const matchType = await consola.prompt("Match type:", {
909
+ type: "select",
910
+ options: [{
911
+ label: "String (exact match)",
912
+ value: "string"
913
+ }, {
914
+ label: "Regex (regular expression)",
915
+ value: "regex"
916
+ }],
917
+ initial: rule.isRegex ? "regex" : "string"
918
+ });
919
+ if (typeof matchType === "symbol") {
920
+ consola.info("Cancelled.");
921
+ return;
922
+ }
923
+ const pattern = await consola.prompt("Pattern to match:", {
924
+ type: "text",
925
+ default: rule.pattern
926
+ });
927
+ if (typeof pattern === "symbol" || !pattern) {
928
+ consola.info("Cancelled.");
929
+ return;
930
+ }
931
+ if (matchType === "regex") try {
932
+ new RegExp(pattern);
933
+ } catch {
934
+ consola.error(`Invalid regex pattern: ${pattern}`);
935
+ return;
936
+ }
937
+ const replacement = await consola.prompt("Replacement text:", {
938
+ type: "text",
939
+ default: rule.replacement
940
+ });
941
+ if (typeof replacement === "symbol") {
942
+ consola.info("Cancelled.");
943
+ return;
944
+ }
945
+ const updated = await updateReplacement(selected, {
946
+ name: name || void 0,
947
+ pattern,
948
+ replacement,
949
+ isRegex: matchType === "regex"
950
+ });
951
+ if (updated) consola.success(`Updated rule: ${updated.name || updated.id}`);
952
+ else consola.error("Failed to update rule.");
805
953
  }
806
954
  async function removeExistingReplacement() {
807
955
  const userRules = await getUserReplacements();
@@ -884,6 +1032,10 @@ async function mainMenu() {
884
1032
  label: "➕ Add new rule",
885
1033
  value: "add"
886
1034
  },
1035
+ {
1036
+ label: "✏️ Edit rule",
1037
+ value: "edit"
1038
+ },
887
1039
  {
888
1040
  label: "➖ Remove rule",
889
1041
  value: "remove"
@@ -914,6 +1066,9 @@ async function mainMenu() {
914
1066
  case "add":
915
1067
  await addNewReplacement();
916
1068
  break;
1069
+ case "edit":
1070
+ await editExistingReplacement();
1071
+ break;
917
1072
  case "remove":
918
1073
  await removeExistingReplacement();
919
1074
  break;
@@ -1155,6 +1310,39 @@ function getStatusColor(status) {
1155
1310
  return colors.green;
1156
1311
  }
1157
1312
  /**
1313
+ * Log raw HTTP request details (for debug mode)
1314
+ */
1315
+ async function logRawRequest(c) {
1316
+ const method = c.req.method;
1317
+ const url = c.req.url;
1318
+ const headers = Object.fromEntries(c.req.raw.headers.entries());
1319
+ const lines = [];
1320
+ lines.push(`${colors.magenta}${colors.bold}[DEBUG] Incoming Request${colors.reset}`);
1321
+ lines.push(`${colors.cyan}${method}${colors.reset} ${url}`);
1322
+ lines.push(`${colors.dim}Headers:${colors.reset}`);
1323
+ for (const [key, value] of Object.entries(headers)) {
1324
+ const displayValue = key.toLowerCase().includes("authorization") ? `${value.slice(0, 20)}...` : value;
1325
+ lines.push(` ${colors.gray}${key}:${colors.reset} ${displayValue}`);
1326
+ }
1327
+ if (method !== "GET" && method !== "HEAD") try {
1328
+ const body = await c.req.raw.clone().text();
1329
+ if (body) try {
1330
+ const parsed = JSON.parse(body);
1331
+ const sanitized = {};
1332
+ for (const [key, value] of Object.entries(parsed)) if (key === "messages" || key === "prompt") sanitized[key] = `[${Array.isArray(value) ? value.length : 1} items omitted]`;
1333
+ else sanitized[key] = value;
1334
+ lines.push(`${colors.dim}Body (sanitized):${colors.reset}`);
1335
+ lines.push(` ${JSON.stringify(sanitized, null, 2).split("\n").join("\n ")}`);
1336
+ } catch {
1337
+ lines.push(`${colors.dim}Body:${colors.reset} [${body.length} bytes]`);
1338
+ }
1339
+ } catch {
1340
+ lines.push(`${colors.dim}Body:${colors.reset} [unable to read]`);
1341
+ }
1342
+ lines.push(`${colors.dim}${"─".repeat(60)}${colors.reset}`);
1343
+ console.log(lines.join("\n"));
1344
+ }
1345
+ /**
1158
1346
  * Set request context for logging
1159
1347
  */
1160
1348
  function setRequestContext(c, ctx) {
@@ -1168,6 +1356,7 @@ function setRequestContext(c, ctx) {
1168
1356
  * Custom request logger middleware
1169
1357
  */
1170
1358
  async function requestLogger(c, next) {
1359
+ if (state.debug) await logRawRequest(c);
1171
1360
  const startTime = Date.now();
1172
1361
  const method = c.req.method;
1173
1362
  const path$1 = c.req.path + (c.req.raw.url.includes("?") ? "?" + c.req.raw.url.split("?")[1] : "");
@@ -1202,6 +1391,18 @@ const awaitApproval = async () => {
1202
1391
  if (!await consola.prompt(`Accept incoming request?`, { type: "confirm" })) throw new HTTPError("Request rejected", Response.json({ message: "Request rejected" }, { status: 403 }));
1203
1392
  };
1204
1393
 
1394
+ //#endregion
1395
+ //#region src/lib/model-resolver.ts
1396
+ /**
1397
+ * Normalize a model name by converting dashes to dots between numbers.
1398
+ * e.g., "claude-opus-4-5" -> "claude-opus-4.5"
1399
+ * "gpt-4-1" -> "gpt-4.1"
1400
+ * "gpt-5-1-codex" -> "gpt-5.1-codex"
1401
+ */
1402
+ function normalizeModelName(model) {
1403
+ return model.replace(/(\d)-(\d)/g, (_, p1, p2) => `${p1}.${p2}`);
1404
+ }
1405
+
1205
1406
  //#endregion
1206
1407
  //#region src/lib/rate-limit.ts
1207
1408
  async function checkRateLimit(state$1) {
@@ -1454,6 +1655,10 @@ async function handleCompletion$1(c) {
1454
1655
  await checkRateLimit(state);
1455
1656
  const rawPayload = await c.req.json();
1456
1657
  let payload = await applyReplacementsToPayload(rawPayload);
1658
+ payload = {
1659
+ ...payload,
1660
+ model: normalizeModelName(payload.model)
1661
+ };
1457
1662
  consola.debug("Request payload:", JSON.stringify(payload).slice(-400));
1458
1663
  if (isAzureOpenAIModel(payload.model)) {
1459
1664
  if (!state.azureOpenAIConfig) return c.json({ error: "Azure OpenAI not configured" }, 500);
@@ -1463,7 +1668,7 @@ async function handleCompletion$1(c) {
1463
1668
  });
1464
1669
  if (state.manualApprove) await awaitApproval();
1465
1670
  const response$1 = await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, payload);
1466
- if (isNonStreaming$1(response$1)) {
1671
+ if (isNonStreaming(response$1)) {
1467
1672
  consola.debug("Non-streaming response:", JSON.stringify(response$1));
1468
1673
  if (response$1.usage) setRequestContext(c, {
1469
1674
  inputTokens: response$1.usage.prompt_tokens,
@@ -1494,7 +1699,7 @@ async function handleCompletion$1(c) {
1494
1699
  try {
1495
1700
  if (selectedModel) {
1496
1701
  const tokenCount = await getTokenCount(payload, selectedModel);
1497
- setRequestContext(c, { inputTokens: tokenCount });
1702
+ setRequestContext(c, { inputTokens: tokenCount.input });
1498
1703
  }
1499
1704
  } catch (error) {
1500
1705
  consola.warn("Failed to calculate token count:", error);
@@ -1508,7 +1713,7 @@ async function handleCompletion$1(c) {
1508
1713
  consola.debug("Set max_tokens to:", JSON.stringify(payload.max_tokens));
1509
1714
  }
1510
1715
  const response = await createChatCompletions(payload);
1511
- if (isNonStreaming$1(response)) {
1716
+ if (isNonStreaming(response)) {
1512
1717
  consola.debug("Non-streaming response:", JSON.stringify(response));
1513
1718
  if (response.usage) setRequestContext(c, {
1514
1719
  inputTokens: response.usage.prompt_tokens,
@@ -1531,7 +1736,7 @@ async function handleCompletion$1(c) {
1531
1736
  }
1532
1737
  });
1533
1738
  }
1534
- const isNonStreaming$1 = (response) => Object.hasOwn(response, "choices");
1739
+ const isNonStreaming = (response) => Object.hasOwn(response, "choices");
1535
1740
 
1536
1741
  //#endregion
1537
1742
  //#region src/routes/chat-completions/route.ts
@@ -1599,8 +1804,8 @@ function translateToOpenAI(payload) {
1599
1804
  };
1600
1805
  }
1601
1806
  function translateModelName(model) {
1602
- if (model.startsWith("claude-sonnet-4-")) return model.replace(/^claude-sonnet-4-.*/, "claude-sonnet-4");
1603
- else if (model.startsWith("claude-opus-")) return model.replace(/^claude-opus-4-.*/, "claude-opus-4");
1807
+ if (model.match(/^claude-sonnet-4-\d{8}/)) return "claude-sonnet-4";
1808
+ else if (model.match(/^claude-opus-4-\d{8}/)) return "claude-opus-4";
1604
1809
  return model;
1605
1810
  }
1606
1811
  function translateAnthropicMessagesToOpenAI(anthropicMessages, system) {
@@ -1821,12 +2026,7 @@ function translateChunkToAnthropicEvents(chunk, state$1) {
1821
2026
  content: [],
1822
2027
  model: chunk.model,
1823
2028
  stop_reason: null,
1824
- stop_sequence: null,
1825
- usage: {
1826
- input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
1827
- output_tokens: 0,
1828
- ...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
1829
- }
2029
+ stop_sequence: null
1830
2030
  }
1831
2031
  });
1832
2032
  state$1.messageStartSent = true;
@@ -1908,6 +2108,9 @@ function translateChunkToAnthropicEvents(chunk, state$1) {
1908
2108
  });
1909
2109
  state$1.contentBlockOpen = false;
1910
2110
  }
2111
+ const inputTokens = chunk.usage?.prompt_tokens ?? 0;
2112
+ const outputTokens = chunk.usage?.completion_tokens ?? 0;
2113
+ const cachedTokens = chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0;
1911
2114
  events$1.push({
1912
2115
  type: "message_delta",
1913
2116
  delta: {
@@ -1915,9 +2118,10 @@ function translateChunkToAnthropicEvents(chunk, state$1) {
1915
2118
  stop_sequence: null
1916
2119
  },
1917
2120
  usage: {
1918
- input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
1919
- output_tokens: chunk.usage?.completion_tokens ?? 0,
1920
- ...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
2121
+ input_tokens: inputTokens,
2122
+ output_tokens: outputTokens,
2123
+ cache_creation_input_tokens: 0,
2124
+ cache_read_input_tokens: cachedTokens
1921
2125
  }
1922
2126
  }, { type: "message_stop" });
1923
2127
  }
@@ -1931,98 +2135,75 @@ async function handleCompletion(c) {
1931
2135
  const anthropicPayload = await c.req.json();
1932
2136
  consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
1933
2137
  const translatedPayload = translateToOpenAI(anthropicPayload);
1934
- const openAIPayload = await applyReplacementsToPayload(translatedPayload);
2138
+ let openAIPayload = await applyReplacementsToPayload(translatedPayload);
2139
+ openAIPayload = {
2140
+ ...openAIPayload,
2141
+ model: normalizeModelName(openAIPayload.model)
2142
+ };
1935
2143
  consola.debug("Translated OpenAI request payload:", JSON.stringify(openAIPayload));
1936
2144
  if (state.manualApprove) await awaitApproval();
1937
- if (isAzureOpenAIModel(openAIPayload.model)) {
2145
+ const isAzureModel = isAzureOpenAIModel(openAIPayload.model);
2146
+ if (isAzureModel) {
1938
2147
  if (!state.azureOpenAIConfig) return c.json({ error: "Azure OpenAI not configured" }, 500);
1939
2148
  setRequestContext(c, {
1940
2149
  provider: "Azure OpenAI",
1941
2150
  model: openAIPayload.model
1942
2151
  });
1943
- const response$1 = await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, openAIPayload);
1944
- if (isNonStreaming(response$1)) {
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
- });
1950
- const anthropicResponse = translateToAnthropic(response$1);
1951
- consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
1952
- return c.json(anthropicResponse);
1953
- }
1954
- consola.debug("Streaming response from Azure OpenAI");
2152
+ } else setRequestContext(c, {
2153
+ provider: "Copilot",
2154
+ model: openAIPayload.model
2155
+ });
2156
+ if (anthropicPayload.stream) {
2157
+ const streamPayload = {
2158
+ ...openAIPayload,
2159
+ stream: true,
2160
+ stream_options: { include_usage: true }
2161
+ };
2162
+ const eventStream = isAzureModel ? await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, streamPayload) : await createChatCompletions(streamPayload);
1955
2163
  return streamSSE(c, async (stream) => {
1956
2164
  const streamState = {
1957
2165
  messageStartSent: false,
1958
- contentBlockIndex: 0,
1959
2166
  contentBlockOpen: false,
2167
+ contentBlockIndex: 0,
1960
2168
  toolCalls: {}
1961
2169
  };
1962
- for await (const rawEvent of response$1) {
1963
- consola.debug("Azure OpenAI raw stream event:", JSON.stringify(rawEvent));
1964
- if (rawEvent.data === "[DONE]") break;
1965
- if (!rawEvent.data) continue;
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
- });
1971
- const events$1 = translateChunkToAnthropicEvents(chunk, streamState);
1972
- for (const event of events$1) {
1973
- consola.debug("Translated Anthropic event:", JSON.stringify(event));
1974
- await stream.writeSSE({
1975
- event: event.type,
1976
- data: JSON.stringify(event)
2170
+ for await (const event of eventStream) {
2171
+ if (!event.data || event.data === "[DONE]") continue;
2172
+ try {
2173
+ const chunk = JSON.parse(event.data);
2174
+ consola.debug("OpenAI chunk:", JSON.stringify(chunk));
2175
+ const anthropicEvents = translateChunkToAnthropicEvents(chunk, streamState);
2176
+ for (const anthropicEvent of anthropicEvents) {
2177
+ consola.debug("Anthropic event:", JSON.stringify(anthropicEvent));
2178
+ await stream.writeSSE({
2179
+ event: anthropicEvent.type,
2180
+ data: JSON.stringify(anthropicEvent)
2181
+ });
2182
+ }
2183
+ if (chunk.usage) setRequestContext(c, {
2184
+ inputTokens: chunk.usage.prompt_tokens,
2185
+ outputTokens: chunk.usage.completion_tokens
1977
2186
  });
2187
+ } catch (error) {
2188
+ consola.error("Failed to parse chunk:", error, event.data);
1978
2189
  }
1979
2190
  }
1980
2191
  });
1981
2192
  }
1982
- setRequestContext(c, {
1983
- provider: "Copilot",
1984
- model: openAIPayload.model
1985
- });
1986
- const response = await createChatCompletions(openAIPayload);
1987
- if (isNonStreaming(response)) {
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
- });
1993
- const anthropicResponse = translateToAnthropic(response);
1994
- consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
1995
- return c.json(anthropicResponse);
1996
- }
1997
- consola.debug("Streaming response from Copilot");
1998
- return streamSSE(c, async (stream) => {
1999
- const streamState = {
2000
- messageStartSent: false,
2001
- contentBlockIndex: 0,
2002
- contentBlockOpen: false,
2003
- toolCalls: {}
2004
- };
2005
- for await (const rawEvent of response) {
2006
- consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
2007
- if (rawEvent.data === "[DONE]") break;
2008
- if (!rawEvent.data) continue;
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
- });
2014
- const events$1 = translateChunkToAnthropicEvents(chunk, streamState);
2015
- for (const event of events$1) {
2016
- consola.debug("Translated Anthropic event:", JSON.stringify(event));
2017
- await stream.writeSSE({
2018
- event: event.type,
2019
- data: JSON.stringify(event)
2020
- });
2021
- }
2022
- }
2193
+ const nonStreamPayload = {
2194
+ ...openAIPayload,
2195
+ stream: false
2196
+ };
2197
+ const response = isAzureModel ? await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, nonStreamPayload) : await createChatCompletions(nonStreamPayload);
2198
+ consola.debug("Response from upstream:", JSON.stringify(response).slice(-400));
2199
+ if (response.usage) setRequestContext(c, {
2200
+ inputTokens: response.usage.prompt_tokens,
2201
+ outputTokens: response.usage.completion_tokens
2023
2202
  });
2203
+ const anthropicResponse = translateToAnthropic(response);
2204
+ consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
2205
+ return c.json(anthropicResponse);
2024
2206
  }
2025
- const isNonStreaming = (response) => Object.hasOwn(response, "choices");
2026
2207
 
2027
2208
  //#endregion
2028
2209
  //#region src/routes/messages/route.ts
@@ -2089,7 +2270,7 @@ replacementsRoute.get("/", async (c) => {
2089
2270
  replacementsRoute.post("/", async (c) => {
2090
2271
  const body = await c.req.json();
2091
2272
  if (!body.pattern) return c.json({ error: "Pattern is required" }, 400);
2092
- const rule = await addReplacement(body.pattern, body.replacement ?? "", body.isRegex ?? false);
2273
+ const rule = await addReplacement(body.pattern, body.replacement ?? "", body.isRegex ?? false, body.name);
2093
2274
  return c.json(rule, 201);
2094
2275
  });
2095
2276
  replacementsRoute.delete("/:id", async (c) => {
@@ -2097,6 +2278,13 @@ replacementsRoute.delete("/:id", async (c) => {
2097
2278
  if (!await removeReplacement(id)) return c.json({ error: "Replacement not found or is a system rule" }, 404);
2098
2279
  return c.json({ success: true });
2099
2280
  });
2281
+ replacementsRoute.patch("/:id", async (c) => {
2282
+ const id = c.req.param("id");
2283
+ const body = await c.req.json();
2284
+ const rule = await updateReplacement(id, body);
2285
+ if (!rule) return c.json({ error: "Replacement not found or is a system rule" }, 404);
2286
+ return c.json(rule);
2287
+ });
2100
2288
  replacementsRoute.patch("/:id/toggle", async (c) => {
2101
2289
  const id = c.req.param("id");
2102
2290
  const rule = await toggleReplacement(id);
@@ -2171,6 +2359,8 @@ async function runServer(options) {
2171
2359
  state.rateLimitSeconds = options.rateLimit;
2172
2360
  state.rateLimitWait = options.rateLimitWait;
2173
2361
  state.showToken = options.showToken;
2362
+ state.debug = options.debug;
2363
+ if (options.debug) consola.info("Debug mode enabled - raw HTTP requests will be logged");
2174
2364
  await ensurePaths();
2175
2365
  await cacheVSCodeVersion();
2176
2366
  if (options.githubToken) {
@@ -2284,6 +2474,12 @@ const start = defineCommand({
2284
2474
  type: "boolean",
2285
2475
  default: false,
2286
2476
  description: "Disable SSL certificate verification (for corporate proxies with self-signed certs)"
2477
+ },
2478
+ debug: {
2479
+ alias: "d",
2480
+ type: "boolean",
2481
+ default: false,
2482
+ description: "Log raw HTTP requests received by the server (headers, method, path)"
2287
2483
  }
2288
2484
  },
2289
2485
  run({ args }) {
@@ -2300,7 +2496,8 @@ const start = defineCommand({
2300
2496
  claudeCode: args["claude-code"],
2301
2497
  showToken: args["show-token"],
2302
2498
  proxyEnv: args["proxy-env"],
2303
- insecure: args.insecure
2499
+ insecure: args.insecure,
2500
+ debug: args.debug
2304
2501
  });
2305
2502
  }
2306
2503
  });