@dianshuv/copilot-api 0.4.3 → 0.6.0

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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/main.mjs +291 -96
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -15,6 +15,7 @@
15
15
  - **Graceful shutdown**: 4-phase shutdown sequence — stops accepting requests, waits for in-flight requests to complete, sends abort signal, then force-closes. Configurable via `--shutdown-graceful-wait` and `--shutdown-abort-wait`.
16
16
  - **Stream repetition detection**: Detects when models get stuck in repetitive output loops using KMP-based pattern matching and logs a warning.
17
17
  - **Stale request reaping**: Automatically force-fails requests that exceed a configurable maximum age (default 600s) to prevent resource leaks.
18
+ - **PostHog analytics**: Optional PostHog Cloud integration (`--posthog-key`) sends per-request token usage events for long-term trend analysis. Free tier (1M events/month) is more than sufficient for individual use.
18
19
 
19
20
  ## Quick Start
20
21
 
@@ -66,6 +67,7 @@ copilot-api start
66
67
  | `--redirect-anthropic` | Force Anthropic through OpenAI translation | false |
67
68
  | `--no-rewrite-anthropic-tools` | Don't rewrite server-side tools | false |
68
69
  | `--timezone-offset` | Timezone offset in hours from UTC for log timestamps (e.g., +8, -5, 0) | +8 |
70
+ | `--posthog-key` | PostHog API key for token usage analytics (opt-in) | none |
69
71
 
70
72
  ### Patch-Claude Command Options
71
73
 
package/dist/main.mjs CHANGED
@@ -4,11 +4,12 @@ import consola from "consola";
4
4
  import fs from "node:fs/promises";
5
5
  import os from "node:os";
6
6
  import path, { dirname, join } from "node:path";
7
- import { randomUUID } from "node:crypto";
7
+ import { createHash, randomUUID } from "node:crypto";
8
8
  import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
9
9
  import clipboard from "clipboardy";
10
10
  import { serve } from "srvx";
11
11
  import invariant from "tiny-invariant";
12
+ import { PostHog } from "posthog-node";
12
13
  import { getProxyForUrl } from "proxy-from-env";
13
14
  import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
14
15
  import { execSync } from "node:child_process";
@@ -68,7 +69,14 @@ const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`;
68
69
  const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`;
69
70
  const API_VERSION = "2025-04-01";
70
71
  const copilotBaseUrl = (state) => state.accountType === "individual" ? "https://api.githubcopilot.com" : `https://api.${state.accountType}.githubcopilot.com`;
