@dianshuv/copilot-api 0.20.7 → 0.21.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 +9 -1
  2. package/dist/main.mjs +66 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -15,7 +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
+ - **PostHog analytics**: Optional PostHog Cloud integration (`--posthog-key`) sends per-request token usage and estimated GitHub AI credit consumption for long-term trend analysis. Credit estimates use the live per-model prices returned by Copilot's `/models` endpoint, including default/long-context and cache pricing; no price table is hardcoded. Free tier (1M events/month) is more than sufficient for individual use.
19
19
  - **GitHub Copilot CLI emulation**: All upstream requests to GitHub — device-flow `login`, `/copilot_internal/user` bootstrap, and CAPI model/chat calls — carry the official GitHub Copilot CLI's (`@github/copilot`) identity: its `copilot-integration-id` (`copilot-developer-cli`), `editor-version`/`user-agent` (`copilot/<version>`), `x-github-api-version`, and a persistent `x-client-machine-id` (stored at `~/.local/share/copilot-api/machine_id`). The CAPI Bearer is the GitHub OAuth token itself — the CLI does not perform a separate token exchange. The `login` flow uses the CLI's own OAuth app, so **new** logins request the `read:user`, `read:org`, `repo`, and `gist` scopes; existing tokens keep working without re-authentication.
20
20
 
21
21
  ## Quick Start
@@ -120,6 +120,14 @@ Currently hidden (grouped):
120
120
  | `/history` | GET | Request history Web UI with token analytics (enabled by default) |
121
121
  | `/history/api/*` | GET/DELETE | History API endpoints |
122
122
 
123
+ ### PostHog AI credit analytics
124
+
125
+ When `--posthog-key` is configured, successful `copilot_api_request` events with upstream usage data include an `ai_credits` property. The proxy calculates it from the request's fresh input, output, cache-read, and cache-write token counts using the selected model's `billing.token_prices` metadata cached from Copilot's `/models` response. If total prompt tokens exceed the model's default threshold, the calculation uses its `long_context` prices.
126
+
127
+ To chart daily consumption in PostHog, filter to `copilot_api_request`, aggregate **sum of `ai_credits`**, set the interval to **day**, and optionally break down by `model`. Events omit `ai_credits` when Copilot provides no usage data or usable billing metadata; zero-priced models report `0`.
128
+
129
+ `ai_credits` is a per-request estimate from token categories exposed by the upstream response. If an upstream API bills a token category it does not report (for example, an undisclosed cache write), the GitHub billing total can be higher than the estimate.
130
+
123
131
  ## Auto-Truncate
124
132
 
125
133
  When enabled (default), auto-truncate automatically compacts conversation history when it exceeds the model's token limit. This prevents request failures due to context overflow.
