Skip to content
| Marketplace
Sign in
Visual Studio Code>Visualization>Token PieNew to Visual Studio Code? Get it now.
Token Pie

Token Pie

Token Pie

|
8 installs
| (0) | Free
Local, machine-wide visibility into GitHub Copilot token and AI-credit spend.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

Token Pie

Will I still be able to work this week? That is the question a developer on a metered Copilot plan actually has, and nothing GitHub ships answers it — the billing dashboard is org-scoped, lagging, and silent on what to do differently.

Token Pie runs locally on each developer's machine, reads telemetry Copilot already writes, and answers three things in order:

  1. Will I be throttled? Measured burn rate joined to live remaining quota, projected against the reset date.
  2. What should I change? Findings derived from that machine's own spend, each one carrying the measurement behind it.
  3. Where did it go? By model, by workspace, by whether you or Auto chose the model, and weighted by what each kind of token actually costs.

Nothing leaves the machine. No server, no account, no telemetry of our own.

Not affiliated with GitHub, Microsoft, or the Copilot team. Token Pie is an independent tool that reads telemetry the Copilot extension writes locally. Figures are reported by Copilot; the analysis and any advice are ours.

Getting started

Token Pie shows nothing until Copilot is asked to write its trace database locally, which is off by default. Two commands, once:

  1. Token Pie: Enable Local Trace Collection — writes the two settings below for you and offers to reload.
  2. Token Pie: Check Quota — signs in to GitHub so the panel has a denominator to project against.

Then use Copilot as usual. The status bar fills in on the next refresh, and clicking it opens the report.

If you would rather set it by hand, this is all the first command does:

{
  "github.copilot.chat.otel.dbSpanExporter.enabled": true,
  // 0 (the default) means UNLIMITED prompt text on disk. Read "Content
  // capture" below before enabling this anywhere but your own machine.
  "github.copilot.chat.otel.maxAttributeSizeChars": 1
}

Recording starts now, not retroactively. Copilot keeps no cost history from before the day you switch this on — see What it can and cannot see.

What you get

The panel answers in the order the questions matter, and the status bar carries the headline so you do not have to open it to know.

The verdict, first.

The verdict: percent remaining, the allowance meter, and your pace against a sustainable one

Your measured burn rate against what is actually left on your account, projected at the reset date. Your pace beside sustainable pace is the comparison that decides whether you finish the period — 14.40 against 319 credits a day here.

Where the credits went.

The breakdown: by kind of text, by model with who chose it, and by project

By kind of text, by model, and by project. Chosen by separates the models you picked from the ones Auto picked for you. Prices are solved from your own billed messages rather than read off a price list, so composition is weighted by what each kind of token actually cost you. On claude-sonnet-5 that came out at 0.02 credits per 1k cached against 0.25 fresh — the 12.5× the evidence line quotes — with output at 4× fresh again.

What to change.

Recommendations, each with the measurement behind it and the credits at stake

Findings derived from this machine's own spend. Each carries the credits at stake and the evidence behind it, so you can check the arithmetic rather than take it on faith. A figure marked ≤ is an upper bound rather than a measurement, and never outranks something measured — a speculative saving should not sort above money demonstrably already spent.

What makes the numbers different

Credits come from copilot_chat.copilot_usage_nano_aiu, the cost Copilot itself reports per request — not estimated from public list pricing, and not the chat transcript's copilotCredits.

That last distinction is the important one. copilotCredits records only completed user turns, so retried and cancelled requests you were still billed for never appear. Measured against a live quota delta on one agent session:

source total
agent-traces.db (what Token Pie reads) 13.5631
quota delta (ground truth) 13.6000
chatSessions.copilotCredits 6.1461

A ~55% shortfall on agent-mode work. Trackers built on the session files inherit it. See docs/DECISIONS.md.

Output tokens are not input tokens

Cost is linear in the three token classes, so the per-class rates can be solved for from your own requests. In testing this recovered claude-sonnet-5 exactly — R² = 1.00000, max residual 0.0000 credits:

token class credits per 1k relative to fresh input
fresh input 0.25000 1×
cached input 0.02000 0.08×
output 1.00001 4.00×

That changes what the numbers mean. On the same data, model output was 2% of tokens but 16% of spend, and cached context was 66% of tokens but 12% of spend. Token Pie weights composition by cost wherever it has solved a card, and says plainly when it has not. Nothing is taken from a published price list.

