@ashsec/copilot-api 0.7.8 → 0.7.11

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
@@ -17,6 +17,85 @@ import { Hono } from "hono";
17
17
  import { cors } from "hono/cors";
18
18
  import { streamSSE } from "hono/streaming";
19
19
 
20
+ //#region package.json
21
+ var name = "@ashsec/copilot-api";
22
+ var version = "0.7.11";
23
+ var description = "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!";
24
+ var keywords = [
25
+ "proxy",
26
+ "github-copilot",
27
+ "openai-compatible"
28
+ ];
29
+ var homepage = "https://github.com/ericc-ch/copilot-api";
30
+ var bugs = "https://github.com/ericc-ch/copilot-api/issues";
31
+ var repository = {
32
+ "type": "git",
33
+ "url": "git+https://github.com/ericc-ch/copilot-api.git"
34
+ };
35
+ var author = "Erick Christian <erickchristian48@gmail.com>";
36
+ var type = "module";
37
+ var bin = { "copilot-api": "./dist/main.js" };
38
+ var files = ["dist"];
39
+ var scripts = {
40
+ "build": "tsdown",
41
+ "dev": "bun run --watch ./src/main.ts",
42
+ "knip": "knip-bun",
43
+ "lint": "eslint --cache",
44
+ "lint:all": "eslint --cache .",
45
+ "prepack": "bun run build",
46
+ "prepare": "simple-git-hooks",
47
+ "release": "bumpp && bun publish --access public",
48
+ "start": "NODE_ENV=production bun run ./src/main.ts",
49
+ "typecheck": "tsc"
50
+ };
51
+ var simple_git_hooks = { "pre-commit": "bunx lint-staged" };
52
+ var lint_staged = { "*": "bun run lint --fix" };
53
+ var dependencies = {
54
+ "citty": "^0.1.6",
55
+ "clipboardy": "^5.0.0",
56
+ "consola": "^3.4.2",
57
+ "fetch-event-stream": "^0.1.5",
58
+ "gpt-tokenizer": "^3.0.1",
59
+ "hono": "^4.9.9",
60
+ "proxy-from-env": "^1.1.0",
61
+ "srvx": "^0.8.9",
62
+ "tiny-invariant": "^1.3.3",
63
+ "undici": "^7.16.0",
64
+ "zod": "^4.1.11"
65
+ };
66
+ var devDependencies = {
67
+ "@echristian/eslint-config": "^0.0.54",
68
+ "@types/bun": "^1.2.23",
69
+ "@types/proxy-from-env": "^1.0.4",
70
+ "bumpp": "^10.2.3",
71
+ "eslint": "^9.37.0",
72
+ "knip": "^5.64.1",
73
+ "lint-staged": "^16.2.3",
74
+ "prettier-plugin-packagejson": "^2.5.19",
75
+ "simple-git-hooks": "^2.13.1",
76
+ "tsdown": "^0.15.6",
77
+ "typescript": "^5.9.3"
78
+ };
79
+ var package_default = {
80
+ name,
81
+ version,
82
+ description,
83
+ keywords,
84
+ homepage,
85
+ bugs,
86
+ repository,
87
+ author,
88
+ type,
89
+ bin,
90
+ files,
91
+ scripts,
92
+ "simple-git-hooks": simple_git_hooks,
93
+ "lint-staged": lint_staged,
94
+ dependencies,
95
+ devDependencies
96
+ };
97
+
98
+ //#endregion
20
99
  //#region src/lib/paths.ts
21
100
  const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api");
22
101
  const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token");
@@ -105,7 +184,7 @@ var HTTPError = class extends Error {
105
184
  }
106
185
  };
107
186
  function isContentFilterError(obj) {
108
- return typeof obj === "object" && obj !== null && "error" in obj && typeof obj.error === "object" && obj.error?.code === "content_filter";
187
+ return typeof obj === "object" && obj !== null && "error" in obj && typeof obj.error === "object" && obj.error.code === "content_filter";
109
188
  }