package/dist/main.mjs CHANGED
@@ -1011,7 +1011,7 @@ const logout = defineCommand({
1011
1011
 
1012
1012
  //#endregion
1013
1013
  //#region package.json
1014
- var version = "0.20.7";
1014
+ var version = "0.21.0";
1015
1015
 
1016
1016
  //#endregion
1017
1017
  //#region src/lib/event-loop-lag.ts
@@ -1596,6 +1596,8 @@ function captureRequest(params) {
1596
1596
  if (params.cachedInputTokens !== void 0) properties.cached_input_tokens = params.cachedInputTokens;
1597
1597
  if (params.cacheCreationInputTokens !== void 0) properties.cache_creation_input_tokens = params.cacheCreationInputTokens;
1598
1598
  if (params.totalInputTokens !== void 0) properties.total_input_tokens = params.totalInputTokens;
1599
+ const aiCredits = calculateAiCredits(params);
1600
+ if (aiCredits !== void 0) properties.ai_credits = aiCredits;
1599
1601
  if (params.stopReason !== void 0) properties.stop_reason = params.stopReason;
1600
1602
  if (params.status !== void 0) properties.status = params.status;
1601
1603
  if (params.copilotErrorCode !== void 0) properties.copilot_error_code = params.copilotErrorCode;
@@ -1613,6 +1615,47 @@ function captureRequest(params) {
1613
1615
  properties
1614
1616
  });
1615
1617
  }
1618
+ function calculateAiCredits(params) {
1619
+ const tokenPrices = params.tokenPrices;
1620
+ if (!isModelTokenPrices(tokenPrices)) return void 0;
1621
+ const cachedInputTokens = params.cachedInputTokens ?? 0;
1622
+ const cacheCreationInputTokens = params.cacheCreationInputTokens ?? 0;
1623
+ const totalInputTokens = params.totalInputTokens ?? params.inputTokens + cachedInputTokens + cacheCreationInputTokens;
1624
+ const defaultMaxPromptTokens = tokenPrices.default.max_prompt_tokens;
1625
+ const prices = tokenPrices.long_context && defaultMaxPromptTokens !== void 0 && isNonNegativeFiniteNumber(defaultMaxPromptTokens) && totalInputTokens > defaultMaxPromptTokens ? tokenPrices.long_context : tokenPrices.default;
1626
+ const tokens = [
1627
+ params.inputTokens,
1628
+ params.outputTokens,
1629
+ cachedInputTokens,
1630
+ cacheCreationInputTokens,
1631
+ totalInputTokens
1632
+ ];
1633
+ const rates = [
1634
+ prices.input_price,
1635
+ prices.output_price,
1636
+ prices.cache_read_price,
1637
+ prices.cache_write_price
1638
+ ];
1639
+ if (!tokens.every((token) => isNonNegativeFiniteNumber(token)) || !rates.every((rate) => isNonNegativeFiniteNumber(rate)) || !isNonNegativeFiniteNumber(tokenPrices.batch_size)) return;
1640
+ if (tokenPrices.batch_size === 0) return rates.every((rate) => rate === 0) ? 0 : void 0;
1641
+ return (params.inputTokens * prices.input_price + params.outputTokens * prices.output_price + cachedInputTokens * prices.cache_read_price + cacheCreationInputTokens * prices.cache_write_price) / tokenPrices.batch_size;
1642
+ }
1643
+ function isModelTokenPrices(value) {
1644
+ if (!isRecord$1(value)) return false;
1645
+ if (!isNonNegativeFiniteNumber(value.batch_size)) return false;
1646
+ if (!isModelTokenPriceTier(value.default)) return false;
1647
+ return value.long_context === void 0 || isModelTokenPriceTier(value.long_context);
1648
+ }
1649
+ function isModelTokenPriceTier(value) {
1650
+ if (!isRecord$1(value)) return false;
1651
+ return isNonNegativeFiniteNumber(value.cache_read_price) && isNonNegativeFiniteNumber(value.cache_write_price) && isNonNegativeFiniteNumber(value.input_price) && isNonNegativeFiniteNumber(value.output_price) && (value.max_prompt_tokens === void 0 || isNonNegativeFiniteNumber(value.max_prompt_tokens));
1652
+ }
1653
+ function isRecord$1(value) {
1654
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1655
+ }
1656
+ function isNonNegativeFiniteNumber(value) {
1657
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
1658
+ }
1616
1659
  async function shutdownPostHog() {
1617
1660
  if (!client) return;
1618
1661
  try {
@@ -4062,14 +4105,6 @@ function recordStreamError(opts) {
4062
4105
 
4063
4106
  //#endregion
4064
4107
  //#region src/routes/tracker-mutations.ts
4065
- /**
4066
- * TUI tracker mutations and analytics completion.
4067
- *
4068
- * All in-place updates to the TUI request tracker (model, status, resolved
4069
- * model) and the success/failure terminal transitions. `completeTracking`
4070
- * additionally emits a PostHog analytics event for successful requests; error
4071
- * paths emit their PostHog events through `observability-recording.ts`.
4072
- */
4073
4108
  /** Helper to update tracker model */
4074
4109
  function updateTrackerModel(trackingId, model, resolvedModel) {
4075
4110
  if (!trackingId) return;
@@ -4119,6 +4154,7 @@ function completeTracking(trackingId, inputTokens, outputTokens, queueWaitMs, re
4119
4154
  cachedInputTokens: cache?.cachedInputTokens,
4120
4155
  cacheCreationInputTokens: cache?.cacheCreationInputTokens,
4121
4156
  totalInputTokens: cache?.totalInputTokens,
4157
+ tokenPrices: analytics.tokenPrices,
4122
4158
  stopReason: analytics.stopReason
4123
4159
  });
4124
4160
  }
@@ -4265,7 +4301,7 @@ async function executeRequest(opts) {
4265
4301
  signal: abort.signal
4266
4302
  }));
4267
4303
  ctx.queueWaitMs = queueWaitMs;
4268
- if (isNonStreaming(response)) return handleNonStreamingResponse(c, response, ctx, payload);
4304
+ if (isNonStreaming(response)) return handleNonStreamingResponse(c, response, ctx, payload, selectedModel);
4269
4305
  consola.debug("Streaming response");
4270
4306
  updateTrackerStatus(ctx.trackingId, "streaming");
4271
4307
  return streamSSE(c, async (stream) => {
@@ -4274,6 +4310,7 @@ async function executeRequest(opts) {
4274
4310
  stream,
4275
4311
  response,
4276
4312
  payload,
4313
+ selectedModel,
4277
4314
  ctx
4278
4315
  });
4279
4316
  });