What you see

While it works, the status bar says so rather than going blank:

$(sync~spin) TP | usage          reading local usage
$(sync~spin) TP | history 24/61  reading chat transcripts
$(sync~spin) TP | allowance      asking GitHub what is left

While it is working the item is not clickable — there is no report to open yet. If anything goes wrong the mark becomes $(warning) and clicking opens the log.

The item appears the moment the extension activates, before any file or network access, and the work behind it yields to the editor between each unit. Nothing is ever chained behind the network call.

Status bar, once it settles:

$(pie-chart) TP | 98%             on track
$(pie-chart) TP | 6.2d left       tight, yellow background
$(pie-chart) TP | 2.1d left       will run out first, red background
$(pie-chart) TP | 5.0d to reset   already used up, red background

The icon encodes the verdict, so it changes when your situation does. Click for the panel: the projection and allowance meter, ranked findings with the numbers behind them, cost-weighted composition, and the by-model and by-workspace breakdowns — one screen, no tabs.

Findings state the measurement they rest on, and whether it is exact:

1 request re-read its whole context uncached, costing 5.95 credits more than the same tokens cost warm. claude-sonnet-5: 0.25 per 1k input tokens uncached vs 0.02 cached, over 1 uncached and 6 cached requests — 12.5×. Both rates are from this model's solved rate card, so no output cost is mixed into either side.

A rate card needs six billed requests on one model. Below that, the same cards fall back to pooled averages and say so.

What it can and cannot see

Copilot's trace database is written only from the moment you enable it and holds nothing retroactively — a fresh install starts from zero no matter how long you have used Copilot. Token Pie recovers what it can from VS Code's own chat transcripts for the preceding 30 days. In testing, 38 of 42 transcripts recorded no cost figures at all, so most history is simply not there. Recovered days are marked as a floor and never feed the throttle projection.

Status

Proof of concept, confirmed against a real database — github.copilot-chat 0.62.0, schema_version 1, on VS Code 1.134.0. Ingest, de-duplication, per-model, per-workspace and Auto-vs-manual attribution all verified on live spans. 244 automated checks.

Two caveats:

  • creditsPerNanoAiu is calibrated against one quota delta on one plan. Evidence, not proof. See Calibration.
  • Free, Individual and Business quota shapes are covered by recorded fixtures, including an exhausted Business seat. Enterprise is still unseen.
  • Rate cards have so far been solved for a small number of models. The guards mean a bad fit reports nothing rather than a wrong number, but "reports nothing" is itself untested at scale.

For contributors and agents

  • CLAUDE.md — intent, invariants, and the findings that reversed earlier assumptions. Read before changing anything.
  • docs/ARCHITECTURE.md — module map, data-flow and join diagrams, UI rules, upstream schema.
  • docs/DECISIONS.md — what was believed, what the data showed, what changed.

Content capture — read before enabling org-wide

Out of the box, the trace database stores your prompts, Copilot's responses, system prompts, and tool-call results in plain text. Setting github.copilot.chat.otel.captureContent to false does not stop it, and that is not what the docs lead you to expect.

What the docs say

"When OTel export is disabled, the debug panel automatically captures full prompt and response content. When OTel export is enabled, the github.copilot.chat.otel.captureContent setting controls content capture for both the debug panel and OTLP export."

— Monitor agent usage with OpenTelemetry

What copilot-chat 0.62.0 actually does

captureContent resolves correctly to false and is honoured on the BYOK provider paths (Anthropic, Gemini). But the first-party Copilot chat path writes content attributes with no captureContent check at all:

span.setAttribute(INPUT_MESSAGES, truncate(JSON.stringify([...]), max));
userRequest && span.setAttribute(USER_REQUEST, truncate(userRequest, max));
systemTexts  && span.setAttribute(SYSTEM_INSTRUCTIONS, truncate(...,  max));
span.addEvent("user_message", { content: truncate(userRequest, max) });

Confirmed empirically: with the startup log reporting captureContent=false, gen_ai.input.messages held verbatim prompt text and gen_ai.system_instructions held 1,162 bytes of system prompt. One trivial "hello" produced ~59 KB of stored content across 6 spans. This looks like a bug against documented behaviour and is worth reporting upstream.

The control that does work

Every content attribute is passed through a truncator, and that truncator is driven by maxAttributeSizeChars unconditionally:

