@dianshuv/copilot-api 0.20.7 → 0.21.1
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 +12 -4
- package/dist/main.mjs +68 -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 40 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
|
|
@@ -87,9 +87,9 @@ Currently hidden (grouped):
|
|
|
87
87
|
- **GPT-4.1 family** — `gpt-4.1`, `gpt-4.1-2025-04-14`, `gpt-41-copilot`
|
|
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
|
-
- **Older Gemini** — `gemini-2.5-pro`, `gemini-3-flash-preview`, `gemini-3.5-flash`
|
|
90
|
+
- **Older Gemini** — `gemini-2.5-pro`, `gemini-3-flash-preview`, `gemini-3.1-pro-preview`, `gemini-3.5-flash`
|
|
91
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
|
-
- **Special-purpose** — `mai-code-1-flash-internal`, `mai-code-1-flash-picker`, `trajectory-compaction`
|
|
92
|
+
- **Special-purpose** — `mai-code-1-flash-internal`, `mai-code-1-flash-picker`, `mai-code-1.1-flash`, `trajectory-compaction`
|
|
93
93
|
|
|
94
94
|
## API Endpoints
|
|
95
95
|
|
|
@@ -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.1";
|
|
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 {
|
|
@@ -2458,6 +2501,7 @@ const HIDDEN_MODEL_IDS = new Set([
|
|
|
2458
2501
|
"text-embedding-3-small-inference",
|
|
2459
2502
|
"gemini-2.5-pro",
|
|
2460
2503
|
"gemini-3-flash-preview",
|
|
2504
|
+
"gemini-3.1-pro-preview",
|
|
2461
2505
|
"gemini-3.5-flash",
|
|
2462
2506
|
"claude-opus-4.5",
|
|
2463
2507
|
"claude-opus-4.6",
|
|
@@ -2469,6 +2513,7 @@ const HIDDEN_MODEL_IDS = new Set([
|
|
|
2469
2513
|
"claude-sonnet-4.6",
|
|
2470
2514
|
"mai-code-1-flash-internal",
|
|
2471
2515
|
"mai-code-1-flash-picker",
|
|
2516
|
+
"mai-code-1.1-flash",
|
|
2472
2517
|
"trajectory-compaction"
|
|
2473
2518
|
]);
|
|
2474
2519
|
function isHiddenModel(id, showAll) {
|
|
@@ -4062,14 +4107,6 @@ function recordStreamError(opts) {
|
|
|
4062
4107
|
|
|
4063
4108
|
//#endregion
|
|
4064
4109
|
//#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
4110
|
/** Helper to update tracker model */
|
|
4074
4111
|
function updateTrackerModel(trackingId, model, resolvedModel) {
|
|
4075
4112
|
if (!trackingId) return;
|
|
@@ -4119,6 +4156,7 @@ function completeTracking(trackingId, inputTokens, outputTokens, queueWaitMs, re
|
|
|
4119
4156
|
cachedInputTokens: cache?.cachedInputTokens,
|
|
4120
4157
|
cacheCreationInputTokens: cache?.cacheCreationInputTokens,
|
|
4121
4158
|
totalInputTokens: cache?.totalInputTokens,
|
|
4159
|
+
tokenPrices: analytics.tokenPrices,
|
|
4122
4160
|
stopReason: analytics.stopReason
|
|
4123
4161
|
});
|
|
4124
4162
|
}
|
|
@@ -4265,7 +4303,7 @@ async function executeRequest(opts) {
|
|
|
4265
4303
|
signal: abort.signal
|
|
4266
4304
|
}));
|
|
4267
4305
|
ctx.queueWaitMs = queueWaitMs;
|
|
4268
|
-
if (isNonStreaming(response)) return handleNonStreamingResponse(c, response, ctx, payload);
|
|
4306
|
+
if (isNonStreaming(response)) return handleNonStreamingResponse(c, response, ctx, payload, selectedModel);
|
|
4269
4307
|
consola.debug("Streaming response");
|
|
4270
4308
|
updateTrackerStatus(ctx.trackingId, "streaming");
|
|
4271
4309
|
return streamSSE(c, async (stream) => {
|
|
@@ -4274,6 +4312,7 @@ async function executeRequest(opts) {
|
|
|
4274
4312
|
stream,
|
|
4275
4313
|
response,
|
|
4276
4314
|
payload,
|
|
4315
|
+
selectedModel,
|
|
4277
4316
|
ctx
|
|
4278
4317
|
});
|
|
4279
4318
|
});
|
|
@@ -4299,7 +4338,7 @@ async function logTokenCount(payload, selectedModel) {
|
|
|
4299
4338
|
consola.debug("Failed to calculate token count:", error);
|
|
4300
4339
|
}
|
|
4301
4340
|
}
|
|
4302
|
-
function handleNonStreamingResponse(c, originalResponse, ctx, payload) {
|
|
4341
|
+
function handleNonStreamingResponse(c, originalResponse, ctx, payload, selectedModel) {
|
|
4303
4342
|
consola.debug("Non-streaming response:", JSON.stringify(originalResponse));
|
|
4304
4343
|
let response = originalResponse;
|
|
4305
4344
|
if (state.verbose && ctx.truncateResult?.wasCompacted && response.choices[0]?.message.content) {
|
|
@@ -4354,6 +4393,7 @@ function handleNonStreamingResponse(c, originalResponse, ctx, payload) {
|
|
|
4354
4393
|
cachedInputTokens,
|
|
4355
4394
|
cacheCreationInputTokens: 0,
|
|
4356
4395
|
totalInputTokens: usage?.prompt_tokens,
|
|
4396
|
+
tokenPrices: usage ? selectedModel?.billing?.token_prices : void 0,
|
|
4357
4397
|
stopReason: choice.finish_reason
|
|
4358
4398
|
});
|
|
4359
4399
|
return c.json(echoResponseBody(response, ctx));
|
|
@@ -4386,6 +4426,7 @@ function createStreamAccumulator() {
|
|
|
4386
4426
|
outputTokens: 0,
|
|
4387
4427
|
cachedTokens: 0,
|
|
4388
4428
|
reasoningTokens: 0,
|
|
4429
|
+
usageObserved: false,
|
|
4389
4430
|
finishReason: "",
|
|
4390
4431
|
content: "",
|
|
4391
4432
|
toolCalls: [],
|
|
@@ -4393,7 +4434,7 @@ function createStreamAccumulator() {
|
|
|
4393
4434
|
};
|
|
4394
4435
|
}
|
|
4395
4436
|
async function handleStreamingResponse(opts) {
|
|
4396
|
-
const { stream, response, payload, ctx } = opts;
|
|
4437
|
+
const { stream, response, payload, selectedModel, ctx } = opts;
|
|
4397
4438
|
const acc = createStreamAccumulator();
|
|
4398
4439
|
const checkRepetition = createStreamRepetitionChecker(`openai:${payload.model}`);
|
|
4399
4440
|
try {
|
|
@@ -4427,7 +4468,8 @@ async function handleStreamingResponse(opts) {
|
|
|
4427
4468
|
stream: true,
|
|
4428
4469
|
durationMs: Date.now() - ctx.startTime,
|
|
4429
4470
|
stopReason: acc.finishReason || void 0,
|
|
4430
|
-
toolCount: payload.tools?.length ?? 0
|
|
4471
|
+
toolCount: payload.tools?.length ?? 0,
|
|
4472
|
+
tokenPrices: acc.usageObserved ? selectedModel?.billing?.token_prices : void 0
|
|
4431
4473
|
}, ctx.timings, {
|
|
4432
4474
|
cachedInputTokens: acc.cachedTokens,
|
|
4433
4475
|
cacheCreationInputTokens: 0,
|
|
@@ -4499,6 +4541,7 @@ async function accumulateAndEchoChunk(chunk, acc, checkRepetition, ctx, stream)
|
|
|
4499
4541
|
function accumulateParsedChunk(parsed, acc, checkRepetition) {
|
|
4500
4542
|
if (parsed.model && !acc.model) acc.model = parsed.model;
|
|
4501
4543
|
if (parsed.usage) {
|
|
4544
|
+
acc.usageObserved = true;
|
|
4502
4545
|
acc.inputTokens = parsed.usage.prompt_tokens;
|
|
4503
4546
|
acc.outputTokens = parsed.usage.completion_tokens;
|
|
4504
4547
|
acc.cachedTokens = parsed.usage.prompt_tokens_details?.cached_tokens ?? 0;
|
|
@@ -7899,11 +7942,12 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
|
|
|
7899
7942
|
stream,
|
|
7900
7943
|
response,
|
|
7901
7944
|
anthropicPayload: effectivePayload,
|
|
7945
|
+
selectedModel,
|
|
7902
7946
|
ctx
|
|
7903
7947
|
});
|
|
7904
7948
|
});
|
|
7905
7949
|
}
|
|
7906
|
-
return handleDirectAnthropicNonStreamingResponse(c, recoverLeakedToolCallsInResponse(response, toolNameSet(effectivePayload.tools)), ctx, truncateResult, effectivePayload);
|
|
7950
|
+
return handleDirectAnthropicNonStreamingResponse(c, recoverLeakedToolCallsInResponse(response, toolNameSet(effectivePayload.tools)), ctx, truncateResult, effectivePayload, selectedModel);
|
|
7907
7951
|
}
|
|
7908
7952
|
consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (direct Anthropic)");
|
|
7909
7953
|
updateTrackerStatus(ctx.trackingId, "streaming");
|
|
@@ -7919,6 +7963,7 @@ async function handleDirectAnthropicCompletion(c, anthropicPayload, ctx, initiat
|
|
|
7919
7963
|
stream,
|
|
7920
7964
|
response,
|
|
7921
7965
|
anthropicPayload: effectivePayload,
|
|
7966
|
+
selectedModel,
|
|
7922
7967
|
ctx
|
|
7923
7968
|
});
|
|
7924
7969
|
},
|
|
@@ -7973,7 +8018,7 @@ function logPayloadSizeInfoAnthropic(payload, model) {
|
|
|
7973
8018
|
/**
|
|
7974
8019
|
* Handle non-streaming direct Anthropic response
|
|
7975
8020
|
*/
|
|
7976
|
-
function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateResult, payload) {
|
|
8021
|
+
function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateResult, payload, selectedModel) {
|
|
7977
8022
|
consola.debug("Non-streaming response from Copilot (direct Anthropic):", JSON.stringify(response).slice(-400));
|
|
7978
8023
|
recordResponse(ctx.historyId, {
|
|
7979
8024
|
success: true,
|
|
@@ -8026,6 +8071,7 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
|
|
|
8026
8071
|
cachedInputTokens: cacheRead,
|
|
8027
8072
|
cacheCreationInputTokens: cacheCreation,
|
|
8028
8073
|
totalInputTokens,
|
|
8074
|
+
tokenPrices: selectedModel?.billing?.token_prices,
|
|
8029
8075
|
stopReason: response.stop_reason ?? void 0
|
|
8030
8076
|
});
|
|
8031
8077
|
let finalResponse = response;
|
|
@@ -8071,7 +8117,7 @@ function echoForwardData(forwardData, eventType, ctx) {
|
|
|
8071
8117
|
* Handle streaming direct Anthropic response (passthrough SSE events)
|
|
8072
8118
|
*/
|
|
8073
8119
|
async function handleDirectAnthropicStreamingResponse(opts) {
|
|
8074
|
-
const { stream, response, anthropicPayload, ctx } = opts;
|
|
8120
|
+
const { stream, response, anthropicPayload, selectedModel, ctx } = opts;
|
|
8075
8121
|
const acc = createAnthropicStreamAccumulator();
|
|
8076
8122
|
const checkRepetition = createStreamRepetitionChecker(`anthropic:${anthropicPayload.model}`);
|
|
8077
8123
|
const serverToolFilter = createServerToolBlockFilter();
|
|
@@ -8110,7 +8156,8 @@ async function handleDirectAnthropicStreamingResponse(opts) {
|
|
|
8110
8156
|
stream: true,
|
|
8111
8157
|
durationMs: Date.now() - ctx.startTime,
|
|
8112
8158
|
stopReason: acc.stopReason || void 0,
|
|
8113
|
-
toolCount: anthropicPayload.tools?.length ?? 0
|
|
8159
|
+
toolCount: anthropicPayload.tools?.length ?? 0,
|
|
8160
|
+
tokenPrices: selectedModel?.billing?.token_prices
|
|
8114
8161
|
}, ctx.timings, {
|
|
8115
8162
|
cachedInputTokens: acc.cacheReadInputTokens,
|
|
8116
8163
|
cacheCreationInputTokens: acc.cacheCreationInputTokens,
|
|
@@ -9041,7 +9088,8 @@ const handleResponses = async (c) => {
|
|
|
9041
9088
|
model: finalResult.model || model,
|
|
9042
9089
|
stream: true,
|
|
9043
9090
|
durationMs: Date.now() - startTime,
|
|
9044
|
-
toolCount: tools.length
|
|
9091
|
+
toolCount: tools.length,
|
|
9092
|
+
tokenPrices: usage ? selectedModel?.billing?.token_prices : void 0
|
|
9045
9093
|
}, ctx.timings, {
|
|
9046
9094
|
cachedInputTokens,
|
|
9047
9095
|
cacheCreationInputTokens: 0,
|
|
@@ -9098,7 +9146,8 @@ const handleResponses = async (c) => {
|
|
|
9098
9146
|
model: result.model || model,
|
|
9099
9147
|
stream: false,
|
|
9100
9148
|
durationMs: Date.now() - startTime,
|
|
9101
|
-
toolCount: tools.length
|
|
9149
|
+
toolCount: tools.length,
|
|
9150
|
+
tokenPrices: usage ? selectedModel?.billing?.token_prices : void 0
|
|
9102
9151
|
}, ctx.timings, {
|
|
9103
9152
|
cachedInputTokens,
|
|
9104
9153
|
cacheCreationInputTokens: 0,
|