@@ -4299,7 +4336,7 @@ async function logTokenCount(payload, selectedModel) {
4299
4336
  consola.debug("Failed to calculate token count:", error);
4300
4337
  }
4301
4338
  }
4302
- function handleNonStreamingResponse(c, originalResponse, ctx, payload) {
4339
+ function handleNonStreamingResponse(c, originalResponse, ctx, payload, selectedModel) {
4303
4340
  consola.debug("Non-streaming response:", JSON.stringify(originalResponse));
4304
4341
  let response = originalResponse;
4305
4342
  if (state.verbose && ctx.truncateResult?.wasCompacted && response.choices[0]?.message.content) {
@@ -4354,6 +4391,7 @@ function handleNonStreamingResponse(c, originalResponse, ctx, payload) {
4354
4391
  cachedInputTokens,
4355
4392
  cacheCreationInputTokens: 0,
4356
4393
  totalInputTokens: usage?.prompt_tokens,
4394
+ tokenPrices: usage ? selectedModel?.billing?.token_prices : void 0,
4357
4395
  stopReason: choice.finish_reason
4358
4396
  });
4359
4397
  return c.json(echoResponseBody(response, ctx));
@@ -4386,6 +4424,7 @@ function createStreamAccumulator() {
4386
4424
  outputTokens: 0,
4387
4425
  cachedTokens: 0,
4388
4426
  reasoningTokens: 0,
4427
+ usageObserved: false,
4389
4428
  finishReason: "",
4390
4429
  content: "",
4391
4430
  toolCalls: [],
@@ -4393,7 +4432,7 @@ function createStreamAccumulator() {
4393
4432
  };
4394
4433
  }
4395
4434
  async function handleStreamingResponse(opts) {
4396
- const { stream, response, payload, ctx } = opts;
4435
+ const { stream, response, payload, selectedModel, ctx } = opts;
4397
4436
  const acc = createStreamAccumulator();
4398
4437
  const checkRepetition = createStreamRepetitionChecker(`openai:${payload.model}`);
4399
4438
  try {
@@ -4427,7 +4466,8 @@ async function handleStreamingResponse(opts) {
4427
4466
  stream: true,
4428
4467
  durationMs: Date.now() - ctx.startTime,
4429
4468
  stopReason: acc.finishReason || void 0,
4430
- toolCount: payload.tools?.length ?? 0
4469
+ toolCount: payload.tools?.length ?? 0,
4470
+ tokenPrices: acc.usageObserved ? selectedModel?.billing?.token_prices : void 0
4431
4471
  }, ctx.timings, {
4432
4472
  cachedInputTokens: acc.cachedTokens,
4433
4473
  cacheCreationInputTokens: 0,
@@ -4499,6 +4539,7 @@ async function accumulateAndEchoChunk(chunk, acc, checkRepetition, ctx, stream)
4499
4539
  function accumulateParsedChunk(parsed, acc, checkRepetition) {
4500
4540
  if (parsed.model && !acc.model) acc.model = parsed.model;
4501
4541
  if (parsed.usage) {
4542
+ acc.usageObserved = true;
4502
4543
  acc.inputTokens = parsed.usage.prompt_tokens;
4503
4544
  acc.outputTokens = parsed.usage.completion_tokens;
4504
4545
  acc.cachedTokens = parsed.usage.prompt_tokens_details?.cached_tokens ?? 0;
@@ -7899,11 +7940,12 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
7899
7940
  stream,
7900
7941
  response,
7901
7942
  anthropicPayload: effectivePayload,
7943
+ selectedModel,
7902
7944
  ctx
7903
7945
  });
7904
7946
  });
7905
7947
  }
7906
- return handleDirectAnthropicNonStreamingResponse(c, recoverLeakedToolCallsInResponse(response, toolNameSet(effectivePayload.tools)), ctx, truncateResult, effectivePayload);
7948
+ return handleDirectAnthropicNonStreamingResponse(c, recoverLeakedToolCallsInResponse(response, toolNameSet(effectivePayload.tools)), ctx, truncateResult, effectivePayload, selectedModel);
7907
7949
  }
7908
7950
  consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (direct Anthropic)");
7909
7951
  updateTrackerStatus(ctx.trackingId, "streaming");
@@ -7919,6 +7961,7 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
7919
7961
  stream,
7920
7962
  response,
7921
7963
  anthropicPayload: effectivePayload,
7964
+ selectedModel,
7922
7965
  ctx
7923
7966
  });