function truncate(s, max = 0) {
  if (max <= 0 || s.length <= max) return s;   // 0 = UNLIMITED, not "none"
  ...
}

The default of 0 means unlimited — which is exactly why full prompts land on disk. So:

{
  "github.copilot.chat.otel.dbSpanExporter.enabled": true,
  "github.copilot.chat.otel.maxAttributeSizeChars": 1
}

1 truncates every string attribute to a single character. Reload to apply. It covers most, but not all, of the content — see the next section.

Three call sites the setting cannot reach

maxAttributeSizeChars suppresses user prompts, system instructions, tool arguments and tool results. It does not reach three writers, because they call the truncator without passing a limit — and the parameter defaults to 0, which means unlimited:

span.setAttribute(OUTPUT_MESSAGES,  truncate(JSON.stringify([...])))   // no max
span.setAttribute(TOOL_DEFINITIONS, truncate(toolJson))                // no max
span.addEvent("tools_available",  { toolDefinitions: truncate(json) }) // no max

All three are on the agent-loop path, so they fire once per agent turn. Measured on a real turn:

Survives truncation Size What it is
gen_ai.output.messages 106 B Model response text — actual content
gen_ai.tool.definitions 99.6 KB Copilot's own static tool schemas
tools_available event 103.8 KB The same static schemas again

Worth separating the two problems. The tool schemas are not user data — they are Copilot's built-in tool definitions, identical every turn — but at ~200 KB per agent turn they are the dominant driver of database growth. Only gen_ai.output.messages is a genuine content leak, and it is narrow.

No setting suppresses these. Until it is fixed upstream, Token Pie removes them after the fact — automatically after every ingest, and on demand:

npm run purge          # or: Token Pie: Purge Retained Prompt Content

Auto-purge is on by default (tokenPie.autoPurge.enabled). A draft bug report for the three call sites is in docs/upstream-issue.md.

Why running it constantly is cheap

Measured against a synthetic 400-agent-turn workload:

59.6 MB backlog steady state nothing to do
DELETE attributes 19.0 ms 0.5 ms 0.4 ms
DELETE events 6.0 ms 0.1 ms —
wal_checkpoint 4.0 ms 0.3 ms —
VACUUM 2.0 ms 2.0 ms —
total 34 ms → 0.56 MB 2.9 ms 0.4 ms

The cost is self-limiting: purging continuously keeps the database near 0.5 MB, so VACUUM never has a large file to rewrite. Three further guards keep it out of the way:

  • Probe before writing. A cycle with nothing to delete takes no write lock at all and costs ~0.4 ms.
  • Short busy timeout (tokenPie.autoPurge.busyTimeoutMs, default 250 ms). If Copilot holds the lock we skip and retry next cycle rather than stalling the editor. Verified under a held BEGIN EXCLUSIVE: gives up at the timeout, reports a skip, never hangs. The purge is idempotent, so a missed cycle costs nothing.
  • Deferred VACUUM. Free pages are reclaimed only once ~256 KB has accumulated. The manual command always reclaims.

This is the one place Token Pie writes to a database it does not own, and the exception is deliberate: none of those rows are read by Token Pie, the statements are bounded, and SQLite serialises writers. A locked database is reported rather than retried into contention.

Deleting rows is not sufficient on its own. In WAL mode the superseded pages — including the content just deleted — stay in the -wal file until a checkpoint folds them in, so the purge checkpoints, vacuums, then checkpoints again. Verified on a real database: 210 KB of rows removed, -wal truncated from 800 KB to 0, and no residual plaintext for apply_patch, "role":"assistant" or system-prompt text anywhere in the file.

Nothing Token Pie needs is affected. Token counts and copilot_usage_nano_aiu are written as numeric attributes and never passed through the truncator, and models, session ids and operations are typed columns on spans. Cost and usage reporting are unchanged.

The trade-off: the Chat Debug View's prompt inspection becomes useless, since it reads the same truncated attributes. If a developer needs to debug a prompt, they raise the value temporarily.

Deployment note: like dbSpanExporter.enabled, maxAttributeSizeChars has no enterprise-policy precedence — it reads from the user setting or the COPILOT_OTEL_MAX_ATTRIBUTE_SIZE_CHARS environment variable. Ship it via the environment, settings sync, or let Token Pie write it (its setup command does, and Diagnostics flags a value of 0).

Setup

1. Enable local trace collection