71
- const copilotHeaders = (state, vision = false) => {
72
+ function hasHeaderKey(headers, key) {
73
+ const lowerKey = key.toLowerCase();
74
+ return Object.keys(headers).some((existingKey) => {
75
+ return existingKey.toLowerCase() === lowerKey;
76
+ });
77
+ }
78
+ function copilotHeaders(state, visionOrOptions) {
79
+ const options = typeof visionOrOptions === "boolean" ? { vision: visionOrOptions } : visionOrOptions ?? {};
72
80
  const headers = {
73
81
  Authorization: `Bearer ${state.copilotToken}`,
74
82
  "content-type": standardHeaders()["content-type"],
@@ -76,14 +84,15 @@ const copilotHeaders = (state, vision = false) => {
76
84
  "editor-version": `vscode/${state.vsCodeVersion}`,
77
85
  "editor-plugin-version": EDITOR_PLUGIN_VERSION,
78
86
  "user-agent": USER_AGENT,
79
- "openai-intent": "conversation-panel",
87
+ "openai-intent": options.intent ?? "conversation-panel",
80
88
  "x-github-api-version": API_VERSION,
81
89
  "x-request-id": randomUUID(),
82
90
  "x-vscode-user-agent-library-version": "electron-fetch"
83
91
  };
84
- if (vision) headers["copilot-vision-request"] = "true";
92
+ for (const [key, value] of Object.entries(options.modelRequestHeaders ?? {})) if (!hasHeaderKey(headers, key)) headers[key] = value;
93
+ if (options.vision) headers["copilot-vision-request"] = "true";
85
94
  return headers;
86
- };
95
+ }
87
96
  const GITHUB_API_BASE_URL = "https://api.github.com";
88
97
  const githubHeaders = (state) => ({
89
98
  ...standardHeaders(),
@@ -208,6 +217,10 @@ function formatRateLimitError(copilotMessage) {
208
217
  }
209
218
  };
210
219
  }
220
+ function truncateForLog(text, maxLen) {
221
+ if (text.length <= maxLen) return text;
222
+ return `${text.slice(0, maxLen)}...`;
223
+ }
211
224
  function forwardError(c, error) {
212
225
  if (error instanceof HTTPError) {
213
226
  if (error.status === 413) {
@@ -246,7 +259,9 @@ function forwardError(c, error) {
246
259
  consola.warn(`HTTP 429: Rate limit exceeded`);
247
260
  return c.json(formattedError, 429);
248
261
  }
249
- consola.error(`HTTP ${error.status}:`, errorJson);
262
+ let loggedError = errorJson;
263
+ if (typeof errorJson === "string") loggedError = errorJson.trimStart().startsWith("<") ? `[HTML ${errorJson.length} bytes]` : truncateForLog(errorJson, 200);
264
+ consola.error(`HTTP ${error.status}:`, loggedError);
250
265
  return c.json({ error: {
251
266
  message: error.responseText,
252
267
  type: "error"
@@ -1021,7 +1036,7 @@ const patchClaude = defineCommand({
1021
1036
 
1022
1037
  //#endregion
1023
1038
  //#region package.json
1024
- var version = "0.4.3";
1039
+ var version = "0.6.0";
1025
1040
 
1026
1041
  //#endregion
1027
1042
  //#region src/lib/adaptive-rate-limiter.ts
@@ -1933,6 +1948,55 @@ function exportHistory(format = "json") {
1933
1948
  return [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
1934
1949
  }
1935
1950
 
1951
+ //#endregion
1952
+ //#region src/lib/posthog.ts
1953
+ let client = null;
1954
+ let distinctId = "";
1955
+ function initPostHog(apiKey) {
1956
+ if (!apiKey) return;
1957
+ try {
1958
+ client = new PostHog(apiKey, {
1959
+ host: "https://us.i.posthog.com",
1960
+ flushAt: 20,
1961
+ flushInterval: 1e4
1962
+ });
1963
+ distinctId = createHash("sha256").update(os.hostname() + os.userInfo().username).digest("hex");
1964
+ } catch (error) {
1965
+ consola.warn("Failed to initialize PostHog:", error instanceof Error ? error.message : error);
1966
+ client = null;
1967
+ }
1968
+ }
1969
+ function isPostHogEnabled() {
1970
+ return client !== null;
1971
+ }
1972
+ function captureRequest(params) {
1973
+ if (!client) return;
1974
+ const properties = {
1975
+ model: params.model,
1976
+ input_tokens: params.inputTokens,
1977
+ output_tokens: params.outputTokens,
1978
+ duration_ms: params.durationMs,
1979
+ success: params.success,
1980
+ stream: params.stream,
1981
+ tool_count: params.toolCount
1982
+ };
1983
+ if (params.reasoningTokens !== void 0) properties.reasoning_tokens = params.reasoningTokens;
1984
+ if (params.stopReason !== void 0) properties.stop_reason = params.stopReason;
1985
+ client.capture({
1986
+ distinctId,
1987
+ event: "copilot_api_request",
1988
+ properties
1989
+ });
1990
+ }
1991
+ async function shutdownPostHog() {
1992
+ if (!client) return;
1993
+ try {
1994
+ await client.shutdown();
1995
+ } catch (error) {
1996
+ consola.warn("Failed to flush PostHog events:", error instanceof Error ? error.message : error);
1997
+ }
1998
+ }
1999
+
1936
2000
  //#endregion
1937
2001
  //#region src/lib/proxy.ts
1938
2002
  /**
@@ -2143,7 +2207,7 @@ async function gracefulShutdown(signal, deps) {
2143
2207
  try {
2144
2208
  if (await drainActiveRequests(gracefulWaitMs, tracker, drainOpts) === "drained") {
2145
2209
  consola.info("All requests completed naturally");
2146
- finalize(tracker);
2210
+ await finalize(tracker);
2147
2211
  return;
2148
2212
  }
2149
2213
  } catch (error) {
@@ -2155,7 +2219,7 @@ async function gracefulShutdown(signal, deps) {
2155
2219
  try {
2156
2220
  if (await drainActiveRequests(abortWaitMs, tracker, drainOpts) === "drained") {
2157
2221
  consola.info("All requests completed after abort signal");
2158
- finalize(tracker);
2222
+ await finalize(tracker);
2159
2223
  return;
2160
2224
  }
2161
2225
  } catch (error) {
@@ -2169,13 +2233,15 @@ async function gracefulShutdown(signal, deps) {
2169
2233
  consola.error("Error force-closing server:", error);
2170
2234
  }
2171
2235
  }
2172
- finalize(tracker);
2236
+ await finalize(tracker);
2173
2237
  } else {
2238
+ await shutdownPostHog();
2174
2239
  consola.info("Shutdown complete");
2175
2240
  shutdownResolve?.();
2176
2241
  }
2177
2242
  }
2178
- function finalize(tracker) {
2243
+ async function finalize(tracker) {
2244
+ await shutdownPostHog();
2179
2245
  tracker.destroy();
2180
2246
  consola.info("Shutdown complete");
2181
2247
  shutdownResolve?.();
@@ -2465,8 +2531,10 @@ var RequestTracker = class {
2465
2531
  if (update.durationMs !== void 0) request.durationMs = update.durationMs;
2466
2532
  if (update.inputTokens !== void 0) request.inputTokens = update.inputTokens;
2467
2533
  if (update.outputTokens !== void 0) request.outputTokens = update.outputTokens;
2534
+ if (update.reasoningTokens !== void 0) request.reasoningTokens = update.reasoningTokens;
2468
2535
  if (update.error !== void 0) request.error = update.error;
2469
2536
  if (update.queuePosition !== void 0) request.queuePosition = update.queuePosition;
2537
+ if (update.queueWaitMs !== void 0) request.queueWaitMs = update.queueWaitMs;
2470
2538
  this.renderer?.onRequestUpdate(id, update);
2471
2539
  }
2472
2540
  /**
@@ -2481,6 +2549,7 @@ var RequestTracker = class {
2481
2549
  if (usage) {
2482
2550
  request.inputTokens = usage.inputTokens;
2483
2551
  request.outputTokens = usage.outputTokens;
2552
+ if (usage.reasoningTokens !== void 0) request.reasoningTokens = usage.reasoningTokens;
2484
2553
  }
2485
2554
  this.renderer?.onRequestComplete(request);
2486
2555
  this.requests.delete(id);
@@ -3281,6 +3350,26 @@ function createTruncationResponseMarkerOpenAI(result) {
3281
3350
  return `\n\n---\n[Auto-truncated: ${result.removedMessageCount} messages removed, ${result.originalTokens} → ${result.compactedTokens} tokens (${percentage}% reduction)]`;
3282
3351
  }
3283
3352
 
3353
+ //#endregion
3354
+ //#region src/lib/message-sanitizer.ts
3355
+ const startPattern = /^\s*<system-reminder>[\s\S]*?<\/system-reminder>\n*/;
3356
+ const endPatternWithNewline = /\n+<system-reminder>[\s\S]*?<\/system-reminder>\s*$/;
3357
+ const endPatternOnly = /^\s*<system-reminder>[\s\S]*?<\/system-reminder>\s*$/;
3358
+ function removeSystemReminderTags(text) {
3359
+ let result = text;
3360
+ let prev;
3361
+ do {
3362
+ prev = result;
3363
+ result = result.replace(startPattern, "");
3364
+ } while (result !== prev);
3365
+ do {
3366
+ prev = result;
3367
+ result = result.replace(endPatternWithNewline, "");
3368
+ } while (result !== prev);
3369
+ result = result.replace(endPatternOnly, "");
3370
+ return result;
3371
+ }
3372
+
3284
3373
  //#endregion
3285
3374
  //#region src/lib/repetition-detector.ts
3286
3375
  /**
@@ -3409,7 +3498,10 @@ const createChatCompletions = async (payload, options) => {
3409
3498
  const enableVision = payload.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
3410
3499
  const isAgentCall = payload.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
3411
3500
  const headers = {
3412
- ...copilotHeaders(state, enableVision),
3501
+ ...copilotHeaders(state, {
3502
+ vision: enableVision,
3503
+ intent: isAgentCall ? "conversation-agent" : "conversation-panel"
3504
+ }),
3413
3505
  "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
3414
3506
  };
3415
3507
  const response = await fetch(`${copilotBaseUrl(state)}/chat/completions`, {
@@ -3463,17 +3555,30 @@ function recordErrorResponse(ctx, model, error) {
3463
3555
  content: null
3464
3556
  }, Date.now() - ctx.startTime);
3465
3557
  }
3466
- /** Complete TUI tracking */
3467
- function completeTracking(trackingId, inputTokens, outputTokens, queueWaitMs) {
3558
+ /** Complete TUI tracking and send PostHog analytics */
3559
+ function completeTracking(trackingId, inputTokens, outputTokens, queueWaitMs, reasoningTokens, analytics) {
3468
3560
  if (!trackingId) return;
3469
3561
  requestTracker.updateRequest(trackingId, {
3470
3562
  inputTokens,
3471
3563
  outputTokens,
3472
- queueWaitMs
3564
+ queueWaitMs,
3565
+ reasoningTokens
3473
3566
  });
3474
3567
  requestTracker.completeRequest(trackingId, 200, {
3475
3568
  inputTokens,
3476
- outputTokens
3569
+ outputTokens,
3570
+ reasoningTokens
3571
+ });
3572
+ if (analytics) captureRequest({
3573
+ model: analytics.model,
3574
+ inputTokens,
3575
+ outputTokens,
3576
+ durationMs: analytics.durationMs,
3577
+ success: true,
3578
+ stream: analytics.stream,
3579
+ toolCount: analytics.toolCount ?? 0,
3580
+ reasoningTokens,
3581
+ stopReason: analytics.stopReason
3477
3582
  });
3478
3583
  }
3479
3584
  /** Fail TUI tracking */
@@ -3593,6 +3698,9 @@ async function logPayloadSizeInfo(payload, model) {
3593
3698
 
3594
3699
  //#endregion
3595
3700
  //#region src/routes/chat-completions/handler.ts
3701
+ function getReasoningTokensFromOpenAIUsage(usage) {
3702
+ return usage?.completion_tokens_details?.reasoning_tokens;
3703
+ }
3596
3704
  async function handleCompletion$1(c) {
3597
3705
  const originalPayload = await c.req.json();
3598
3706
  consola.debug("Request payload:", JSON.stringify(originalPayload).slice(-400));
@@ -3640,7 +3748,7 @@ async function executeRequest(opts) {
3640
3748
  try {
3641
3749
  const { result: response, queueWaitMs } = await executeWithAdaptiveRateLimit(() => createChatCompletions(payload));
3642
3750
  ctx.queueWaitMs = queueWaitMs;
3643
- if (isNonStreaming(response)) return handleNonStreamingResponse$1(c, response, ctx);
3751
+ if (isNonStreaming(response)) return handleNonStreamingResponse$1(c, response, ctx, payload);
3644
3752
  consola.debug("Streaming response");
3645
3753
  updateTrackerStatus(trackingId, "streaming");
3646
3754
  return streamSSE(c, async (stream) => {
@@ -3667,7 +3775,7 @@ async function logTokenCount(payload, selectedModel) {
3667
3775
  consola.debug("Failed to calculate token count:", error);
3668
3776
  }
3669
3777
  }
3670
- function handleNonStreamingResponse$1(c, originalResponse, ctx) {
3778
+ function handleNonStreamingResponse$1(c, originalResponse, ctx, payload) {
3671
3779
  consola.debug("Non-streaming response:", JSON.stringify(originalResponse));
3672
3780
  let response = originalResponse;
3673
3781
  if (state.verbose && ctx.truncateResult?.wasCompacted && response.choices[0]?.message.content) {
@@ -3685,21 +3793,36 @@ function handleNonStreamingResponse$1(c, originalResponse, ctx) {
3685
3793
  }
3686
3794
  const choice = response.choices[0];
3687
3795
  const usage = response.usage;
3796
+ const reasoningTokens = getReasoningTokensFromOpenAIUsage(usage);
3797
+ const durationMs = Date.now() - ctx.startTime;
3688
3798
  recordResponse(ctx.historyId, {
3689
3799
  success: true,
3690
3800
  model: response.model,
3691
3801
  usage: {
3692
3802
  input_tokens: usage?.prompt_tokens ?? 0,
3693
- output_tokens: usage?.completion_tokens ?? 0
3803
+ output_tokens: usage?.completion_tokens ?? 0,
3804
+ ...reasoningTokens !== void 0 ? { output_tokens_details: { reasoning_tokens: reasoningTokens } } : {}
3694
3805
  },
3695
3806
  stop_reason: choice.finish_reason,
3696
3807
  content: buildResponseContent(choice),
3697
3808
  toolCalls: extractToolCalls(choice)
3698
- }, Date.now() - ctx.startTime);
3809
+ }, durationMs);
3699
3810
  if (ctx.trackingId && usage) requestTracker.updateRequest(ctx.trackingId, {
3700
3811
  inputTokens: usage.prompt_tokens,
3701
3812
  outputTokens: usage.completion_tokens,
3702
- queueWaitMs: ctx.queueWaitMs
3813
+ queueWaitMs: ctx.queueWaitMs,
3814
+ reasoningTokens
3815
+ });
3816
+ captureRequest({
3817
+ model: response.model,
3818
+ inputTokens: usage?.prompt_tokens ?? 0,
3819
+ outputTokens: usage?.completion_tokens ?? 0,
3820
+ durationMs,
3821
+ success: true,
3822
+ stream: false,
3823
+ toolCount: payload.tools?.length ?? 0,
3824
+ reasoningTokens,
3825
+ stopReason: choice.finish_reason
3703
3826
  });
3704
3827
  return c.json(response);
3705
3828
  }
@@ -3729,6 +3852,7 @@ function createStreamAccumulator() {
3729
3852
  model: "",
3730
3853
  inputTokens: 0,
3731
3854
  outputTokens: 0,
3855
+ reasoningTokens: 0,
3732
3856
  finishReason: "",
3733
3857
  content: "",
3734
3858
  toolCalls: [],
@@ -3766,7 +3890,13 @@ async function handleStreamingResponse$1(opts) {
3766
3890
  await stream.writeSSE(chunk);
3767
3891
  }
3768
3892
  recordStreamSuccess(acc, payload.model, ctx);
3769
- completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs);
3893
+ completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs, acc.reasoningTokens, {
3894
+ model: acc.model || payload.model,
3895
+ stream: true,
3896
+ durationMs: Date.now() - ctx.startTime,
3897
+ stopReason: acc.finishReason || void 0,
3898
+ toolCount: payload.tools?.length ?? 0
3899
+ });
3770
3900
  } catch (error) {
3771
3901
  recordStreamError({
3772
3902
  acc,
@@ -3786,6 +3916,7 @@ function parseStreamChunk(chunk, acc, checkRepetition) {
3786
3916
  if (parsed.usage) {
3787
3917
  acc.inputTokens = parsed.usage.prompt_tokens;
3788
3918
  acc.outputTokens = parsed.usage.completion_tokens;
3919
+ acc.reasoningTokens = getReasoningTokensFromOpenAIUsage(parsed.usage) ?? 0;
3789
3920
  }
3790
3921
  const choice = parsed.choices[0];
3791
3922
  if (choice) {
@@ -3826,7 +3957,8 @@ function recordStreamSuccess(acc, fallbackModel, ctx) {
3826
3957
  model: acc.model || fallbackModel,
3827
3958
  usage: {
3828
3959
  input_tokens: acc.inputTokens,
3829
- output_tokens: acc.outputTokens
3960
+ output_tokens: acc.outputTokens,
3961
+ ...acc.reasoningTokens > 0 ? { output_tokens_details: { reasoning_tokens: acc.reasoningTokens } } : {}
3830
3962
  },
3831
3963
  stop_reason: acc.finishReason || void 0,
3832
3964
  content: {
@@ -3845,7 +3977,7 @@ function convertOpenAIMessages(messages) {
3845
3977
  return messages.map((msg) => {
3846
3978
  const result = {
3847
3979
  role: msg.role,
3848
- content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
3980
+ content: typeof msg.content === "string" ? removeSystemReminderTags(msg.content) : JSON.stringify(msg.content)
3849
3981
  };
3850
3982
  if ("tool_calls" in msg && msg.tool_calls) result.tool_calls = msg.tool_calls.map((tc) => ({
3851
3983
  id: tc.id,
@@ -6006,7 +6138,10 @@ async function createAnthropicMessages(payload, options) {
6006
6138
  });
6007
6139
  const isAgentCall = filteredPayload.messages.some((msg) => msg.role === "assistant");
6008
6140
  const headers = {
6009
- ...copilotHeaders(state, enableVision),
6141
+ ...copilotHeaders(state, {
6142
+ vision: enableVision,
6143
+ intent: isAgentCall ? "conversation-agent" : "conversation-panel"
6144
+ }),
6010
6145
  "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user"),
6011
6146
  "anthropic-version": "2023-06-01"
6012
6147
  };
@@ -6141,12 +6276,12 @@ function convertAnthropicMessages(messages) {
6141
6276
  return messages.map((msg) => {
6142
6277
  if (typeof msg.content === "string") return {
6143
6278
  role: msg.role,
6144
- content: msg.content
6279
+ content: removeSystemReminderTags(msg.content)
6145
6280
  };
6146
6281
  const content = msg.content.map((block) => {
6147
6282
  if (block.type === "text") return {
6148
6283
  type: "text",
6149
- text: block.text
6284
+ text: removeSystemReminderTags(block.text)
6150
6285
  };
6151
6286
  if (block.type === "tool_use") return {
6152
6287
  type: "tool_use",
@@ -6213,9 +6348,13 @@ function createAnthropicStreamAccumulator() {
6213
6348
  stopReason: "",
6214
6349
  content: "",
6215
6350
  toolCalls: [],
6351
+ serverToolResults: [],
6216
6352
  currentToolCall: null
6217
6353
  };
6218
6354
  }
6355
+ function isServerToolResultType(type) {
6356
+ return type !== "tool_result" && type.endsWith("_tool_result");
6357
+ }
6219
6358
  function processAnthropicEvent(event, acc) {
6220
6359
  switch (event.type) {
6221
6360
  case "content_block_delta":
@@ -6238,11 +6377,14 @@ function handleContentBlockDelta(delta, acc) {
6238
6377
  else if (delta.type === "input_json_delta" && acc.currentToolCall) acc.currentToolCall.input += delta.partial_json;
6239
6378
  }
6240
6379
  function handleContentBlockStart(block, acc) {
6241
- if (block.type === "tool_use") acc.currentToolCall = {
6242
- id: block.id,
6243
- name: block.name,
6244
- input: ""
6245
- };
6380
+ if (block.type === "tool_use") {
6381
+ const toolBlock = block;
6382
+ acc.currentToolCall = {
6383
+ id: toolBlock.id,
6384
+ name: toolBlock.name,
6385
+ input: ""
6386
+ };
6387
+ } else if (isServerToolResultType(block.type)) acc.serverToolResults.push(block);
6246
6388
  }
6247
6389
  function handleContentBlockStop(acc) {
6248
6390
  if (acc.currentToolCall) {
@@ -6257,6 +6399,32 @@ function handleMessageDelta(delta, usage, acc) {
6257
6399
  acc.outputTokens = usage.output_tokens;
6258
6400
  }
6259
6401
  }
6402
+ function recordAnthropicStreamingResponse(acc, fallbackModel, ctx) {
6403
+ const contentBlocks = [];
6404
+ if (acc.content) contentBlocks.push({
6405
+ type: "text",
6406
+ text: acc.content
6407
+ });
6408
+ for (const tc of acc.toolCalls) contentBlocks.push({
6409
+ type: "tool_use",
6410
+ ...tc
6411
+ });
6412
+ for (const result of acc.serverToolResults) contentBlocks.push(result);
6413
+ recordResponse(ctx.historyId, {
6414
+ success: true,
6415
+ model: acc.model || fallbackModel,
6416
+ usage: {
6417
+ input_tokens: acc.inputTokens,
6418
+ output_tokens: acc.outputTokens
6419
+ },
6420
+ stop_reason: acc.stopReason || void 0,
6421
+ content: contentBlocks.length > 0 ? {
6422
+ role: "assistant",
6423
+ content: contentBlocks
6424
+ } : null,
6425
+ toolCalls: acc.toolCalls.length > 0 ? acc.toolCalls : void 0
6426
+ }, Date.now() - ctx.startTime);
6427
+ }
6260
6428
 
6261
6429
  //#endregion
6262
6430
  //#region src/routes/messages/non-stream-translation.ts
@@ -6762,7 +6930,7 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
6762
6930
  });
6763
6931
  });
6764
6932
  }
6765
- return handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateResult);
6933
+ return handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateResult, effectivePayload);
6766
6934
  } catch (error) {
6767
6935
  if (error instanceof HTTPError && error.status === 413) logPayloadSizeInfoAnthropic(effectivePayload, selectedModel);
6768
6936
  recordErrorResponse(ctx, anthropicPayload.model, error);
@@ -6787,7 +6955,7 @@ function logPayloadSizeInfoAnthropic(payload, model) {
6787
6955
  /**
6788
6956
  * Handle non-streaming direct Anthropic response
6789
6957
  */
6790
- function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateResult) {
6958
+ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateResult, payload) {
6791
6959
  consola.debug("Non-streaming response from Copilot (direct Anthropic):", JSON.stringify(response).slice(-400));
6792
6960
  recordResponse(ctx.historyId, {
6793
6961
  success: true,
@@ -6823,6 +6991,16 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
6823
6991
  outputTokens: response.usage.output_tokens,
6824
6992
  queueWaitMs: ctx.queueWaitMs
6825
6993
  });
6994
+ captureRequest({
6995
+ model: response.model,
6996
+ inputTokens: response.usage.input_tokens,
6997
+ outputTokens: response.usage.output_tokens,
6998
+ durationMs: Date.now() - ctx.startTime,
6999
+ success: true,
7000
+ stream: false,
7001
+ toolCount: payload.tools?.length ?? 0,
7002
+ stopReason: response.stop_reason ?? void 0
7003
+ });
6826
7004
  let finalResponse = response;
6827
7005
  if (state.verbose && truncateResult?.wasCompacted) finalResponse = prependMarkerToAnthropicResponse$1(response, createTruncationMarker$1(truncateResult));
6828
7006
  return c.json(finalResponse);
@@ -6875,8 +7053,14 @@ async function handleDirectAnthropicStreamingResponse(opts) {
6875
7053
  data: rawEvent.data
6876
7054
  });
6877
7055
  }
6878
- recordStreamingResponse$1(acc, anthropicPayload.model, ctx);
6879
- completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs);
7056
+ recordAnthropicStreamingResponse(acc, anthropicPayload.model, ctx);
7057
+ completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs, void 0, {
7058
+ model: acc.model || anthropicPayload.model,
7059
+ stream: true,
7060
+ durationMs: Date.now() - ctx.startTime,
7061
+ stopReason: acc.stopReason || void 0,
7062
+ toolCount: anthropicPayload.tools?.length ?? 0
7063
+ });
6880
7064
  } catch (error) {
6881
7065
  consola.error("Direct Anthropic stream error:", error);
6882
7066
  recordStreamError({
@@ -6893,31 +7077,6 @@ async function handleDirectAnthropicStreamingResponse(opts) {
6893
7077
  });
6894
7078
  }
6895
7079
  }
6896
- function recordStreamingResponse$1(acc, fallbackModel, ctx) {
6897
- const contentBlocks = [];
6898
- if (acc.content) contentBlocks.push({
6899
- type: "text",
6900
- text: acc.content
6901
- });
6902
- for (const tc of acc.toolCalls) contentBlocks.push({
6903
- type: "tool_use",
6904
- ...tc
6905
- });
6906
- recordResponse(ctx.historyId, {
6907
- success: true,
6908
- model: acc.model || fallbackModel,
6909
- usage: {
6910
- input_tokens: acc.inputTokens,
6911
- output_tokens: acc.outputTokens
6912
- },
6913
- stop_reason: acc.stopReason || void 0,
6914
- content: contentBlocks.length > 0 ? {
6915
- role: "assistant",
6916
- content: contentBlocks
6917
- } : null,
6918
- toolCalls: acc.toolCalls.length > 0 ? acc.toolCalls : void 0
6919
- }, Date.now() - ctx.startTime);
6920
- }
6921
7080
 
6922
7081
  //#endregion
6923
7082
  //#region src/routes/messages/subagent-marker.ts
@@ -6984,7 +7143,8 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
6984
7143
  c,
6985
7144
  response,
6986
7145
  toolNameMapping,
6987
- ctx
7146
+ ctx,
7147
+ anthropicPayload
6988
7148
  });
6989
7149
  consola.debug("Streaming response from Copilot");
6990
7150
  updateTrackerStatus(ctx.trackingId, "streaming");
@@ -7004,7 +7164,7 @@ async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOve
7004
7164
  }
7005
7165
  }
7006
7166
  function handleNonStreamingResponse(opts) {
7007
- const { c, response, toolNameMapping, ctx } = opts;
7167
+ const { c, response, toolNameMapping, ctx, anthropicPayload } = opts;
7008
7168
  consola.debug("Non-streaming response from Copilot:", JSON.stringify(response).slice(-400));
7009
7169
  let anthropicResponse = translateToAnthropic(response, toolNameMapping);
7010
7170
  consola.debug("Translated Anthropic response:", JSON.stringify(anthropicResponse));
@@ -7040,6 +7200,16 @@ function handleNonStreamingResponse(opts) {
7040
7200
  outputTokens: anthropicResponse.usage.output_tokens,
7041
7201
  queueWaitMs: ctx.queueWaitMs
7042
7202
  });
7203
+ captureRequest({
7204
+ model: anthropicResponse.model,
7205
+ inputTokens: anthropicResponse.usage.input_tokens,
7206
+ outputTokens: anthropicResponse.usage.output_tokens,
7207
+ durationMs: Date.now() - ctx.startTime,
7208
+ success: true,
7209
+ stream: false,
7210
+ toolCount: anthropicPayload.tools?.length ?? 0,
7211
+ stopReason: anthropicResponse.stop_reason ?? void 0
7212
+ });
7043
7213
  return c.json(anthropicResponse);
7044
7214
  }
7045
7215
  function prependMarkerToAnthropicResponse(response, marker) {
@@ -7084,8 +7254,14 @@ async function handleStreamingResponse(opts) {
7084
7254
  acc,
7085
7255
  checkRepetition
7086
7256
  });
7087
- recordStreamingResponse(acc, anthropicPayload.model, ctx);
7088
- completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs);
7257
+ recordAnthropicStreamingResponse(acc, anthropicPayload.model, ctx);
7258
+ completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs, void 0, {
7259
+ model: acc.model || anthropicPayload.model,
7260
+ stream: true,
7261
+ durationMs: Date.now() - ctx.startTime,
7262
+ stopReason: acc.stopReason || void 0,
7263
+ toolCount: anthropicPayload.tools?.length ?? 0
7264
+ });
7089
7265
  } catch (error) {
7090
7266
  consola.error("Stream error:", error);
7091
7267
  recordStreamError({
@@ -7163,31 +7339,6 @@ async function processStreamChunks(opts) {
7163
7339
  }
7164
7340
  }
7165
7341
  }
7166
- function recordStreamingResponse(acc, fallbackModel, ctx) {
7167
- const contentBlocks = [];
7168
- if (acc.content) contentBlocks.push({
7169
- type: "text",
7170
- text: acc.content
7171
- });
7172
- for (const tc of acc.toolCalls) contentBlocks.push({
7173
- type: "tool_use",
7174
- ...tc
7175
- });
7176
- recordResponse(ctx.historyId, {
7177
- success: true,
7178
- model: acc.model || fallbackModel,
7179
- usage: {
7180
- input_tokens: acc.inputTokens,
7181
- output_tokens: acc.outputTokens
7182
- },
7183
- stop_reason: acc.stopReason || void 0,
7184
- content: contentBlocks.length > 0 ? {
7185
- role: "assistant",
7186
- content: contentBlocks
7187
- } : null,
7188
- toolCalls: acc.toolCalls.length > 0 ? acc.toolCalls : void 0
7189
- }, Date.now() - ctx.startTime);
7190
- }
7191
7342
 
7192
7343
  //#endregion
7193
7344
  //#region src/routes/messages/handler.ts
@@ -7361,7 +7512,7 @@ modelRoutes.get("/", async (c) => {
7361
7512
  const createResponses = async (payload, { vision, initiator }) => {
7362
7513
  if (!state.copilotToken) throw new Error("Copilot token not found");
7363
7514
  const headers = {
7364
- ...copilotHeaders(state, vision),
7515
+ ...copilotHeaders(state, { vision }),
7365
7516
  "X-Initiator": initiator
7366
7517
  };
7367
7518
  payload.service_tier = null;
@@ -7633,7 +7784,12 @@ const handleResponses = async (c) => {
7633
7784
  if (finalResult) {
7634
7785
  recordResponseResult(finalResult, model, historyId, startTime);
7635
7786
  const usage = finalResult.usage;
7636
- completeTracking(trackingId, usage?.input_tokens ?? 0, usage?.output_tokens ?? 0, queueWaitMs);
7787
+ completeTracking(trackingId, usage?.input_tokens ?? 0, usage?.output_tokens ?? 0, queueWaitMs, usage?.output_tokens_details?.reasoning_tokens, {
7788
+ model: finalResult.model || model,
7789
+ stream: true,
7790
+ durationMs: Date.now() - startTime,
7791
+ toolCount: tools.length
7792
+ });
7637
7793
  } else if (streamErrorMessage) {
7638
7794
  recordResponse(historyId, {
7639
7795
  success: false,
@@ -7662,7 +7818,12 @@ const handleResponses = async (c) => {
7662
7818
  const result = response;
7663
7819
  const usage = result.usage;
7664
7820
  recordResponseResult(result, model, historyId, startTime);
7665
- completeTracking(trackingId, usage?.input_tokens ?? 0, usage?.output_tokens ?? 0, ctx.queueWaitMs);
7821
+ completeTracking(trackingId, usage?.input_tokens ?? 0, usage?.output_tokens ?? 0, ctx.queueWaitMs, usage?.output_tokens_details?.reasoning_tokens, {
7822
+ model: result.model || model,
7823
+ stream: false,
7824
+ durationMs: Date.now() - startTime,
7825
+ toolCount: tools.length
7826
+ });
7666
7827
  consola.debug("Forwarding native Responses result:", JSON.stringify(result).slice(-400));
7667
7828
  return c.json(result);
7668
7829
  } catch (error) {
@@ -7713,7 +7874,8 @@ function recordResponseResult(result, fallbackModel, historyId, startTime) {
7713
7874
  model: result.model || fallbackModel,
7714
7875
  usage: {
7715
7876
  input_tokens: usage?.input_tokens ?? 0,
7716
- output_tokens: usage?.output_tokens ?? 0
7877
+ output_tokens: usage?.output_tokens ?? 0,
7878
+ ...usage?.output_tokens_details ? { output_tokens_details: { reasoning_tokens: usage.output_tokens_details.reasoning_tokens } } : {}
7717
7879
  },
7718
7880
  stop_reason: extractResponseStopReason(result),
7719
7881
  content,
@@ -7788,6 +7950,18 @@ server.route("/history", historyRoutes);
7788
7950
 
7789
7951
  //#endregion
7790
7952
  //#region src/start.ts
7953
+ const VALID_ACCOUNT_TYPES = [
7954
+ "individual",
7955
+ "business",
7956
+ "enterprise"
7957
+ ];
7958
+ function isValidAccountType(accountType) {
7959
+ return VALID_ACCOUNT_TYPES.includes(accountType);
7960
+ }
7961
+ function validateAccountType(accountType) {
7962
+ if (isValidAccountType(accountType)) return;
7963
+ throw new Error(`Invalid account type: "${accountType}". Available: ${VALID_ACCOUNT_TYPES.join(", ")}`);
7964
+ }
7791
7965
  /** Format limit values as "Xk" or "?" if not available */
7792
7966
  function formatLimit(value) {
7793
7967
  return value ? `${Math.round(value / 1e3)}k` : "?";
@@ -7810,6 +7984,12 @@ async function runServer(options) {
7810
7984
  state.verbose = true;
7811
7985
  }
7812
7986
  state.accountType = options.accountType;
7987
+ try {
7988
+ validateAccountType(state.accountType);
7989
+ } catch (error) {
7990
+ consola.error(error instanceof Error ? error.message : String(error));
7991
+ process.exit(1);
7992
+ }
7813
7993
  if (options.accountType !== "individual") consola.info(`Using ${options.accountType} plan GitHub account`);
7814
7994
  state.manualApprove = options.manual;
7815
7995
  state.showToken = options.showToken;
@@ -7834,6 +8014,10 @@ async function runServer(options) {
7834
8014
  const limitText = options.historyLimit === 0 ? "unlimited" : `max ${options.historyLimit}`;
7835
8015
  consola.info(`History recording enabled (${limitText} entries)`);
7836
8016
  }
8017
+ if (options.posthogKey) {
8018
+ initPostHog(options.posthogKey);
8019
+ if (isPostHogEnabled()) consola.info("PostHog analytics enabled");
8020
+ }
7837
8021
  initTui({ enabled: true });
7838
8022
  initRequestContextManager(state.staleRequestMaxAge).startReaper();
7839
8023
  await ensurePaths();
@@ -7843,7 +8027,13 @@ async function runServer(options) {
7843
8027
  consola.info("Using provided GitHub token");
7844
8028
  } else await setupGitHubToken();
7845
8029
  await setupCopilotToken();
7846
- await cacheModels();
8030
+ try {
8031
+ await cacheModels();
8032
+ } catch (error) {
8033
+ consola.error(`Failed to fetch available models for account type "${state.accountType}". Check that the account type matches your Copilot plan.`);
8034
+ consola.error(error instanceof Error ? error.message : String(error));
8035
+ process.exit(1);
8036
+ }
7847
8037
  consola.info(`Available models:\n${state.models?.data.map((m) => formatModelInfo(m)).join("\n")}`);
7848
8038
  const serverUrl = `http://${options.host ?? "localhost"}:${options.port}`;
7849
8039
  if (options.claudeCode) {
@@ -8002,6 +8192,10 @@ const start = defineCommand({
8002
8192
  type: "string",
8003
8193
  default: "+8",
8004
8194
  description: "Timezone offset in hours from UTC for log timestamps (e.g., +8, -5, 0)"
8195
+ },
8196
+ "posthog-key": {
8197
+ type: "string",
8198
+ description: "PostHog API key for token usage analytics (opt-in, no key = disabled)"
8005
8199
  }
8006
8200
  },
8007
8201
  run({ args }) {
@@ -8026,7 +8220,8 @@ const start = defineCommand({
8026
8220
  compressToolResults: args["compress-tool-results"],
8027
8221
  redirectAnthropic: args["redirect-anthropic"],
8028
8222
  rewriteAnthropicTools: !args["no-rewrite-anthropic-tools"],
8029
- timezoneOffset: parseTimezoneOffset(args["timezone-offset"])
8223
+ timezoneOffset: parseTimezoneOffset(args["timezone-offset"]),
8224
+ posthogKey: args["posthog-key"]
8030
8225
  });
8031
8226
  }
8032
8227
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.4.3",
3
+ "version": "0.6.0",
4
4
  "description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!",
5
5
  "author": "dianshuv",
6
6
  "type": "module",
@@ -41,6 +41,7 @@
41
41
  "gpt-tokenizer": "^3.4.0",
42
42
  "hono": "^4.11.7",
43
43
  "picocolors": "^1.1.1",
44
+ "posthog-node": "^5.28.6",
44
45
  "proxy-from-env": "^1.1.0",
45
46
  "srvx": "^0.10.1",
46
47
  "tiny-invariant": "^1.3.3",