-
Notifications
You must be signed in to change notification settings - Fork 0
EN Logging Troubleshooting
Use this page when copilot-relay starts but Claude Code requests fail, route to
the wrong model, or feel slow — and as the reference for what every log line
means.
For what each config key does see Configuration. For why the log format is the way it is see Internals.
-
Confirm the relay is listening:
curl -sS http://127.0.0.1:4142/healthz curl -sS http://127.0.0.1:4142/v1/models
-
Follow today's log:
tail -f ~/.copilot-relay/logs/copilot-relay.$(date +%F).log
-
Check config:
cat ~/.copilot-relay/config.yaml
A 200 from /healthz or /v1/models proves the relay is listening, not
that it can serve a request. Neither contacts Copilot, so a relay whose token
expired an hour ago passes both. The check that actually proves it works is:
copilot-relay status --deep--deep sends a real request through Copilot. It is opt-in because it spends a
few tokens. Exit codes: 0 running and reachable, 1 not running, 2 running
but not usable.
The active file carries the local calendar date and rotates at local midnight:
~/.copilot-relay/logs/copilot-relay.2026-07-31.log
The path is resolved per write, so a relay running across midnight starts the
next day's file on its own — there is no rotation timer to drift. Local rather
than UTC on purpose: logRetentionDays is a human-facing "how many days do I
keep" setting, and a UTC stamp would roll the file over in the middle of the
local afternoon for anyone west of Greenwich.
Files are deleted according to logRetentionDays in
~/.copilot-relay/config.yaml. The default is 3.
Retention counts local calendar days including today, so 3 keeps today,
yesterday, and the day before. Eligibility is decided by the date in the
filename, falling back to mtime for files that carry no stamp. The filename is
preferred because mtime is rewritten by backups, cp, and editors touching a
file, any of which would silently extend or shorten the window.
If you upgraded from before v0.2.3, an undated copilot-relay.log may still be
present. It is the old single log file; it carries no filename date, so it ages
out by mtime once the relay stops appending to it. Nothing writes to it any more
and no manual cleanup is needed.
Rotation is what makes retention work at all. Before it existed, every append
refreshed the one log file's mtime, so it never aged past the cutoff and nothing
was ever deleted; one observed install reached 9.3 GB with logRetentionDays: 3
configured the whole time.
Every log entry — including error entries carrying full request/response context — is written as a single physical line, with object payloads rendered at bounded depth and no pretty-printing.
This matters for searching as much as for size. Multi-line object dumps
previously made the grep recipes below return the first fragment of a payload
rather than the matching entry, and accounted for roughly two thirds of log
volume by bytes.
Payloads are bounded at depth 6, 100 array elements, and 4000 characters per string. A value past those limits is truncated in the log, not dropped.
<iso_timestamp> <level> <message...>
2026-06-06T04:00:00.000Z info request_id=3b241101-e2bb-4255-8caf-4136c566a962 POST /v1/messages -> 200 1234ms
Only three levels are valid:
| Level | Logs |
|---|---|
error |
Startup, preflight, request, token refresh, and upstream failures. |
info |
Errors plus startup status, preflight status, request IDs, upstream lifecycle, and local HTTP status codes. |
debug |
Info plus model routing summaries, Copilot upstream timings, and request payloads. |
Invalid values such as warn, trace, or silent stop startup. File logs
follow the same logLevel filter as console logs.
Start with info. Set logLevel: debug only while you need model routing,
upstream timings, or request payloads — it can log prompts and tool payloads.
The glob spans every retained day:
grep -n "Startup preflight failed" ~/.copilot-relay/logs/copilot-relay.*.log
grep -n "Failed to create" ~/.copilot-relay/logs/copilot-relay.*.log
grep -n "request_id=" ~/.copilot-relay/logs/copilot-relay.*.log
grep -n "Model request" ~/.copilot-relay/logs/copilot-relay.*.log
grep -n "Copilot POST" ~/.copilot-relay/logs/copilot-relay.*.log
grep -n "Failed to refresh Copilot token" ~/.copilot-relay/logs/copilot-relay.*.logTo follow one request end to end across a day boundary:
grep -h "request_id=<id>" ~/.copilot-relay/logs/copilot-relay.*.log | sortAt info, startup logs confirm the active config and preflight:
info Log level: info
info Think effort: xhigh
info Exposed models: gpt-5.6-sol[1m], claude-opus-5
info Running upstream preflight
info Upstream models available: gpt-5.6-sol, claude-opus-5
info Preflight OK: model=gpt-5.6-sol think_effort=xhigh
info Preflight OK: model=claude-opus-5 think_effort=xhigh
info copilot-relay listening on http://127.0.0.1:4142
Every local HTTP request gets a GUID request_id, logged on receipt:
info request_id=3b241101-e2bb-4255-8caf-4136c566a962 request received method=POST path=/v1/messages
The same request_id appears on the final status summary:
info request_id=3b241101-e2bb-4255-8caf-4136c566a962 POST /v1/messages -> 200 1234ms
Fields: method, path, response status, elapsed milliseconds, request ID.
For streaming requests the local HTTP response opens immediately, so the relay also logs end-to-end stream duration:
info request_id=3b241101-e2bb-4255-8caf-4136c566a962 stream completed 1234ms
For non-2xx responses the same line includes a short error message when one is available:
info request_id=3b241101-e2bb-4255-8caf-4136c566a962 POST /v1/messages -> 400 123ms error="Invalid request"
At debug:
debug Model request client=claude requested_model=opus upstream_model=claude-opus-5 requested_think_effort=high requested_thinking=type:enabled,budget:2048 effective_think_effort=xhigh
| Field | Meaning |
|---|---|
client |
claude for Claude Code traffic, generic for internal startup preflight |
requested_model |
model name sent by Claude Code |
upstream_model |
actual Copilot model used |
requested_think_effort |
reasoning_effort sent by Claude Code, or none
|
requested_thinking |
Claude Code thinking config, including budget when present |
effective_think_effort |
value sent upstream after config/routing |
Use this line first when debugging "why did my request use this model/effort?"
At info, every upstream call logs send and return lifecycle lines:
info request_id=3b241101-e2bb-4255-8caf-4136c566a962 send upstream method=POST path=/responses attempt=1 upstream_request_id=5a0f91b1-e0d3-4fd3-81a3-116238688754
info request_id=3b241101-e2bb-4255-8caf-4136c566a962 return from upstream method=POST path=/responses status=200 ms=9200 attempt=1 upstream_request_id=5a0f91b1-e0d3-4fd3-81a3-116238688754
Fields: upstream method, upstream path, upstream response status, elapsed
milliseconds, retry attempt, local request_id, and a per-call
upstream_request_id.
At debug, a compact timing summary is also emitted:
debug request_id=3b241101-e2bb-4255-8caf-4136c566a962 Copilot POST /responses -> 200 9200ms (attempt 1) upstream_request_id=5a0f91b1-e0d3-4fd3-81a3-116238688754
Transient 5xx retries are logged at error with retry context. When Copilot
returns a non-2xx, the error entry keeps the full upstream context on one line:
error Failed to create responses: route=/responses model=gpt-5.6-sol status=400 { request: { ... }, response: { status: 400, headers: { ... }, body: { ... } } }
At debug only:
debug Full Claude request payload { payload: ... }
debug Full request payload { payload: ... }
Use this only when you need the exact request shape.
Token values are never printed. Lifecycle logs carry paths and scheduling only:
info Using cached GitHub token at ~/.copilot-relay/github_token
info Using cached Copilot token at ~/.copilot-relay/copilot_token.json
info Next Copilot token refresh in 1430s
info Refreshed Copilot token
error Failed to refresh Copilot token: ...
info Config reloaded: logLevel=debug thinkEffort=xhigh upstreamTimeoutSeconds=180
Hot reload updates logLevel, logRetentionDays, thinkEffort,
upstreamTimeoutSeconds, copilotBaseUrl, webSearchBackend, gptModel, and
opusModel. Changing host, port, or claudeSetup requires a restart.
grep -n "Startup preflight failed\|Preflight failed\|Required Copilot model\|Invalid logLevel" ~/.copilot-relay/logs/copilot-relay.*.logCommon causes:
-
github_tokenis missing or stale - Copilot cannot mint a bearer token from the cached GitHub token
- configured
gptModeloropusModelis not present in upstream/models - invalid
logLevel -
thinkEffortis rejected by the configured model
Fix auth and retry:
copilot-relay auth
copilot-relay startA fix shipped in a release you installed, but nothing changed. Check which build is actually serving:
copilot-relay statuscopilot-relay 0.3.0
process running (pid 30516, up 1h 58m)
version 0.2.6 — MISMATCH, 0.3.0 is installed
The first line is the CLI you invoked; version is what the running daemon
reports about itself. npm i -g replaces the binary on disk and does not touch
the process already running, so the two disagree until it restarts:
copilot-relay restartUnder a service manager, restart through it rather than through the CLI. On macOS
with KeepAlive, launchd can relaunch the job out from under
copilot-relay restart, so the restart silently does not take — use:
launchctl kickstart -k "gui/$(id -u)/com.d0n9x1n.copilot-relay"and re-check status. See the macOS,
Linux, or Windows page.
version unknown means the daemon predates v0.3.1 and does not report its
version at all; restarting it makes the row meaningful. A mismatch never changes
the exit code — the relay works, it is simply not the build you installed.
At info, local failures look like:
info POST /v1/messages -> 400 123ms error="Invalid request"
The matching error line contains full upstream context in the same log file:
route, model, request payload, response status, response headers, and response
body.
grep -n "Failed to create" ~/.copilot-relay/logs/copilot-relay.*.logIf the response body mentions request shape, check the surrounding request
object in the error entry.
If it mentions auth or model access, refresh the login and then verify with a check that actually reaches Copilot:
copilot-relay auth
copilot-relay restart # if the relay was already running
copilot-relay status --deepDo not use /v1/models to confirm this. It lists the model IDs your
config names and never contacts Copilot, so an expired token or a model your
subscription cannot access passes it unchanged. Only POST /v1/messages
exercises token refresh and real upstream model access — which is what
status --deep sends, and what a real Claude Code request does.
Temporarily set:
logLevel: debugThen:
grep -n "Model request" ~/.copilot-relay/logs/copilot-relay.*.logCheck requested_model (what Claude Code sent) against upstream_model (what
copilot-relay sent to Copilot). Routing is intentionally simple: requests
containing opus use opusModel; everything else uses gptModel.
grep -n "effective_think_effort" ~/.copilot-relay/logs/copilot-relay.*.logCompare effective_think_effort with requested_think_effort and with
thinkEffort in config. thinkEffort in ~/.copilot-relay/config.yaml wins
over client-provided reasoning effort, so startup preflight and real traffic
exercise the same upstream behavior.
Claude WebSearch is executed by the relay through Copilot /responses with
web_search_preview. If search returns an error result:
grep -n "web_search_preview\|Failed to create responses\|Copilot web search" ~/.copilot-relay/logs/copilot-relay.*.logBy default WebSearch uses gptModel. To use a different Copilot Responses model:
webSearchBackend: gpt-5.5Each Claude request has a configurable upstream timeout:
upstreamTimeoutSeconds: 180These mean different things and are easy to confuse:
| Status | Meaning |
|---|---|
499 |
The client disconnected before Copilot finished. |
504 |
The relay's own upstream timeout fired, reported as upstream_timeout. |
info request_id=... POST /v1/messages -> 499 60004ms error="Client request cancelled before Copilot upstream completed."
A 499 at about 60 seconds means the caller closed the local HTTP request long
before the 180 second upstream timeout could fire — so raising
upstreamTimeoutSeconds would change nothing.
At info, local request latency:
info request_id=... POST /v1/messages -> 200 8291ms
For streaming requests the relay opens the local SSE response immediately while
waiting for upstream headers, so use the stream completed line for end-to-end
duration:
info request_id=... stream completed 8291ms
Compare with upstream latency at info:
info request_id=... return from upstream method=POST path=/chat/completions status=200 ms=8287 attempt=1 upstream_request_id=...
Or the compact debug summary:
debug Copilot POST /chat/completions -> 200 8287ms (attempt 1)
If local and upstream timings are close, the delay is upstream/model latency. If local is much larger, inspect stream translation or client-side behavior.
~/.copilot-relay/github_token
~/.copilot-relay/copilot_token.json
github_token is the long-lived login source. copilot_token.json is a
short-lived bearer token cache refreshed before expiry.
grep -n "Failed to refresh Copilot token\|Using cached Copilot token\|Next Copilot token refresh" ~/.copilot-relay/logs/copilot-relay.*.logIf requests suddenly fail with auth errors:
- Check whether
github_tokenexists. - Check whether
copilot_token.jsonexists. - Check for
Failed to refresh Copilot token. - Run
copilot-relay authto refresh the GitHub login token.
copilot-relay start can update ~/.claude/settings.json when
claudeSetup: true.
cat ~/.claude/settings.jsonExpected values:
-
ANTHROPIC_BASE_URLpoints athttp://127.0.0.1:4142 -
ANTHROPIC_AUTH_TOKENexists; it is a dummy value for local relay use
Changing host or port requires restarting the relay, because the listening
socket cannot move during hot reload.
Do not share full debug logs publicly without review — they can include prompts
and tool payloads.
Upstream URLs are redacted in logs: a copilotBaseUrl with a path, query string,
or fragment is written as https://gateway.example[redacted], because a gateway
path can carry a token and the log file is the one thing users are asked to
attach to a bug report.
For bug reports, include:
- exact timestamp
- the
inforequest summary - the related
errorentry if present - whether
logLevel: debugwas enabled - relevant config with private endpoints removed