Add exactly this to your user settings.json:

{
  "github.copilot.chat.otel.dbSpanExporter.enabled": true,
  // 0 (the default) means UNLIMITED. See "Content capture" above.
  "github.copilot.chat.otel.maxAttributeSizeChars": 1
}

Then reload the window. Or run Token Pie: Enable Local Trace Collection, which writes the setting and checks for the conflicts below.

Do not also set github.copilot.chat.otel.enabled.

Copilot resolves a local database-only mode as dbSpanExporter && !enabledExplicitly && !fileExporterPath && exporterType !== "console". Setting otel.enabled flips enabledExplicitly, which builds a real OTLP HTTP exporter pointed at localhost:4318. The database is still written — the SQLite processor is attached independently — but Copilot also retries every span against a port nobody is listening on, for no benefit. The setting description reads as though you should set both. You should not.

Leave github.copilot.chat.otel.captureContent at its default of false — but do not rely on it alone. It does not gate the first-party chat path; see Content capture.

2. Generate some data

Use Copilot Chat normally for a few requests. Spans are written as you go.

3. Confirm the schema

npm install
npm run probe

This prints the real table shape, every attribute key it finds, how many spans carry a billed-cost attribute, and which attribute identifies the workspace. Use the output to confirm or correct the candidate lists in src/schema.ts and src/ingest.ts.

Troubleshooting: the log line that looks wrong

On startup Copilot logs something like:

[OTel] Instrumentation enabled — exporter=otlp-http endpoint=http://localhost:4318/ captureContent=false

This is cosmetic and expected, even in database-only mode. The message interpolates config.exporterType and config.otlpEndpoint unconditionally, regardless of which exporter was actually constructed. When database-only mode wins, the span exporter Copilot builds is a no-op:

class { export(spans, done) { done({ code: 0 }); } ... }

Nothing is sent to localhost:4318. The SQLite processor is attached separately and does the real work. Seeing that line means instrumentation came up — which is what you want.

The database is created lazily on the first span flush, so it will not exist until you send a Copilot Chat request. The setting also requires a reload, and only windows started after the change pick it up — restart VS Code rather than reloading a single window.

npm run probe distinguishes these cases and tells you which one you are in.

4. Run it

npm run compile

Then press F5, or package with npx vsce package.

Commands

Command Does
Token Pie: Enable Local Trace Collection Writes the setting, warns about conflicts, offers reload
Token Pie: Show Usage Report The panel: throttle projection, allowance meter, ranked findings, composition, by-model and by-workspace
Token Pie: Refresh Now Forces an ingest
Token Pie: Diagnostics Settings state, databases found, detected schema, ingest counts
Token Pie: Check Quota Reads copilot_internal/user for your live entitlement
Token Pie: Purge Retained Prompt Content Deletes model output and tool schemas the truncation setting cannot reach, and reclaims the space

The status bar shows the throttle verdict across every window and workspace — days of headroom when a rate can be projected, percentage remaining when it cannot. Click for the panel.

How it works

agent-traces.db  ──read-only──▶  ingest  ──▶  rollup.json  ──▶  projection  ──▶  panel
(written by Copilot)             schema        (day × model ×     advice        status bar
                                 detection      workspace ×          ▲
                                                operation ×          │
                                                selection)     copilot_internal/user

Full diagrams in docs/ARCHITECTURE.md.

agent-traces.db lives in the Copilot extension's globalStorage, not workspaceStorage:

~/Library/Application Support/Code/User/globalStorage/github.copilot-chat/agent-traces.db

That is the reason this is useful. Global storage is per profile, so every window, folder and repository on the machine writes to one database. A developer with eight windows open across eight repos produces one unified stream, and spans still carry workspace attributes — so you get per-repo breakdown without per-repo fragmentation.

Token Pie enumerates every install channel (Stable, Insiders, forks) and every profile, plus the tmpdir fallback the exporter uses when globalStorage is unavailable.

The actual schema

spans carries first-class typed columns — no JSON parsing needed:

span_id  trace_id  parent_span_id  name  start_time_ms  end_time_ms
status_code  status_message  operation_name  provider_name  agent_name
conversation_id  request_model  response_model  input_tokens  output_tokens
cached_tokens  reasoning_tokens  tool_name  tool_call_id  tool_type
chat_session_id  turn_index  ttft_ms

