Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## [1.4.5] - 2026-08-18

### Fixed
- **The dashboard now shows Copilot quota before the first proxy request.** It
renders the union of the response-header snapshot and `GET /usage`, so a new
or idle process no longer hides an account quota that is already available.
- **Enterprise token-billed quota now follows the GitHub account meter.** For
those seats the upstream fields are counterintuitive: `credits_used` carries
the available AI-unit balance while `remaining` and `percent_remaining`
describe the consumed side. The dashboard now reports the correct amount and
percentage left without changing the interpretation for ordinary plans.

### Documentation
- Documented the `/usage` response fields, their units, and the enterprise
token-billed field direction.

## [1.4.4] - 2026-08-15

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ghc-proxy"
version = "1.4.4"
version = "1.4.5"
edition = "2021"
description = "GitHub Copilot API Proxy - Provides OpenAI and Anthropic compatible endpoints via GitHub Copilot (Rust port of ghc-tunnel)"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ upstream_read_timeout_seconds: 900 # max silence from upstream; 0 disables
| `POST /v1beta/models/{model}:countTokens` | Gemini token counting |
| `POST /v1/embeddings` | Embeddings (also `/embeddings`) |
| `GET /v1/models/full/` | Raw upstream model catalog with capabilities |
| `GET /usage` | Copilot plan and quota usage (also `check-usage`) |
| `GET /usage` | Copilot plan and quota usage in account billing units (also `check-usage`; see [API Reference](docs/api.md#usage-and-quota)) |
| `GET /health` | Liveness/readiness probe (`?strict=true` for 503 when not ready) |
| `GET /openapi.json` | OpenAPI v3 specification |
| `GET /` | Web dashboard — overview |
Expand Down
48 changes: 44 additions & 4 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ curl http://127.0.0.1:8314/health
{
"status": "ok",
"ready": true,
"version": "1.4.4",
"version": "1.4.5",
"uptime_seconds": 128,
"copilot_token": { "present": true, "expires_in_seconds": 1487 },
"models_loaded": 77,
Expand All @@ -218,9 +218,49 @@ to get `503 Service Unavailable` instead when the proxy is not ready.

The `quota` object holds the most recent per-SKU allowance reported by the
upstream. Copilot attaches it to every response, so it is current without any
extra API call — but it stays empty until the first request has been proxied.
The same figures are exported on `/metrics` as `ghc_proxy_quota_*` gauges
labelled by `sku`.
extra API call, but the health payload stays empty until the first request has
been proxied. The overview dashboard also loads `/usage`, so its quota panel
works immediately on a new or idle process. The same figures are exported on
`/metrics` as `ghc_proxy_quota_*` gauges labelled by `sku`.

## Usage and quota

`GET /usage` contacts Copilot's account endpoint and returns the plan, account,
reset date, and one entry per quota SKU. It does not depend on requests recorded
by this proxy.

```json
{
"plan": "enterprise",
"login": "example",
"token_based_billing": true,
"quota_reset_date": "2026-09-01T00:00:00Z",
"quotas": {
"premium_interactions": {
"unlimited": false,
"entitlement": 10000000,
"remaining": 9229138,
"percent_remaining": 92.2,
"credits_used": 770861
}
}
}
```

For a token-billed `premium_interactions` SKU, `entitlement`, `remaining`, and
`credits_used` are measured in **AI units**. An AI unit is an account billing
unit, not a token, request, premium interaction, or currency amount; the model,
token mix, cache use, and billing rates determine how many units a call costs.
`percent_remaining` is a percentage rather than an AI-unit count.

Enterprise token-billed seats expose counterintuitive upstream names:
`credits_used` is the available balance, while `remaining` and
`percent_remaining` describe the consumed side. The example therefore means
770,861 of 10,000,000 AI units remain (about 7.7%) and 9,229,138 have been
consumed. The dashboard corrects this direction only for
`enterprise + token_based_billing`; ordinary plans retain the upstream field
direction. `raw` preserves the complete upstream payload for callers that need
fields omitted from the compact `quotas` object.

## Retrieve a model

Expand Down
39 changes: 26 additions & 13 deletions public/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -294,12 +294,15 @@ <h2>Prompt cache</h2>
}
}

// Quota arrives on every upstream response, so it is empty only until the
// first request has been proxied. Hide the panel rather than render zeros,
// which would read as "exhausted".
// Header snapshots are unavailable until the first proxied request, while
// /usage can provide the account's quota immediately. Render the union so
// an idle proxy still shows the Copilot quota instead of an empty panel.
function renderQuota(quota) {
lastQuota = quota;
const skus = Object.keys(quota);
const skus = [...new Set([
...Object.keys(quota),
...Object.keys(quotaExact || {})
])];
const panel = document.getElementById('quotaPanel');
if (skus.length === 0) { panel.hidden = true; return; }
panel.hidden = false;
Expand All @@ -310,28 +313,38 @@ <h2>Prompt cache</h2>
// that has been spent against reads as untouched.
const showPct = p => (Number.isInteger(p) ? p.toFixed(0) : p.toFixed(1)) + '%';

let reset = null;
let reset = quotaAccount?.quota_reset_date || null;
const account = quotaAccount || {};
// Enterprise token-billed seats report the consumed side in `remaining`
// and the available side in `credits_used`, despite those field names.
const reversed = account.plan === 'enterprise' && account.token_based_billing;
const rows = skus.map(name => {
const q = quota[name];
const x = (quotaExact || {})[name];
const q = { ...(x || {}), ...(quota[name] || {}) };
if (q.reset_date && !reset) reset = q.reset_date;

const x = (quotaExact || {})[name];
const exact = !q.unlimited && x && x.entitlement > 0 && x.remaining != null;
const pct = exact ? clamp(x.remaining / x.entitlement * 100)
: clamp(q.percent_remaining);
const cls = pct <= 10 ? 'err' : pct <= 25 ? 'warn' : '';
// The upstream reports consumption itself; `entitlement - remaining`
// is only a stand-in for a plan that does not.
const used = exact
? (x.credits_used != null ? x.credits_used : x.entitlement - x.remaining)
? (reversed ? x.remaining
: (x.credits_used != null ? x.credits_used : x.entitlement - x.remaining))
: null;
const remaining = exact
? (reversed
? (x.credits_used != null ? x.credits_used : x.entitlement - x.remaining)
: x.remaining)
: null;
const pct = exact ? clamp(remaining / x.entitlement * 100)
: clamp(reversed ? 100 - q.percent_remaining : q.percent_remaining);
const cls = pct <= 10 ? 'err' : pct <= 25 ? 'warn' : '';
const right = q.unlimited
? '<b>unlimited</b>'
// What is left and what has gone, in the units the entitlement is
// denominated in — on a token-billed plan those are the same AI
// units the Consumption panel above reports, so the two read together.
: (exact
? `<b>${compact(x.remaining)}</b> of ${compact(x.entitlement)} left` +
? `<b>${compact(remaining)}</b> of ${compact(x.entitlement)} left` +
` · ${compact(used)} used`
: `<b>${showPct(pct)}</b> of ${compact(q.entitlement)}`) +
(q.overage > 0 ? ` · ${q.overage} over` : '');
Expand All @@ -358,7 +371,7 @@ <h2>Prompt cache</h2>
// account beside it is unreadable the moment more than one token is in
// play, and `token-billed` is what says the entitlement above counts AI
// units rather than interactions.
const a = quotaAccount || {};
const a = account;
const d = reset ? new Date(reset) : null;
const parts = [];
if (a.login) parts.push(a.login);
Expand Down