@ashsec/copilot-api 0.7.7 → 0.7.10

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.10";
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");
@@ -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);
@@ -656,11 +735,11 @@ 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, isRegex = false, name$1) {
660
739
  await ensureLoaded();
661
740
  const rule = {
662
741
  id: `user-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
663
- name,
742
+ name: name$1,
664
743
  pattern,
665
744
  replacement,
666
745
  isRegex,
@@ -811,11 +890,11 @@ async function applyReplacementsToPayload(payload) {
811
890
  //#region src/config.ts
812
891
  function formatRule(rule, index) {
813
892
  const status = rule.enabled ? "✓" : "✗";
814
- const type = rule.isRegex ? "regex" : "string";
893
+ const type$1 = rule.isRegex ? "regex" : "string";
815
894
  const system = rule.isSystem ? " [system]" : "";
816
- const name = rule.name ? ` "${rule.name}"` : "";
895
+ const name$1 = rule.name ? ` "${rule.name}"` : "";
817
896
  const replacement = rule.replacement || "(empty)";
818
- return `${index + 1}. [${status}] (${type})${system}${name} "${rule.pattern}" → "${replacement}"`;
897
+ return `${index + 1}. [${status}] (${type$1})${system}${name$1} "${rule.pattern}" → "${replacement}"`;
819
898
  }
820
899
  async function listReplacements() {
821
900
  const all = await getAllReplacements();
@@ -828,11 +907,11 @@ async function listReplacements() {
828
907
  console.log();
829
908
  }
830
909
  async function addNewReplacement() {
831
- const name = await consola.prompt("Name (optional, short description):", {
910
+ const name$1 = await consola.prompt("Name (optional, short description):", {
832
911
  type: "text",
833
912
  default: ""
834
913
  });
835
- if (typeof name === "symbol") {
914
+ if (typeof name$1 === "symbol") {
836
915
  consola.info("Cancelled.");
837
916
  return;
838
917
  }
@@ -869,7 +948,7 @@ async function addNewReplacement() {
869
948
  consola.info("Cancelled.");
870
949
  return;
871
950
  }
872
- const rule = await addReplacement(pattern, replacement, matchType === "regex", name || void 0);
951
+ const rule = await addReplacement(pattern, replacement, matchType === "regex", name$1 || void 0);
873
952
  consola.success(`Added rule: ${rule.name || rule.id}`);
874
953
  }
875
954
  async function editExistingReplacement() {
@@ -897,11 +976,11 @@ async function editExistingReplacement() {
897
976
  }
898
977
  consola.info(`\nEditing rule: ${rule.name || rule.id}`);
899
978
  consola.info("Press Enter to keep current value.\n");
900
- const name = await consola.prompt("Name:", {
979
+ const name$1 = await consola.prompt("Name:", {
901
980
  type: "text",
902
981
  default: rule.name || ""
903
982
  });
904
- if (typeof name === "symbol") {
983
+ if (typeof name$1 === "symbol") {
905
984
  consola.info("Cancelled.");
906
985
  return;
907
986
  }
@@ -943,7 +1022,7 @@ async function editExistingReplacement() {
943
1022
  return;
944
1023
  }
945
1024
  const updated = await updateReplacement(selected, {
946
- name: name || void 0,
1025
+ name: name$1 || void 0,
947
1026
  pattern,
948
1027
  replacement,
949
1028
  isRegex: matchType === "regex"
@@ -1128,9 +1207,9 @@ async function checkTokenExists() {
1128
1207
  }
1129
1208
  }
1130
1209
  async function getDebugInfo() {
1131
- const [version, tokenExists] = await Promise.all([getPackageVersion(), checkTokenExists()]);
1210
+ const [version$1, tokenExists] = await Promise.all([getPackageVersion(), checkTokenExists()]);
1132
1211
  return {
1133
- version,
1212
+ version: version$1,
1134
1213
  runtime: getRuntimeInfo(),
1135
1214
  paths: {
1136
1215
  APP_DIR: PATHS.APP_DIR,
@@ -1922,7 +2001,7 @@ function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice) {
1922
2001
  default: return;
1923
2002
  }
1924
2003
  }
1925
- function translateToAnthropic(response) {
2004
+ function translateToAnthropic(response, originalModel) {
1926
2005
  const allTextBlocks = [];
1927
2006
  const allToolUseBlocks = [];
1928
2007
  let stopReason = null;
@@ -1938,7 +2017,7 @@ function translateToAnthropic(response) {
1938
2017
  id: response.id,
1939
2018
  type: "message",
1940
2019
  role: "assistant",
1941
- model: response.model,
2020
+ model: originalModel ?? response.model,
1942
2021
  content: [...allTextBlocks, ...allToolUseBlocks],
1943
2022
  stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
1944
2023
  stop_sequence: null,
@@ -2012,12 +2091,57 @@ function isToolBlockOpen(state$1) {
2012
2091
  if (!state$1.contentBlockOpen) return false;
2013
2092
  return Object.values(state$1.toolCalls).some((tc) => tc.anthropicBlockIndex === state$1.contentBlockIndex);
2014
2093
  }
2015
- function translateChunkToAnthropicEvents(chunk, state$1) {
2094
+ function createMessageDeltaEvents(finishReason, usage) {
2095
+ const stopReason = mapOpenAIStopReasonToAnthropic(finishReason);
2096
+ console.log(`[stream-translation] Creating message_delta with stop_reason: ${stopReason}, finishReason: ${finishReason}`);
2097
+ return [{
2098
+ type: "message_delta",
2099
+ delta: {
2100
+ stop_reason: stopReason,
2101
+ stop_sequence: null
2102
+ },
2103
+ usage: {
2104
+ input_tokens: usage.prompt_tokens,
2105
+ output_tokens: usage.completion_tokens,
2106
+ cache_creation_input_tokens: 0,
2107
+ cache_read_input_tokens: usage.cached_tokens
2108
+ }
2109
+ }, { type: "message_stop" }];
2110
+ }
2111
+ function createFallbackMessageDeltaEvents(state$1) {
2112
+ if (state$1.messageDeltaSent) return [];
2113
+ if (state$1.pendingFinishReason) {
2114
+ const usage = state$1.pendingUsage ?? {
2115
+ prompt_tokens: 0,
2116
+ completion_tokens: 0,
2117
+ cached_tokens: 0
2118
+ };
2119
+ return createMessageDeltaEvents(state$1.pendingFinishReason, usage);
2120
+ }
2121
+ return [];
2122
+ }
2123
+ function translateChunkToAnthropicEvents(chunk, state$1, originalModel) {
2016
2124
  const events$1 = [];
2125
+ if (chunk.usage) {
2126
+ state$1.pendingUsage = {
2127
+ prompt_tokens: chunk.usage.prompt_tokens ?? 0,
2128
+ completion_tokens: chunk.usage.completion_tokens ?? 0,
2129
+ cached_tokens: chunk.usage.prompt_tokens_details?.cached_tokens ?? 0
2130
+ };
2131
+ if (state$1.pendingFinishReason && !state$1.messageDeltaSent) {
2132
+ events$1.push(...createMessageDeltaEvents(state$1.pendingFinishReason, state$1.pendingUsage));
2133
+ state$1.messageDeltaSent = true;
2134
+ }
2135
+ }
2017
2136
  if (chunk.choices.length === 0) return events$1;
2018
2137
  const choice = chunk.choices[0];
2019
2138
  const { delta } = choice;
2020
2139
  if (!state$1.messageStartSent) {
2140
+ const usage = state$1.pendingUsage ?? {
2141
+ prompt_tokens: 0,
2142
+ completion_tokens: 0,
2143
+ cached_tokens: 0
2144
+ };
2021
2145
  events$1.push({
2022
2146
  type: "message_start",
2023
2147
  message: {
@@ -2025,9 +2149,15 @@ function translateChunkToAnthropicEvents(chunk, state$1) {
2025
2149
  type: "message",
2026
2150
  role: "assistant",
2027
2151
  content: [],
2028
- model: chunk.model,
2152
+ model: originalModel ?? chunk.model,
2029
2153
  stop_reason: null,
2030
- stop_sequence: null
2154
+ stop_sequence: null,
2155
+ usage: {
2156
+ input_tokens: usage.prompt_tokens,
2157
+ output_tokens: usage.completion_tokens,
2158
+ cache_creation_input_tokens: 0,
2159
+ cache_read_input_tokens: usage.cached_tokens
2160
+ }
2031
2161
  }
2032
2162
  });
2033
2163
  state$1.messageStartSent = true;
@@ -2109,28 +2239,44 @@ function translateChunkToAnthropicEvents(chunk, state$1) {
2109
2239
  });
2110
2240
  state$1.contentBlockOpen = false;
2111
2241
  }
2112
- const inputTokens = chunk.usage?.prompt_tokens ?? 0;
2113
- const outputTokens = chunk.usage?.completion_tokens ?? 0;
2114
- const cachedTokens = chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0;
2115
- events$1.push({
2116
- type: "message_delta",
2117
- delta: {
2118
- stop_reason: mapOpenAIStopReasonToAnthropic(choice.finish_reason),
2119
- stop_sequence: null
2120
- },
2121
- usage: {
2122
- input_tokens: inputTokens,
2123
- output_tokens: outputTokens,
2124
- cache_creation_input_tokens: 0,
2125
- cache_read_input_tokens: cachedTokens
2126
- }
2127
- }, { type: "message_stop" });
2242
+ if (chunk.usage || state$1.pendingUsage) {
2243
+ const usage = {
2244
+ prompt_tokens: chunk.usage?.prompt_tokens ?? state$1.pendingUsage?.prompt_tokens ?? 0,
2245
+ completion_tokens: chunk.usage?.completion_tokens ?? state$1.pendingUsage?.completion_tokens ?? 0,
2246
+ cached_tokens: chunk.usage?.prompt_tokens_details?.cached_tokens ?? state$1.pendingUsage?.cached_tokens ?? 0
2247
+ };
2248
+ events$1.push(...createMessageDeltaEvents(choice.finish_reason, usage));
2249
+ state$1.messageDeltaSent = true;
2250
+ } else state$1.pendingFinishReason = choice.finish_reason;
2128
2251
  }
2129
2252
  return events$1;
2130
2253
  }
2131
2254
 
2132
2255
  //#endregion
2133
2256
  //#region src/routes/messages/handler.ts
2257
+ /** Collect all chunks and extract usage data */
2258
+ async function collectChunksWithUsage(eventStream) {
2259
+ const chunks = [];
2260
+ let usage = null;
2261
+ for await (const event of eventStream) {
2262
+ if (!event.data || event.data === "[DONE]") continue;
2263
+ try {
2264
+ const chunk = JSON.parse(event.data);
2265
+ chunks.push(chunk);
2266
+ if (chunk.usage) usage = {
2267
+ prompt_tokens: chunk.usage.prompt_tokens,
2268
+ completion_tokens: chunk.usage.completion_tokens,
2269
+ cached_tokens: chunk.usage.prompt_tokens_details?.cached_tokens ?? 0
2270
+ };
2271
+ } catch (error) {
2272
+ consola.error("Failed to parse chunk:", error, event.data);
2273
+ }
2274
+ }
2275
+ return {
2276
+ chunks,
2277
+ usage
2278
+ };
2279
+ }
2134
2280
  async function handleCompletion(c) {
2135
2281
  await checkRateLimit(state);
2136
2282
  const anthropicPayload = await c.req.json();
@@ -2141,7 +2287,6 @@ async function handleCompletion(c) {
2141
2287
  ...openAIPayload,
2142
2288
  model: normalizeModelName(openAIPayload.model)
2143
2289
  };
2144
- consola.debug("Translated OpenAI request payload:", JSON.stringify(openAIPayload));
2145
2290
  if (state.manualApprove) await awaitApproval();
2146
2291
  const isAzureModel = isAzureOpenAIModel(openAIPayload.model);
2147
2292
  if (isAzureModel) {
@@ -2162,33 +2307,38 @@ async function handleCompletion(c) {
2162
2307
  };
2163
2308
  const eventStream = isAzureModel ? await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, streamPayload) : await createChatCompletions(streamPayload);
2164
2309
  return streamSSE(c, async (stream) => {
2310
+ const { chunks, usage } = await collectChunksWithUsage(eventStream);
2311
+ consola.debug(`[stream] Collected ${chunks.length} chunks, usage:`, usage);
2312
+ if (usage) setRequestContext(c, {
2313
+ inputTokens: usage.prompt_tokens,
2314
+ outputTokens: usage.completion_tokens
2315
+ });
2165
2316
  const streamState = {
2166
2317
  messageStartSent: false,
2167
2318
  contentBlockOpen: false,
2168
2319
  contentBlockIndex: 0,
2169
- toolCalls: {}
2320
+ toolCalls: {},
2321
+ pendingUsage: usage ?? void 0
2170
2322
  };
2171
- for await (const event of eventStream) {
2172
- if (!event.data || event.data === "[DONE]") continue;
2173
- try {
2174
- const chunk = JSON.parse(event.data);
2175
- consola.debug("OpenAI chunk:", JSON.stringify(chunk));
2176
- const anthropicEvents = translateChunkToAnthropicEvents(chunk, streamState);
2177
- for (const anthropicEvent of anthropicEvents) {
2178
- consola.debug("Anthropic event:", JSON.stringify(anthropicEvent));
2179
- await stream.writeSSE({
2180
- event: anthropicEvent.type,
2181
- data: JSON.stringify(anthropicEvent)
2182
- });
2183
- }
2184
- if (chunk.usage) setRequestContext(c, {
2185
- inputTokens: chunk.usage.prompt_tokens,
2186
- outputTokens: chunk.usage.completion_tokens
2323
+ for (const chunk of chunks) {
2324
+ const events$1 = translateChunkToAnthropicEvents(chunk, streamState, anthropicPayload.model);
2325
+ for (const evt of events$1) {
2326
+ consola.debug(`[stream] Emitting event: ${evt.type}`);
2327
+ await stream.writeSSE({
2328
+ event: evt.type,
2329
+ data: JSON.stringify(evt)
2187
2330
  });
2188
- } catch (error) {
2189
- consola.error("Failed to parse chunk:", error, event.data);
2190
2331
  }
2191
2332
  }
2333
+ const fallbackEvents = createFallbackMessageDeltaEvents(streamState);
2334
+ consola.debug(`[stream] Fallback events: ${fallbackEvents.length}, messageDeltaSent: ${streamState.messageDeltaSent}`);
2335
+ for (const evt of fallbackEvents) {
2336
+ consola.debug(`[stream] Emitting fallback event: ${evt.type}`);
2337
+ await stream.writeSSE({
2338
+ event: evt.type,
2339
+ data: JSON.stringify(evt)
2340
+ });
2341
+ }
2192
2342
  });
2193
2343
  }
2194
2344
  const nonStreamPayload = {
@@ -2196,13 +2346,11 @@ async function handleCompletion(c) {
2196
2346
  stream: false
2197
2347
  };
2198
2348
  const response = isAzureModel ? await createAzureOpenAIChatCompletions(state.azureOpenAIConfig, nonStreamPayload) : await createChatCompletions(nonStreamPayload);
2199
- consola.debug("Response from upstream:", JSON.stringify(response).slice(-400));
2200
2349
  if (response.usage) setRequestContext(c, {
2201
2350
  inputTokens: response.usage.prompt_tokens,
2202
2351
  outputTokens: response.usage.completion_tokens
2203
2352
  });
2204
- const anthropicResponse = translateToAnthropic(response);
2205
- consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
2353
+ const anthropicResponse = translateToAnthropic(response, anthropicPayload.model);
2206
2354
  return c.json(anthropicResponse);
2207
2355
  }
2208
2356
 
@@ -2345,6 +2493,7 @@ server.route("/v1/messages", messageRoutes);
2345
2493
  //#endregion
2346
2494
  //#region src/start.ts
2347
2495
  async function runServer(options) {
2496
+ consola.info(`copilot-api v${package_default.version}`);
2348
2497
  if (options.insecure) {
2349
2498
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
2350
2499
  consola.warn("SSL certificate verification disabled (insecure mode)");
@@ -2407,7 +2556,8 @@ async function runServer(options) {
2407
2556
  consola.box(`🌐 Usage Viewer: https://ericc-ch.github.io/copilot-api?endpoint=${serverUrl}/usage`);
2408
2557
  serve({
2409
2558
  fetch: server.fetch,
2410
- port: options.port
2559
+ port: options.port,
2560
+ bun: { idleTimeout: 255 }
2411
2561
  });
2412
2562
  }
2413
2563
  const start = defineCommand({
@@ -2508,6 +2658,7 @@ const start = defineCommand({
2508
2658
  const main = defineCommand({
2509
2659
  meta: {
2510
2660
  name: "copilot-api",
2661
+ version: package_default.version,
2511
2662
  description: "A wrapper around GitHub Copilot API to make it OpenAI compatible, making it usable for other tools."
2512
2663
  },
2513
2664
  subCommands: {