110
189
  async function forwardError(c, error) {
111
190
  consola.error("Error occurred:", error);
@@ -575,13 +654,13 @@ const checkUsage = defineCommand({
575
654
  const premiumUsed = premiumTotal - premium.remaining;
576
655
  const premiumPercentUsed = premiumTotal > 0 ? premiumUsed / premiumTotal * 100 : 0;
577
656
  const premiumPercentRemaining = premium.percent_remaining;
578
- function summarizeQuota(name, snap) {
579
- if (!snap) return `${name}: N/A`;
657
+ function summarizeQuota(name$1, snap) {
658
+ if (!snap) return `${name$1}: N/A`;
580
659
  const total = snap.entitlement;
581
660
  const used = total - snap.remaining;
582
661
  const percentUsed = total > 0 ? used / total * 100 : 0;
583
662
  const percentRemaining = snap.percent_remaining;
584
- return `${name}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`;
663
+ return `${name$1}: ${used}/${total} used (${percentUsed.toFixed(1)}% used, ${percentRemaining.toFixed(1)}% remaining)`;
585
664
  }
586
665
  const premiumLine = `Premium: ${premiumUsed}/${premiumTotal} used (${premiumPercentUsed.toFixed(1)}% used, ${premiumPercentRemaining.toFixed(1)}% remaining)`;
587
666
  const chatLine = summarizeQuota("Chat", usage.quota_snapshots.chat);
@@ -599,7 +678,7 @@ const checkUsage = defineCommand({
599
678
  const SYSTEM_REPLACEMENTS = [{
600
679
  id: "system-anthropic-billing",
601
680
  name: "Remove Anthropic billing header",
602
- pattern: "x-anthropic-billing-header:[^\\n]*\\n?",
681
+ pattern: String.raw`x-anthropic-billing-header:[^\n]*\n?`,
603
682
  replacement: "",
604
683
  isRegex: true,
605
684
  enabled: true,
@@ -656,11 +735,12 @@ async function getUserReplacements() {
656
735
  /**
657
736
  * Add a new user replacement rule
658
737
  */
659
- async function addReplacement(pattern, replacement, isRegex = false, name) {
738
+ async function addReplacement(pattern, replacement, options) {
739
+ const { isRegex = false, name: name$1 } = options ?? {};
660
740
  await ensureLoaded();
661
741
  const rule = {
662
742
  id: `user-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
663
- name,
743
+ name: name$1,
664
744
  pattern,
665
745
  replacement,
666
746
  isRegex,
@@ -811,11 +891,11 @@ async function applyReplacementsToPayload(payload) {
811
891
  //#region src/config.ts
812
892
  function formatRule(rule, index) {
813
893
  const status = rule.enabled ? "✓" : "✗";
814
- const type = rule.isRegex ? "regex" : "string";
894
+ const type$1 = rule.isRegex ? "regex" : "string";
815
895
  const system = rule.isSystem ? " [system]" : "";
816
- const name = rule.name ? ` "${rule.name}"` : "";
896
+ const name$1 = rule.name ? ` "${rule.name}"` : "";
817
897
  const replacement = rule.replacement || "(empty)";
818
- return `${index + 1}. [${status}] (${type})${system}${name} "${rule.pattern}" → "${replacement}"`;
898
+ return `${index + 1}. [${status}] (${type$1})${system}${name$1} "${rule.pattern}" → "${replacement}"`;
819
899
  }
820
900
  async function listReplacements() {
821
901
  const all = await getAllReplacements();
@@ -828,11 +908,11 @@ async function listReplacements() {
828
908
  console.log();
829
909
  }
830
910
  async function addNewReplacement() {
831
- const name = await consola.prompt("Name (optional, short description):", {
911
+ const name$1 = await consola.prompt("Name (optional, short description):", {
832
912
  type: "text",
833
913
  default: ""
834
914
  });
835
- if (typeof name === "symbol") {
915
+ if (typeof name$1 === "symbol") {
836
916
  consola.info("Cancelled.");
837
917
  return;
838
918
  }
@@ -869,7 +949,7 @@ async function addNewReplacement() {
869
949
  consola.info("Cancelled.");
870
950
  return;
871
951
  }
872
- const rule = await addReplacement(pattern, replacement, matchType === "regex", name || void 0);
952
+ const rule = await addReplacement(pattern, replacement, matchType === "regex", name$1 || void 0);
873
953
  consola.success(`Added rule: ${rule.name || rule.id}`);
874
954
  }
875
955
  async function editExistingReplacement() {
@@ -897,11 +977,11 @@ async function editExistingReplacement() {
897
977
  }
898
978
  consola.info(`\nEditing rule: ${rule.name || rule.id}`);
899
979
  consola.info("Press Enter to keep current value.\n");
900
- const name = await consola.prompt("Name:", {
980
+ const name$1 = await consola.prompt("Name:", {
901
981
  type: "text",
902
982
  default: rule.name || ""
903
983
  });
904
- if (typeof name === "symbol") {
984
+ if (typeof name$1 === "symbol") {
905
985
  consola.info("Cancelled.");
906
986
  return;
907
987
  }
@@ -943,7 +1023,7 @@ async function editExistingReplacement() {
943
1023
  return;
944
1024
  }
945
1025
  const updated = await updateReplacement(selected, {
946
- name: name || void 0,
1026
+ name: name$1 || void 0,
947
1027
  pattern,
948
1028
  replacement,
949
1029
  isRegex: matchType === "regex"
@@ -1128,9 +1208,9 @@ async function checkTokenExists() {
1128
1208
  }
1129
1209
  }
1130
1210
  async function getDebugInfo() {
1131
- const [version, tokenExists] = await Promise.all([getPackageVersion(), checkTokenExists()]);
1211
+ const [version$1, tokenExists] = await Promise.all([getPackageVersion(), checkTokenExists()]);
1132
1212
  return {
1133
- version,
1213
+ version: version$1,
1134
1214
  runtime: getRuntimeInfo(),
1135
1215
  paths: {
1136
1216
  APP_DIR: PATHS.APP_DIR,
@@ -1310,6 +1390,14 @@ function getStatusColor(status) {
1310
1390
  return colors.green;
1311
1391
  }
1312
1392
  /**
1393
+ * Sanitize request body by omitting large message/prompt arrays
1394
+ */
1395
+ function sanitizeRequestBody(parsed) {
1396
+ const sanitized = {};
1397
+ for (const [key, value] of Object.entries(parsed)) sanitized[key] = key === "messages" || key === "prompt" ? `[${Array.isArray(value) ? value.length : 1} items omitted]` : value;
1398
+ return sanitized;
1399
+ }
1400
+ /**
1313
1401
  * Log raw HTTP request details (for debug mode)
1314
1402
  */
1315
1403
  async function logRawRequest(c) {
@@ -1317,9 +1405,7 @@ async function logRawRequest(c) {
1317
1405
  const url = c.req.url;
1318
1406
  const headers = Object.fromEntries(c.req.raw.headers.entries());
1319
1407
  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}`);
1408
+ lines.push(`${colors.magenta}${colors.bold}[DEBUG] Incoming Request${colors.reset}`, `${colors.cyan}${method}${colors.reset} ${url}`, `${colors.dim}Headers:${colors.reset}`);
1323
1409
  for (const [key, value] of Object.entries(headers)) {
1324
1410
  const displayValue = key.toLowerCase().includes("authorization") ? `${value.slice(0, 20)}...` : value;
1325
1411
  lines.push(` ${colors.gray}${key}:${colors.reset} ${displayValue}`);
@@ -1328,11 +1414,8 @@ async function logRawRequest(c) {
1328
1414
  const body = await c.req.raw.clone().text();
1329
1415
  if (body) try {
1330
1416
  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 ")}`);
1417
+ const sanitized = sanitizeRequestBody(parsed);
1418
+ lines.push(`${colors.dim}Body (sanitized):${colors.reset}`, ` ${JSON.stringify(sanitized, null, 2).split("\n").join("\n ")}`);
1336
1419
  } catch {
1337
1420
  lines.push(`${colors.dim}Body:${colors.reset} [${body.length} bytes]`);
1338
1421
  }
@@ -1371,7 +1454,7 @@ async function requestLogger(c, next) {
1371
1454
  const statusBadge = `${statusColor}${status}${colors.reset}`;
1372
1455
  const durationStr = `${colors.cyan}${duration}s${colors.reset}`;
1373
1456
  lines.push(`${colors.bold}${method}${colors.reset} ${path$1} ${statusBadge} ${durationStr}`);
1374
- if (ctx?.provider && ctx?.model) {
1457
+ if (ctx?.provider && ctx.model) {
1375
1458
  const providerColor = ctx.provider === "Azure OpenAI" ? colors.blue : colors.magenta;
1376
1459
  lines.push(` ${colors.gray}Provider:${colors.reset} ${providerColor}${ctx.provider}${colors.reset} ${colors.gray}->${colors.reset} ${colors.white}${ctx.model}${colors.reset}`);
1377
1460
  }
@@ -1400,7 +1483,7 @@ const awaitApproval = async () => {
1400
1483
  * "gpt-5-1-codex" -> "gpt-5.1-codex"
1401
1484
  */
1402
1485
  function normalizeModelName(model) {
1403
- return model.replace(/(\d)-(\d)/g, (_, p1, p2) => `${p1}.${p2}`);
1486
+ return model.replaceAll(/(\d)-(\d)/g, (_, p1, p2) => `${p1}.${p2}`);
1404
1487
  }
1405
1488
 
1406
1489
  //#endregion
@@ -1804,8 +1887,8 @@ function translateToOpenAI(payload) {
1804
1887
  };
1805
1888
  }
1806
1889
  function translateModelName(model) {
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";
1890
+ if (/^claude-sonnet-4-\d{8}/.test(model)) return "claude-sonnet-4";
1891
+ else if (/^claude-opus-4-\d{8}/.test(model)) return "claude-opus-4";
1809
1892
  return model;
1810
1893
  }
1811
1894
  function translateAnthropicMessagesToOpenAI(anthropicMessages, system) {
@@ -1923,23 +2006,35 @@ function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice) {
1923
2006
  }
1924
2007
  }
1925
2008
  function translateToAnthropic(response, originalModel) {
2009
+ const { contentBlocks, stopReason } = extractContentFromChoices(response);
2010
+ return buildAnthropicResponse(response, {
2011
+ contentBlocks,
2012
+ stopReason,
2013
+ originalModel
2014
+ });
2015
+ }
2016
+ function extractContentFromChoices(response) {
1926
2017
  const allTextBlocks = [];
1927
2018
  const allToolUseBlocks = [];
1928
- let stopReason = null;
1929
- stopReason = response.choices[0]?.finish_reason ?? stopReason;
2019
+ let stopReason = response.choices[0]?.finish_reason ?? null;
1930
2020
  for (const choice of response.choices) {
1931
- const textBlocks = getAnthropicTextBlocks(choice.message.content);
1932
- const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls);
1933
- allTextBlocks.push(...textBlocks);
1934
- allToolUseBlocks.push(...toolUseBlocks);
2021
+ allTextBlocks.push(...getAnthropicTextBlocks(choice.message.content));
2022
+ allToolUseBlocks.push(...getAnthropicToolUseBlocks(choice.message.tool_calls));
1935
2023
  if (choice.finish_reason === "tool_calls" || stopReason === "stop") stopReason = choice.finish_reason;
1936
2024
  }
2025
+ return {
2026
+ contentBlocks: [...allTextBlocks, ...allToolUseBlocks],
2027
+ stopReason
2028
+ };
2029
+ }
2030
+ function buildAnthropicResponse(response, options) {
2031
+ const { contentBlocks, stopReason, originalModel } = options;
1937
2032
  return {
1938
2033
  id: response.id,
1939
2034
  type: "message",
1940
2035
  role: "assistant",
1941
2036
  model: originalModel ?? response.model,
1942
- content: [...allTextBlocks, ...allToolUseBlocks],
2037
+ content: contentBlocks,
1943
2038
  stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
1944
2039
  stop_sequence: null,
1945
2040
  usage: {
@@ -2013,10 +2108,12 @@ function isToolBlockOpen(state$1) {
2013
2108
  return Object.values(state$1.toolCalls).some((tc) => tc.anthropicBlockIndex === state$1.contentBlockIndex);
2014
2109
  }
2015
2110
  function createMessageDeltaEvents(finishReason, usage) {
2111
+ const stopReason = mapOpenAIStopReasonToAnthropic(finishReason);
2112
+ console.log(`[stream-translation] Creating message_delta with stop_reason: ${stopReason}, finishReason: ${finishReason}`);
2016
2113
  return [{
2017
2114
  type: "message_delta",
2018
2115
  delta: {
2019
- stop_reason: mapOpenAIStopReasonToAnthropic(finishReason),
2116
+ stop_reason: stopReason,
2020
2117
  stop_sequence: null
2021
2118
  },
2022
2119
  usage: {
@@ -2043,8 +2140,8 @@ function translateChunkToAnthropicEvents(chunk, state$1, originalModel) {
2043
2140
  const events$1 = [];
2044
2141
  if (chunk.usage) {
2045
2142
  state$1.pendingUsage = {
2046
- prompt_tokens: chunk.usage.prompt_tokens ?? 0,
2047
- completion_tokens: chunk.usage.completion_tokens ?? 0,
2143
+ prompt_tokens: chunk.usage.prompt_tokens,
2144
+ completion_tokens: chunk.usage.completion_tokens,
2048
2145
  cached_tokens: chunk.usage.prompt_tokens_details?.cached_tokens ?? 0
2049
2146
  };
2050
2147
  if (state$1.pendingFinishReason && !state$1.messageDeltaSent) {
@@ -2224,9 +2321,11 @@ async function handleCompletion(c) {
2224
2321
  stream: true,
2225
2322
  stream_options: { include_usage: true }
2226
2323
  };
2227
- const eventStream = isAzureModel ? await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, streamPayload) : await createChatCompletions(streamPayload);
2324
+ const azureConfig = state.azureOpenAIConfig;
2325
+ const eventStream = isAzureModel && azureConfig ? await createAzureOpenAIChatCompletions(azureConfig, streamPayload) : await createChatCompletions(streamPayload);
2228
2326
  return streamSSE(c, async (stream) => {
2229
2327
  const { chunks, usage } = await collectChunksWithUsage(eventStream);
2328
+ consola.debug(`[stream] Collected ${chunks.length} chunks, usage:`, usage);
2230
2329
  if (usage) setRequestContext(c, {
2231
2330
  inputTokens: usage.prompt_tokens,
2232
2331
  outputTokens: usage.completion_tokens
@@ -2238,21 +2337,33 @@ async function handleCompletion(c) {
2238
2337
  toolCalls: {},
2239
2338
  pendingUsage: usage ?? void 0
2240
2339
  };
2241
- for (const chunk of chunks) for (const evt of translateChunkToAnthropicEvents(chunk, streamState, anthropicPayload.model)) await stream.writeSSE({
2242
- event: evt.type,
2243
- data: JSON.stringify(evt)
2244
- });
2245
- for (const evt of createFallbackMessageDeltaEvents(streamState)) await stream.writeSSE({
2246
- event: evt.type,
2247
- data: JSON.stringify(evt)
2248
- });
2340
+ for (const chunk of chunks) {
2341
+ const events$1 = translateChunkToAnthropicEvents(chunk, streamState, anthropicPayload.model);
2342
+ for (const evt of events$1) {
2343
+ consola.debug(`[stream] Emitting event: ${evt.type}`);
2344
+ await stream.writeSSE({
2345
+ event: evt.type,
2346
+ data: JSON.stringify(evt)
2347
+ });
2348
+ }
2349
+ }
2350
+ const fallbackEvents = createFallbackMessageDeltaEvents(streamState);
2351
+ consola.debug(`[stream] Fallback events: ${fallbackEvents.length}, messageDeltaSent: ${streamState.messageDeltaSent}`);
2352
+ for (const evt of fallbackEvents) {
2353
+ consola.debug(`[stream] Emitting fallback event: ${evt.type}`);
2354
+ await stream.writeSSE({
2355
+ event: evt.type,
2356
+ data: JSON.stringify(evt)
2357
+ });
2358
+ }
2249
2359
  });
2250
2360
  }
2251
2361
  const nonStreamPayload = {
2252
2362
  ...openAIPayload,
2253
2363
  stream: false
2254
2364
  };
2255
- const response = isAzureModel ? await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, nonStreamPayload) : await createChatCompletions(nonStreamPayload);
2365
+ const azureConfigNonStream = state.azureOpenAIConfig;
2366
+ const response = isAzureModel && azureConfigNonStream ? await createAzureOpenAIChatCompletions(azureConfigNonStream, nonStreamPayload) : await createChatCompletions(nonStreamPayload);
2256
2367
  if (response.usage) setRequestContext(c, {
2257
2368
  inputTokens: response.usage.prompt_tokens,
2258
2369
  outputTokens: response.usage.completion_tokens
@@ -2400,6 +2511,7 @@ server.route("/v1/messages", messageRoutes);
2400
2511
  //#endregion
2401
2512
  //#region src/start.ts
2402
2513
  async function runServer(options) {
2514
+ consola.info(`copilot-api v${package_default.version}`);
2403
2515
  if (options.insecure) {
2404
2516
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
2405
2517
  consola.warn("SSL certificate verification disabled (insecure mode)");
@@ -2462,7 +2574,8 @@ async function runServer(options) {
2462
2574
  consola.box(`🌐 Usage Viewer: https://ericc-ch.github.io/copilot-api?endpoint=${serverUrl}/usage`);
2463
2575
  serve({
2464
2576
  fetch: server.fetch,
2465
- port: options.port
2577
+ port: options.port,
2578
+ bun: { idleTimeout: 255 }
2466
2579
  });
2467
2580
  }
2468
2581
  const start = defineCommand({
@@ -2563,6 +2676,7 @@ const start = defineCommand({
2563
2676
  const main = defineCommand({
2564
2677
  meta: {
2565
2678
  name: "copilot-api",
2679
+ version: package_default.version,
2566
2680
  description: "A wrapper around GitHub Copilot API to make it OpenAI compatible, making it usable for other tools."
2567
2681
  },
2568
2682
  subCommands: {