Only the long tail lives in span_attributes (span_id, key, value). Copilot Lens reads the columns directly and dips into that table for exactly one thing: copilot_chat.copilot_usage_nano_aiu, the billed cost.

Design notes

Only chat spans are counted. This is load-bearing. An agent turn also emits an invoke_agent span that repeats its child chat span's token counts verbatim — the same 18,183 input tokens on both — while carrying no cost attribute. Counting every span with tokens would double every agent turn. execute_tool spans carry no tokens at all.

Workspace comes from a session bridge, not an attribute. No span records the workspace. Spans carry chat_session_id, and VS Code stores chat sessions per workspace, so:

workspaceStorage/<hash>/chatSessions/<session-id>.jsonl
workspaceStorage/<hash>/workspace.json  ->  { "folder": "file:///path/to/repo" }

Sessions that cannot be resolved report unknown rather than a guess. Copilot's auxiliary calls have no session id, so they legitimately land there.

agent_name is the most interesting dimension. It separates the request you made (panel/editAgent) from the calls Copilot makes on your behalf (title, progressMessages). On real data those auxiliary calls run on gpt-4o-mini and report zero cost — which is exactly the kind of thing a report should show rather than leave people guessing about.

Read-only, always. Every open VS Code window runs its own extension host writing to that database. Token Pie never opens it for writing.

Rollup, not re-query. Ingest is incremental against a watermark, with a five-minute overlap window and timestamp-keyed span-id de-duplication. Results collapse to (day, model, workspace, agent) in our own store, which bounds our storage regardless of how large the upstream database grows.

BigInt everywhere. start_time_ms is milliseconds and fits in a double, but token counts and nano-AIU are INTEGER, and node:sqlite throws outright on anything past Number.MAX_SAFE_INTEGER rather than truncating. Every statement opts into setReadBigInts(true).

Schema verified, not assumed. Required columns are checked at runtime and schema_version is recorded. Missing optional columns degrade to zero or unknown; missing required ones fail loudly rather than reporting confidently wrong numbers.

Coverage and limits

Know these before promising anything org-wide.

  • Per profile. Multiple VS Code profiles produce multiple databases. Handled — but worth knowing they are separate.
  • Per install channel. Stable, Insiders and forks have separate roots.
  • Remote work is the real gap. Under Remote-SSH, devcontainers or Codespaces, the Copilot extension host runs remotely, so the database is written on the remote machine. Install Token Pie there to capture it.
  • VS Code only. Copilot CLI, JetBrains, Xcode and github.com chat are invisible.
  • Chat and agent operations only. Inline completions and next-edit suggestions are not in this stream. They are also not credit-billed on paid plans, so cost coverage stays close to complete even though usage coverage does not.

Stability

Depend freely on gen_ai.* attributes — that is the OTel GenAI semantic convention and it is stable.

Treat these as versioned internal surface that can move in any monthly Copilot release, and feature-detect accordingly:

  • copilot_chat.copilot_usage_nano_aiu — the only source of real billed cost
  • the spans table shape and schema_version
  • the globalStorage path
  • the chat_session_id → workspaceStorage bridge

Verified against github.copilot-chat 0.62.0, schema_version 1, on VS Code 1.134.0. When the cost attribute goes missing the report says so rather than quietly reporting zero, and token counts still work.

Calibration

tokenPie.creditsPerNanoAiu defaults to 1e-9, which assumes nano-AIU means 10⁻⁹ AI Credits. This has not been verified against a real bill. On observed data, one 18,183-token request on gpt-5.6-luna reported 456,240,000 nano-AIU — 0.456 credits, or about $0.0046, under that assumption. Compare a day's reported total against your GitHub billing dashboard and adjust before treating absolute figures as authoritative. Relative figures — which model, which repo, which day — are unaffected by the constant.

Development

npm run selftest   # end-to-end against a synthetic database
npm run probe      # dump the real schema
npm run verify     # prove content is suppressed AND usage data survives
npm run purge      # delete the content truncation cannot reach
npm run watch

npm run verify is the one to hand a security reviewer. It asserts both halves at once — that no content-bearing attribute exceeds a single character, and that token counts, models, session ids and billed cost are all still present — and exits non-zero if either fails.

scripts/selftest.mjs builds a fixture database and asserts schema detection, tool-span exclusion, de-duplication, incremental ingest, persistence across restart, graceful degradation when the cost attribute is absent, and report rendering.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
© 2026 Microsoft