@dianshuv/copilot-api 0.20.6 → 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.
- package/README.md +11 -3
- package/dist/main.mjs +67 -19
- 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
|
|
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
|
|
@@ -67,7 +67,7 @@ make down
|
|
|
67
67
|
|
|
68
68
|
### Hidden Models
|
|
69
69
|
|
|
70
|
-
By default the proxy hides
|
|
70
|
+
By default the proxy hides 38 stale / duplicate / unused upstream model ids from
|
|
71
71
|
its **listing surfaces** (`/v1/models` and the startup banner).
|
|
72
72
|
|
|
73
73
|
**Important**: this is a *display* filter only. The remaining POST endpoints
|
|
@@ -88,7 +88,7 @@ Currently hidden (grouped):
|
|
|
88
88
|
- **Older / smaller GPT-5** — `gpt-5-mini`, `gpt-5.3-codex`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.5`
|
|
89
89
|
- **All embeddings** — `text-embedding-ada-002`, `text-embedding-3-small`, `text-embedding-3-small-inference`
|
|
90
90
|
- **Older Gemini** — `gemini-2.5-pro`, `gemini-3-flash-preview`, `gemini-3.5-flash`
|
|
91
|
-
- **Older / variant Claude** — `claude-opus-4.5`, `claude-opus-4.6`, `claude-opus-4.7`, `claude-opus-4.7-high`, `claude-opus-4.7-xhigh`, `claude-sonnet-4.5`, `claude-sonnet-4.6`
|
|
91
|
+
- **Older / variant Claude** — `claude-opus-4.5`, `claude-opus-4.6`, `claude-opus-4.7`, `claude-opus-4.8`, `claude-opus-4.7-high`, `claude-opus-4.7-xhigh`, `claude-sonnet-4.5`, `claude-sonnet-4.6`
|
|
92
92
|
- **Special-purpose** — `mai-code-1-flash-internal`, `mai-code-1-flash-picker`, `trajectory-compaction`
|
|
93
93
|
|
|
94
94
|
## API Endpoints
|
|
@@ -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.
|
|
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 {
|
|
@@ -2462,6 +2505,7 @@ const HIDDEN_MODEL_IDS = new Set([
|
|
|
2462
2505
|
"claude-opus-4.5",
|
|
2463
2506
|
"claude-opus-4.6",
|
|
2464
2507
|
"claude-opus-4.7",
|
|
2508
|
+
"claude-opus-4.8",
|
|
2465
2509
|
"claude-opus-4.7-high",
|
|
2466
2510
|
"claude-opus-4.7-xhigh",
|
|
2467
2511
|
"claude-sonnet-4.5",
|
|
@@ -4061,14 +4105,6 @@ function recordStreamError(opts) {
|
|
|
4061
4105
|
|
|
4062
4106
|
//#endregion
|
|
4063
4107
|
//#region src/routes/tracker-mutations.ts
|
|
4064
|
-
/**
|
|
4065
|
-
* TUI tracker mutations and analytics completion.
|
|
4066
|
-
*
|
|
4067
|
-
* All in-place updates to the TUI request tracker (model, status, resolved
|
|
4068
|
-
* model) and the success/failure terminal transitions. `completeTracking`
|
|
4069
|
-
* additionally emits a PostHog analytics event for successful requests; error
|
|
4070
|
-
* paths emit their PostHog events through `observability-recording.ts`.
|
|
4071
|
-
*/
|
|
4072
4108
|
/** Helper to update tracker model */
|
|
4073
4109
|
function updateTrackerModel(trackingId, model, resolvedModel) {
|
|
4074
4110
|
if (!trackingId) return;
|
|
@@ -4118,6 +4154,7 @@ function completeTracking(trackingId, inputTokens, outputTokens, queueWaitMs, re
|
|
|
4118
4154
|
cachedInputTokens: cache?.cachedInputTokens,
|
|
4119
4155
|
cacheCreationInputTokens: cache?.cacheCreationInputTokens,
|
|
4120
4156
|
totalInputTokens: cache?.totalInputTokens,
|
|
4157
|
+
tokenPrices: analytics.tokenPrices,
|
|
4121
4158
|
stopReason: analytics.stopReason
|
|
4122
4159
|
});
|
|
4123
4160
|
}
|
|
@@ -4264,7 +4301,7 @@ async function executeRequest(opts) {
|
|
|
4264
4301
|
signal: abort.signal
|
|
4265
4302
|
}));
|
|
4266
4303
|
ctx.queueWaitMs = queueWaitMs;
|
|
4267
|
-
if (isNonStreaming(response)) return handleNonStreamingResponse(c, response, ctx, payload);
|
|
4304
|
+
if (isNonStreaming(response)) return handleNonStreamingResponse(c, response, ctx, payload, selectedModel);
|
|
4268
4305
|
consola.debug("Streaming response");
|
|
4269
4306
|
updateTrackerStatus(ctx.trackingId, "streaming");
|
|
4270
4307
|
return streamSSE(c, async (stream) => {
|
|
@@ -4273,6 +4310,7 @@ async function executeRequest(opts) {
|
|
|
4273
4310
|
stream,
|
|
4274
4311
|
response,
|
|
4275
4312
|
payload,
|
|
4313
|
+
selectedModel,
|
|
4276
4314
|
ctx
|
|
4277
4315
|
});
|
|
4278
4316
|
});
|
|
@@ -4298,7 +4336,7 @@ async function logTokenCount(payload, selectedModel) {
|
|
|
4298
4336
|
consola.debug("Failed to calculate token count:", error);
|
|
4299
4337
|
}
|
|
4300
4338
|
}
|
|
4301
|
-
function handleNonStreamingResponse(c, originalResponse, ctx, payload) {
|
|
4339
|
+
function handleNonStreamingResponse(c, originalResponse, ctx, payload, selectedModel) {
|
|
4302
4340
|
consola.debug("Non-streaming response:", JSON.stringify(originalResponse));
|
|
4303
4341
|
let response = originalResponse;
|
|
4304
4342
|
if (state.verbose && ctx.truncateResult?.wasCompacted && response.choices[0]?.message.content) {
|
|
@@ -4353,6 +4391,7 @@ function handleNonStreamingResponse(c, originalResponse, ctx, payload) {
|
|
|
4353
4391
|
cachedInputTokens,
|
|
4354
4392
|
cacheCreationInputTokens: 0,
|
|
4355
4393
|
totalInputTokens: usage?.prompt_tokens,
|
|
4394
|
+
tokenPrices: usage ? selectedModel?.billing?.token_prices : void 0,
|
|
4356
4395
|
stopReason: choice.finish_reason
|
|
4357
4396
|
});
|
|
4358
4397
|
return c.json(echoResponseBody(response, ctx));
|
|
@@ -4385,6 +4424,7 @@ function createStreamAccumulator() {
|
|
|
4385
4424
|
outputTokens: 0,
|
|
4386
4425
|
cachedTokens: 0,
|
|
4387
4426
|
reasoningTokens: 0,
|
|
4427
|
+
usageObserved: false,
|
|
4388
4428
|
finishReason: "",
|
|
4389
4429
|
content: "",
|
|
4390
4430
|
toolCalls: [],
|
|
@@ -4392,7 +4432,7 @@ function createStreamAccumulator() {
|
|
|
4392
4432
|
};
|
|
4393
4433
|
}
|
|
4394
4434
|
async function handleStreamingResponse(opts) {
|
|
4395
|
-
const { stream, response, payload, ctx } = opts;
|
|
4435
|
+
const { stream, response, payload, selectedModel, ctx } = opts;
|
|
4396
4436
|
const acc = createStreamAccumulator();
|
|
4397
4437
|
const checkRepetition = createStreamRepetitionChecker(`openai:${payload.model}`);
|
|
4398
4438
|
try {
|
|
@@ -4426,7 +4466,8 @@ async function handleStreamingResponse(opts) {
|
|
|
4426
4466
|
stream: true,
|
|
4427
4467
|
durationMs: Date.now() - ctx.startTime,
|
|
4428
4468
|
stopReason: acc.finishReason || void 0,
|
|
4429
|
-
toolCount: payload.tools?.length ?? 0
|
|
4469
|
+
toolCount: payload.tools?.length ?? 0,
|
|
4470
|
+
tokenPrices: acc.usageObserved ? selectedModel?.billing?.token_prices : void 0
|
|
4430
4471
|
}, ctx.timings, {
|
|
4431
4472
|
cachedInputTokens: acc.cachedTokens,
|
|
4432
4473
|
cacheCreationInputTokens: 0,
|
|
@@ -4498,6 +4539,7 @@ async function accumulateAndEchoChunk(chunk, acc, checkRepetition, ctx, stream)
|
|
|
4498
4539
|
function accumulateParsedChunk(parsed, acc, checkRepetition) {
|
|
4499
4540
|
if (parsed.model && !acc.model) acc.model = parsed.model;
|
|
4500
4541
|
if (parsed.usage) {
|
|
4542
|
+
acc.usageObserved = true;
|
|
4501
4543
|
acc.inputTokens = parsed.usage.prompt_tokens;
|
|
4502
4544
|
acc.outputTokens = parsed.usage.completion_tokens;
|
|
4503
4545
|
acc.cachedTokens = parsed.usage.prompt_tokens_details?.cached_tokens ?? 0;
|
|
@@ -7898,11 +7940,12 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
|
|
|
7898
7940
|
stream,
|
|
7899
7941
|
response,
|
|
7900
7942
|
anthropicPayload: effectivePayload,
|
|
7943
|
+
selectedModel,
|
|
7901
7944
|
ctx
|
|
7902
7945
|
});
|
|
7903
7946
|
});
|
|
7904
7947
|
}
|
|
7905
|
-
return handleDirectAnthropicNonStreamingResponse(c, recoverLeakedToolCallsInResponse(response, toolNameSet(effectivePayload.tools)), ctx, truncateResult, effectivePayload);
|
|
7948
|
+
return handleDirectAnthropicNonStreamingResponse(c, recoverLeakedToolCallsInResponse(response, toolNameSet(effectivePayload.tools)), ctx, truncateResult, effectivePayload, selectedModel);
|
|
7906
7949
|
}
|
|
7907
7950
|
consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (direct Anthropic)");
|
|
7908
7951
|
updateTrackerStatus(ctx.trackingId, "streaming");
|
|
@@ -7918,6 +7961,7 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
|
|
|
7918
7961
|
stream,
|
|
7919
7962
|
response,
|
|
7920
7963
|
anthropicPayload: effectivePayload,
|
|
7964
|
+
selectedModel,
|
|
7921
7965
|
ctx
|
|
7922
7966
|
});
|
|
7923
7967
|
},
|
|
@@ -7972,7 +8016,7 @@ function logPayloadSizeInfoAnthropic(payload, model) {
|
|
|
7972
8016
|
/**
|
|
7973
8017
|
* Handle non-streaming direct Anthropic response
|
|
7974
8018
|
*/
|
|
7975
|
-
function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateResult, payload) {
|
|
8019
|
+
function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateResult, payload, selectedModel) {
|
|
7976
8020
|
consola.debug("Non-streaming response from Copilot (direct Anthropic):", JSON.stringify(response).slice(-400));
|
|
7977
8021
|
recordResponse(ctx.historyId, {
|
|
7978
8022
|
success: true,
|
|
@@ -8025,6 +8069,7 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
|
|
|
8025
8069
|
cachedInputTokens: cacheRead,
|
|
8026
8070
|
cacheCreationInputTokens: cacheCreation,
|
|
8027
8071
|
totalInputTokens,
|
|
8072
|
+
tokenPrices: selectedModel?.billing?.token_prices,
|
|
8028
8073
|
stopReason: response.stop_reason ?? void 0
|
|
8029
8074
|
});
|
|
8030
8075
|
let finalResponse = response;
|
|
@@ -8070,7 +8115,7 @@ function echoForwardData(forwardData, eventType, ctx) {
|
|
|
8070
8115
|
* Handle streaming direct Anthropic response (passthrough SSE events)
|
|
8071
8116
|
*/
|
|
8072
8117
|
async function handleDirectAnthropicStreamingResponse(opts) {
|
|
8073
|
-
const { stream, response, anthropicPayload, ctx } = opts;
|
|
8118
|
+
const { stream, response, anthropicPayload, selectedModel, ctx } = opts;
|
|
8074
8119
|
const acc = createAnthropicStreamAccumulator();
|
|
8075
8120
|
const checkRepetition = createStreamRepetitionChecker(`anthropic:${anthropicPayload.model}`);
|
|
8076
8121
|
const serverToolFilter = createServerToolBlockFilter();
|
|
@@ -8109,7 +8154,8 @@ async function handleDirectAnthropicStreamingResponse(opts) {
|
|
|
8109
8154
|
stream: true,
|
|
8110
8155
|
durationMs: Date.now() - ctx.startTime,
|
|
8111
8156
|
stopReason: acc.stopReason || void 0,
|
|
8112
|
-
toolCount: anthropicPayload.tools?.length ?? 0
|
|
8157
|
+
toolCount: anthropicPayload.tools?.length ?? 0,
|
|
8158
|
+
tokenPrices: selectedModel?.billing?.token_prices
|
|
8113
8159
|
}, ctx.timings, {
|
|
8114
8160
|
cachedInputTokens: acc.cacheReadInputTokens,
|
|
8115
8161
|
cacheCreationInputTokens: acc.cacheCreationInputTokens,
|
|
@@ -9040,7 +9086,8 @@ const handleResponses = async (c) => {
|
|
|
9040
9086
|
model: finalResult.model || model,
|
|
9041
9087
|
stream: true,
|
|
9042
9088
|
durationMs: Date.now() - startTime,
|
|
9043
|
-
toolCount: tools.length
|
|
9089
|
+
toolCount: tools.length,
|
|
9090
|
+
tokenPrices: usage ? selectedModel?.billing?.token_prices : void 0
|
|
9044
9091
|
}, ctx.timings, {
|
|
9045
9092
|
cachedInputTokens,
|
|
9046
9093
|
cacheCreationInputTokens: 0,
|
|
@@ -9097,7 +9144,8 @@ const handleResponses = async (c) => {
|
|
|
9097
9144
|
model: result.model || model,
|
|
9098
9145
|
stream: false,
|
|
9099
9146
|
durationMs: Date.now() - startTime,
|
|
9100
|
-
toolCount: tools.length
|
|
9147
|
+
toolCount: tools.length,
|
|
9148
|
+
tokenPrices: usage ? selectedModel?.billing?.token_prices : void 0
|
|
9101
9149
|
}, ctx.timings, {
|
|
9102
9150
|
cachedInputTokens,
|
|
9103
9151
|
cacheCreationInputTokens: 0,
|