7924
7967
  },
@@ -7973,7 +8016,7 @@ function logPayloadSizeInfoAnthropic(payload, model) {
7973
8016
  /**
7974
8017
  * Handle non-streaming direct Anthropic response
7975
8018
  */
7976
- function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateResult, payload) {
8019
+ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateResult, payload, selectedModel) {
7977
8020
  consola.debug("Non-streaming response from Copilot (direct Anthropic):", JSON.stringify(response).slice(-400));
7978
8021
  recordResponse(ctx.historyId, {
7979
8022
  success: true,
@@ -8026,6 +8069,7 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
8026
8069
  cachedInputTokens: cacheRead,
8027
8070
  cacheCreationInputTokens: cacheCreation,
8028
8071
  totalInputTokens,
8072
+ tokenPrices: selectedModel?.billing?.token_prices,
8029
8073
  stopReason: response.stop_reason ?? void 0
8030
8074
  });
8031
8075
  let finalResponse = response;
@@ -8071,7 +8115,7 @@ function echoForwardData(forwardData, eventType, ctx) {
8071
8115
  * Handle streaming direct Anthropic response (passthrough SSE events)
8072
8116
  */
8073
8117
  async function handleDirectAnthropicStreamingResponse(opts) {
8074
- const { stream, response, anthropicPayload, ctx } = opts;
8118
+ const { stream, response, anthropicPayload, selectedModel, ctx } = opts;
8075
8119
  const acc = createAnthropicStreamAccumulator();
8076
8120
  const checkRepetition = createStreamRepetitionChecker(`anthropic:${anthropicPayload.model}`);
8077
8121
  const serverToolFilter = createServerToolBlockFilter();
@@ -8110,7 +8154,8 @@ async function handleDirectAnthropicStreamingResponse(opts) {
8110
8154
  stream: true,
8111
8155
  durationMs: Date.now() - ctx.startTime,
8112
8156
  stopReason: acc.stopReason || void 0,
8113
- toolCount: anthropicPayload.tools?.length ?? 0
8157
+ toolCount: anthropicPayload.tools?.length ?? 0,
8158
+ tokenPrices: selectedModel?.billing?.token_prices
8114
8159
  }, ctx.timings, {
8115
8160
  cachedInputTokens: acc.cacheReadInputTokens,
8116
8161
  cacheCreationInputTokens: acc.cacheCreationInputTokens,
@@ -9041,7 +9086,8 @@ const handleResponses = async (c) => {
9041
9086
  model: finalResult.model || model,
9042
9087
  stream: true,
9043
9088
  durationMs: Date.now() - startTime,
9044
- toolCount: tools.length
9089
+ toolCount: tools.length,
9090
+ tokenPrices: usage ? selectedModel?.billing?.token_prices : void 0
9045
9091
  }, ctx.timings, {
9046
9092
  cachedInputTokens,
9047
9093
  cacheCreationInputTokens: 0,
@@ -9098,7 +9144,8 @@ const handleResponses = async (c) => {
9098
9144
  model: result.model || model,
9099
9145
  stream: false,
9100
9146
  durationMs: Date.now() - startTime,
9101
- toolCount: tools.length
9147
+ toolCount: tools.length,
9148
+ tokenPrices: usage ? selectedModel?.billing?.token_prices : void 0
9102
9149
  }, ctx.timings, {
9103
9150
  cachedInputTokens,
9104
9151
  cacheCreationInputTokens: 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.20.7",
3
+ "version": "0.21.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",