Skip to the content.

API Reference

Home · Getting Started · Configuration · API Reference · Claude Code & Codex · Headroom Plugin


All endpoints listen on http://<address>:<port> (default http://127.0.0.1:8314). By default no API key is required by the proxy itself — authentication to GitHub Copilot is handled internally. You can optionally require a key on the LLM endpoints; see Authentication below.

Endpoints

Method & path Description
POST /v1/chat/completions OpenAI chat completions (also /chat/completions)
POST /v1/responses OpenAI Responses API for Codex (also /responses)
POST /v1/messages Anthropic Messages API
POST /v1/messages/count_tokens Anthropic token counting (real BPE, local estimate fallback)
POST /v1beta/models/{model}:generateContent Gemini generate content
POST /v1beta/models/{model}:streamGenerateContent Gemini streaming (SSE)
POST /v1beta/models/{model}:countTokens Gemini token counting
POST /v1/embeddings Embeddings (also /embeddings)
GET /v1/models List available models (also /models, /api/models)
GET /v1/models/{model} Retrieve a single model (also /models/{model})
GET /v1/models/full/ Raw upstream model catalog with capabilities
GET /usage Copilot plan and quota usage
GET /health Liveness/readiness probe
GET / Web analytics dashboard — overview
GET /metrics/dashboard Metrics dashboard UI
GET /metrics OpenMetrics exposition endpoint
GET /requests Request browser
GET /app.css Stylesheet shared by the three dashboard pages
GET /api/stats Dashboard statistics (JSON)
GET /api/cache Prompt-cache statistics, overall and per model
GET /api/requests Recent requests (JSON)
GET /api/audit Filtered audit records
GET /api/audit/summary Aggregated audit summary
POST /api/config/reload Reload config.yaml without restart
POST /api/config/debug Turn request/response body capture on or off
GET /openapi.json OpenAPI v3 specification of the LLM endpoints
GET /v1/responses (Upgrade) Responses API over WebSocket

Responses over WebSocket

Several models advertise ws:/responses in supported_endpoints alongside /responses. GET /v1/responses (or /responses) with a WebSocket upgrade exposes that transport.

The protocol is the streaming Responses API with a different carrier: the same response.* event vocabulary, one event per text frame, so a client already written against the SSE stream needs no new parsing. Send one frame to start a turn:

{"type": "response.create", "model": "gpt-5.5", "input": "...", "stream": true}

The frame is flat — model sits at the top level beside the request fields, not nested under a response object. Omitting type or nesting the body is rejected.

A model that does not advertise the transport is refused with an error frame naming the alternative rather than left waiting on the socket:

{"type": "error", "error": {"code": "unsupported_api_for_model",
  "message": "Model 'claude-opus-4.6' does not support ws:/responses. Use POST /v1/responses instead."}}

WebSocket turns are recorded like any other request, under the endpoint ws:/responses.

Read-only dashboard endpoints are reachable without an API key so local monitoring keeps working. The /api/config/ routes are not: they mutate the running process, and one of them turns on body capture, which writes whatever the client sent — credentials included — into the request log.

Statistics and failed attempts

GET /api/stats counts requests that produced an answer. Attempts that did not — a non-2xx status, or a failure_kind on an otherwise successful one — are reported separately as failed_requests and excluded from the rest, so a burst of rejected calls cannot dilute a rate computed over requests that consumed nothing. Token and billing totals count either way: a stream cut off partway consumed what it consumed.

Prompt cache statistics

GET /api/cache reports where input tokens came from. The hit rate is the early warning for a broken prompt prefix: on an agent workload it should sit high and stable, and a sudden drop means the prompt stopped matching and every turn is paying full input price again.

{
  "totals": {
    "input_tokens": 9397,
    "cache_read_tokens": 4682,
    "cache_creation_tokens": 4682,
    "fresh_tokens": 33,
    "hit_rate": 0.498,
    "request_count": 4
  },
  "dispositions": { "served_from_cache": 1, "wrote_to_cache": 1, "no_cache": 2 },
  "sampled_requests": 4,
  "by_model": [
    {
      "model": "gemini-3.5-flash",
      "requests": 4,
      "input_tokens": 9397,
      "cache_read_tokens": 4682,
      "cache_creation_tokens": 0,
      "fresh_tokens": 33,
      "hit_rate": 0.498,
      "saved_nano_aiu": 547830000
    }
  ]
}

totals are the all-time running counters. by_model is derived from the retained ring buffer, so it describes the most recent sampled_requests calls rather than every one ever served.

What a zero means

Copilot states its own per-token rates on each response, in copilot_usage.token_details. saved_nano_aiu is computed from those rates rather than from a price list, so a model Copilot includes at no charge contributes nothing instead of an imagined discount. Positive means the cache paid for itself; negative is the normal shape of a turn that populated a cache it has not read back yet. null means no response for this model reported the rates to compute one from — distinct from 0, which is a real figure: nothing was cached, so nothing was saved.

cache_creation_tokens is only ever non-zero on the Anthropic surface with an explicit cache_control marker. Every other surface caches implicitly: the first call reads nothing, the next reads the whole prefix back, and no write is billed. Measured across twelve calls with a 27k-token prefix, exactly one reported a write, on /v1/messages. The dashboard hides the column outright when nothing wrote.

Cache lifetime

A cache_control breakpoint with no ttl gets the five-minute tier. That is short enough to be self-defeating on long turns: the entry is written during prefill, so a turn that itself runs longer than five minutes has already outlived its own cache by the time it finishes, and the next turn pays a full cold prefill of the whole conversation.

Setting extend_cache_ttl: true promotes breakpoints that carry no explicit ttl to the one-hour tier. An explicit ttl is always left as the client sent it. Copilot honours both tiers and accounts for them separately, in cache_creation.ephemeral_5m_input_tokens and ephemeral_1h_input_tokens.

It is off by default because it is not free. Extended writes bill at a higher rate than five-minute ones while reads cost the same, so the premium is charged on every write and the saving only lands on an expiry that would otherwise have happened. On a conversation that does many small incremental writes between rare expiries it costs more than it saves; it pays off when turns routinely run past five minutes.

A model with no cache activity at all is usually not a fault either: Copilot needs a minimum cacheable prefix before any of the prompt is eligible. claude-haiku-4.5 was observed caching a 6902-token prefix but not a 4082-token one.

Body capture

POST /api/config/debug with {"debug": true} or {"debug": false} turns request/response body capture on or off for the running process. The flag is read live on every request, so it applies from the next call — no restart, and no need to have predicted in advance that you would want the bodies.

It is deliberately not written back to config.yaml. Capture puts prompts, tool output and any credentials they carry into memory and the log, so it lapses on restart rather than staying on because someone forgot. GET /health reports the current value as debug.

Streaming (SSE) is supported on the chat, responses, and messages endpoints by setting "stream": true in the request body. The Gemini surface streams via the dedicated :streamGenerateContent action.

Health check

GET /health answers without contacting the upstream, so it is cheap enough for a service supervisor or container probe to poll frequently. It is never guarded by the optional API key.

curl http://127.0.0.1:8314/health
{
  "status": "ok",
  "ready": true,
  "version": "1.4.5",
  "uptime_seconds": 128,
  "copilot_token": { "present": true, "expires_in_seconds": 1487 },
  "models_loaded": 77,
  "requests_served": 42,
  "auth_required": false
}

ready is true once a Copilot token has been obtained and the model catalog has loaded. A degraded proxy still answers 200 with ready: false so probes can distinguish “process alive” from “able to serve traffic”. Add ?strict=true 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 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.

{
  "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

GET /v1/models/{model} returns a single catalog entry in the OpenAI shape, including the raw capabilities and supported_endpoints reported upstream. Model aliases from model_mappings are resolved, so /v1/models/opus returns the mapped Copilot model. Unknown ids return 404 with an OpenAI-style error body.

curl http://127.0.0.1:8314/v1/models/claude-opus-5

Token counting

POST /v1/messages/count_tokens forwards to the upstream Anthropic count_tokens endpoint for models that expose the native /v1/messages surface, returning exact counts. For every other model (and whenever the upstream call fails) the proxy falls back to a local tiktoken estimate using the tokenizer advertised in the model catalog. Estimated responses are marked:

{ "input_tokens": 812, "estimated": true }

OpenAI SDK

from openai import OpenAI

client = OpenAI(base_url="http://127.0.0.1:8314/v1", api_key="not-needed")
resp = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Hello!"}],
)
print(resp.choices[0].message.content)

Anthropic SDK

import anthropic

client = anthropic.Anthropic(base_url="http://127.0.0.1:8314", api_key="not-needed")
msg = client.messages.create(
    model="claude-sonnet-4",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello!"}],
)
print(msg.content)

The proxy serves Anthropic requests directly from Copilot’s native /v1/messages endpoint when the model supports it, and otherwise translates them through chat completions transparently.

Gemini

curl "http://127.0.0.1:8314/v1beta/models/gemini-2.5-pro:generateContent" \
  -H "Content-Type: application/json" \
  -d '{"contents": [{"role": "user", "parts": [{"text": "Hello!"}]}]}'

The model is taken from the URL path and translated per your mappings. Gemini requests are translated through chat completions, so any Copilot model works. Streaming uses the :streamGenerateContent action and emits data: SSE lines.

Authentication

By default the proxy accepts all local requests. Set api_key in config.yaml (or GHC_PROXY_API_KEY) to require a key on the LLM endpoints. The key is accepted from any of the standard provider headers and compared in constant time:

curl http://127.0.0.1:8314/v1/messages          -H "x-api-key: KEY" ...
curl http://127.0.0.1:8314/v1/chat/completions  -H "Authorization: Bearer KEY" ...
curl "http://127.0.0.1:8314/v1beta/models/gemini-2.5-pro:generateContent" -H "x-goog-api-key: KEY" ...

The dashboard, metrics, and static pages stay open so local monitoring works without a key. Unauthenticated requests to protected endpoints return 401.

cURL

# Chat completions
curl http://127.0.0.1:8314/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "Hello!"}]}'

# List models
curl http://127.0.0.1:8314/v1/models

# Usage / quota
curl http://127.0.0.1:8314/usage

Model discovery

GET /v1/models returns the OpenAI-style list. Codex appends a client_version query parameter; when present, the same endpoint returns its native { "models": [...] } catalog with active and maximum context windows derived from Copilot. This lets Codex size and compact each selected model without a static client-side override.

For full upstream capability data — context-window limits, supported endpoints, vision, tokenizer — use:

curl http://127.0.0.1:8314/v1/models/full/

This is the authoritative source for which models support a 1M-token context window (those advertising max_context_window_tokens greater than 200,000).

Audit API

GET /api/audit returns recent request records with their extracted audit fields. All filters are optional and combine with AND:

Parameter Effect
endpoint Substring match on the endpoint path
status Exact HTTP status code
tool_name Keeps records whose request offered a matching tool
agent true/false — agent- vs user-initiated requests
model Substring match on the requested or translated model
page, per_page Pagination (per_page is clamped to 500)

GET /api/audit/summary aggregates the same records into top tools, stop-reason counts, estimated cost, and prompt-cache hit rate.

Notable behaviors