EN Configuration - D0n9X1n/copilot-relay GitHub Wiki
copilot-relay stores runtime configuration under:
~/.copilot-relay/config.yaml
The file is created from the package template on first start. On subsequent loads, the relay preserves your existing text, comments, key order, unknown flat scalar keys, and explicit values; it appends defaults only for absent keys. Writes use a checked file snapshot and atomic replacement of the resolved target, so a symlink remains a symlink and detected concurrent edits are not overwritten. Upgrades do not migrate saved values to new defaults.
Only flat scalar YAML is supported. Invalid known values, duplicate keys (including
aliases), and unsupported syntax fail before write-back; the file is left for you
to correct. Empty webSearchBackend is valid, not an instruction to erase other
settings. Hot reload is read-only and accepts only a stable, valid document
containing every materialized key; a partial or empty editor save keeps the last
valid runtime settings. Restore the missing keys rather than deleting one to
request its default. See Internals for the writer's guarantees.
To see every key after defaults are resolved, and which of them need a restart,
run copilot-relay status. These are the values on disk, not proof the daemon
has loaded them. With broken config, status exits 2 with a safe diagnostic;
stop can still attempt recovery using verified process identities. See
Logs and troubleshooting.
host: 127.0.0.1
port: 4142
apiKey:
copilotBaseUrl: https://api.githubcopilot.com
upstreamProxy:
claudeSetup: true
logLevel: info
logRetentionDays: 3
thinkEffort: max
upstreamTimeoutSeconds: 180
webSearchBackend:
claudeUpstreamApi: chat-completions
gptModel: gpt-6-astra
opusModel: claude-opus-5.5| Key | Purpose |
|---|---|
host |
Local bind host for the Claude-compatible HTTP server. Keep 127.0.0.1 for local-only use; set apiKey before binding beyond loopback. |
port |
Local port. Default: 4142. |
apiKey |
Optional key that clients must send as x-api-key or Authorization: Bearer. Empty, the default, disables the check. Hot-reloads. See Require a client key. |
copilotBaseUrl |
GitHub Copilot API base URL. Must be an absolute http:// or https:// URL, and may not contain credentials. Keep the default unless you know you need a tenant-specific endpoint. See copilotBaseUrl rules. |
upstreamProxy |
Outbound proxy for Copilot and GitHub calls, sign-in included. Empty (the default) connects directly; env uses HTTPS_PROXY, HTTP_PROXY and NO_PROXY; otherwise an absolute http:// or https:// proxy URL. Requires restart. See upstreamProxy rules. |
claudeSetup |
When true, start updates ~/.claude/settings.json with the local relay endpoint. |
logLevel |
One of error, info, debug. debug automatically captures full observed request/response bodies as well as bounded logs; see Logs and troubleshooting before enabling it. Any other value fails startup. |
logRetentionDays |
Local calendar days to retain normal relay logs and debug captures, including today; positive integer, default 3. Active/unknown captures are protected; see Logs and troubleshooting for cleanup rules. |
thinkEffort |
Fallback reasoning effort when the request omits it: low, medium, high, xhigh, max. |
upstreamTimeoutSeconds |
Max seconds one Claude request can wait for upstream Copilot calls. Default: 180; 0 disables the relay deadline. |
webSearchBackend |
Optional Copilot Responses model for bridge-managed WebSearch. Empty uses gptModel. |
claudeUpstreamApi |
Claude upstream protocol: chat-completions (default), auto, or messages. Does not change non-Claude routing. |
gptModel |
Upstream model for non-Opus requests. |
opusModel |
Upstream model for requested models containing opus. |
Model selection still happens first (opusModel or gptModel). For a selected
upstream ID starting with claude-, claudeUpstreamApi then chooses:
| Value | Behavior |
|---|---|
chat-completions |
Default. Keep the translated Copilot /chat/completions path. |
auto |
Use native /v1/messages only when the current provider's cached model catalog advertises that endpoint; otherwise use the translated path. |
messages |
Force native /v1/messages for Claude models, even without an advertised capability. Upstream rejection remains an error. |
Non-Claude models use catalog-driven chat/Responses selection in every mode.
The current provider's supported_endpoints chooses /chat/completions or
/responses, including newly advertised model IDs. If both are advertised, the
relay preserves its existing preference: Responses for GPT-5.5, GPT-5.6 and GPT-6
Astra families, Chat for other models. Catalog order cannot change that choice.
Missing or malformed endpoint metadata keeps this legacy preference, marked
unverified; an explicit empty list means no advertised route. Other protocols
and non-chat types remain unsupported, not evidence that an account lacks access.
Claude chat-completions pins Chat: if the catalog excludes it, the request has a
protocol-policy conflict rather than silently switching. auto without native
support uses the catalog-driven translated choice. messages remains an explicit
native override even when endpoint metadata disagrees. Deep probes, startup
preflight and normal configured-target requests share these rules. A non-OK
unsupported_api_for_model can retry Chat through Responses once only when the
catalog (or missing metadata) and protocol policy permit it; other errors,
refusals and partially delivered streams never trigger an API switch.
Native transport preserves signed thinking, cache markers, in-place system roles
and controls; it does not silently retry a refusal or failure through another API.
chat-completions remains the shipped default. A bounded 2026-09-30 matched
synthetic trial at low effort measured warm token-weighted cache reads of
99.8384% for chat and 99.7980% for native (about 0.04 percentage points apart).
A second, counterbalanced native-first trial stopped on its cold-turn refusal;
the completed trial does not establish broad non-regression or billing-cost
equivalence. These were isolated checks, not validation of the current production
relay; port 4142 was untouched. Methods and usage counts are in
Internals. Reasoning flattening is still not proven to have
caused the historical refusal.
Native bridge-managed WebSearch supports automatic selection only. With WebSearch
advertised, any or an explicitly forced search returns HTTP 400 JSON before
retrieval/SSE; only one search is allowed per turn. Its deterministic relay marker restores the original signed provider
history on continuation. Old chat-bridge history and native-bridge history are
not transparently interchangeable: keep an existing search conversation on its
original route, or begin a new conversation after switching. See
Internals for protocol boundaries, not just model availability.
Model availability depends on your Copilot account, organization policy, and configured gateway. To fetch the complete catalog advertised by your relay's upstream, run:
copilot-relay modelsThe command makes a fresh authenticated GET /models at the configured
copilotBaseUrl, reusing the relay's cached credentials, token refresh, and
upstreamTimeoutSeconds. Without usable cached credentials, the existing device
login flow may ask you to authenticate. All valid upstream IDs are printed in
sorted order, with duplicates removed; relay aliases such as [1m] are not added.
Models outside the configured gptModel and opusModel mappings are included.
Each row starts with the exact ID that gptModel or opusModel takes, followed by
the catalog's display name. An ID that request admission would refuse is marked
cannot be gptModel or opusModel with the reason, such as not a chat model; the
decision is the same selectCopilotEndpoint that real requests use. Terminal
control characters are stripped and sensitive gateway URL tails are redacted for
safe display; ordinary model IDs are unchanged. After the list, the command prints
the config file path and the current gptModel and opusModel lines with the
requests each serves. It warns when a configured ID is not advertised or cannot be
used, because the startup check rejects it.
To find the value for a model, search by ID or display name. The search ignores case, spaces and punctuation:
copilot-relay models sol fastWhen the search names one routable model, or one ID exactly, the command prints
the line to put in config.yaml, such as gptModel: gpt-5.6-sol-fast; an ID
containing opus is offered as opusModel, matching the routing rule. When
nothing matches, it lists the IDs sharing the search's longest start or end and
exits 1. In the catalog checked on 2026-10-02, gpt6-fast matched nothing and
suggested gpt-5.6-sol-fast and the gpt-6 models. A search cannot be combined
with --deep; check one model with --deep --model <id>.
A running relay applies a gptModel or opusModel change to new requests;
restarting reruns the startup check (see "Hot reload vs restart" below). An empty
catalog is reported explicitly with exit code 0. A search that matches nothing,
and configuration, authentication, network, HTTP, timeout, and malformed-catalog
failures exit 1. A local timeout is reported separately from a real upstream
HTTP 504.
It works while the relay is stopped and when a configured model ID is absent
upstream, which it reports as a warning. It does not bind a port, run startup
preflight or inference probes, change your selected models, or alter Claude
settings. The config loader still materializes resolved keys in config.yaml, and
authentication may update its token cache, just as with other commands.
Advertised does not mean verified. A catalog entry is not proof of successful
inference or compatibility with relay requests, tools, or a particular effort.
After choosing models, startup checks the configured IDs and probes them;
copilot-relay status --deep checks a real request through the running relay.
In contrast, copilot-relay status shows configured relay IDs, and local
GET /v1/models also includes cached context_window, max_input_tokens, and
max_tokens when discovery supplied them. Neither fetches the upstream catalog.
For an interactive picker and any effort metadata it exposes, you can also run
GitHub Copilot CLI (copilot), then enter /model inside its interactive
session. Use the same account as the relay. That selection changes only the
Copilot CLI session, not the relay configuration; a custom gateway may advertise
a different catalog. Never paste bearer tokens into commands, logs, or issue
reports.
Catalog listing is free of inference. Deep mode is opt-in and consumes real Copilot usage; use an exact model selection to avoid probing the whole catalog:
copilot-relay models --deep --model claude-opus-5.5
copilot-relay models --deep --timeout 20 --total-timeout 120
copilot-relay models --deep --model gpt-6-astra --effort low --max-tokens 4096The compact table shows MODEL, STATUS, TIME, and a short RESULT, followed
by nonzero outcome counts. A model ID appears once; long IDs wrap rather than
being silently shortened. Rows arrive as each sequential probe finishes. *
means effort or endpoint metadata was not advertised, not that it was verified.
TTY output uses green for PASS, red for FAIL, yellow for INCOMPLETE and muted
SKIPPED/NOT_TESTED labels. status and status --deep use the same policy for
health, upstream checks and version mismatches. Text labels remain authoritative;
color never changes an exit code. Pipes and dumb terminals default to plain text;
NO_COLOR or FORCE_COLOR=0 disables ANSI; FORCE_COLOR=1 (also 2, 3, true
or an empty value) enables it explicitly. NO_COLOR takes precedence.
status --json is always uncolored.
Routine deep-command setup messages stay in the log file rather than cluttering
the table; required sign-in instructions and setup errors remain visible. Probe
response text, tool arguments and raw errors are never printed. Nonpassing probes
include their generated request_id, and repeated next-step hints are deduplicated.
To see safe evidence for each probe, request details on the same invocation:
copilot-relay models --deep --model claude-opus-5.5 --detailsDetails include planned/actual routes, route_source (catalog, legacy,
policy, or unavailable), known advertised_endpoints, an unknown_endpoints
count, exact sent/reported IDs, effective effort/output cap, client/upstream HTTP
status, completion state, correlation IDs and capture state. Untrusted unknown
endpoint strings are never printed. SKIPPED means no inference was sent.
A replay command appears only for a complete existing private capture. This is
still a new real probe, not offline inspection of an earlier failure; see
Logs and troubleshooting.
| Status | Meaning |
|---|---|
PASS |
The selected model returned completed nonempty text. This is not tool coverage, all-effort coverage, or proof the answer is correct. |
INCOMPLETE |
Generation did not finish. Positive reported output usage with budget exhaustion establishes reachability, not a completed answer. |
FAIL |
HTTP/auth/network failure, timeout, refusal, unexpected output, missing/mismatched model, a listed model that upstream rejects as model_not_supported, or empty completed response. |
SKIPPED |
Advertised metadata excludes the required relay endpoint/effort or identifies a non-chat model, or the ID is unsafe/noncanonical. |
NOT_TESTED |
The overall deadline or an interruption stopped the active probe or prevented a later probe; this is not a model failure. |
The scope is isolated relay pipeline; not running-daemon health. The CLI
passes a synthetic request through the normal in-process Messages handler,
translation, upstream client, token refresh, and response translation. It
selects the exact upstream ID in process-local routing for each sequential probe,
then restores state. Simply sending every ID to the running daemon would instead
remap them to gptModel/opusModel and falsely report per-model coverage. No port
is bound, no daemon is restarted, and model config/Claude settings are unchanged.
Use status --deep to check the running daemon's configured route.
| Option | Default / behavior |
|---|---|
--model |
All advertised IDs; supplied ID must match the catalog exactly. Requires --deep. |
--details |
Include safe per-probe evidence and capture/replay availability. Requires --deep; does not add probes or retries. |
--max-tokens |
4096 per probe, clamped to catalog output and native non-streaming ceilings. |
--effort |
Lowest advertised recognized value above none (low, medium, high, xhigh, max); none only when it is the sole advertised tier, because the relay never sends it on its own and some models advertise it yet reject it. Missing metadata uses low marked unverified. Explicit no-effort support omits the field (effort=omitted). An override unsupported by advertised metadata is skipped. |
--timeout |
30 seconds per probe, capped by a positive upstreamTimeoutSeconds. |
--total-timeout |
300 seconds for the probe phase. Discovery/auth precede this budget. |
Numeric options accept positive whole numbers up to 2,147,483; all probe options
require --deep. Even if config disables upstream deadlines, deep checks still
have their own deadlines. Missing endpoint metadata is marked unverified rather
than treated as proof of support. Model matching removes the relay's known GPT
context suffix. Only for a native /v1/messages probe of catalog ID
claude-opus-5.5, it also accepts the observed provider spelling claude-opus-5-5.
Only for a /responses probe of catalog ID gpt-5.6-sol-fast, it also accepts
gpt-5.6-sol: that catalog entry is the priority service tier of gpt-5.6-sol,
and its replies report the base model. For an undated catalog ID it also accepts
that ID plus one -YYYY-MM-DD snapshot date, such as gpt-5.5-2026-04-23 for
gpt-5.5; a dated ID must match exactly. Keep the catalog spelling in config and
--model; other mismatches still fail, including other -fast IDs and aliases
that upstream serves with another model, such as gpt-4 answered by
gpt-4.1-2025-04-14.
Authentication can refresh tokens; the existing bounded retries may consume additional calls, but
there is no new per-model retry loop. Ctrl+C aborts the active probe and marks
remaining models not tested. Raw shared-pipeline logging is suppressed only for
these diagnostic calls; normal relay logging is unaffected.
Deep exit codes: 0 all selected models passed; 1 invalid options, selection,
authentication, or discovery failure; 2 any other non-pass result or no models;
130 interruption. Ordinary models still reports an empty catalog with exit 0.
With the default claudeUpstreamApi: chat-completions, the configured Opus route
uses Chat Completions and the configured GPT route uses Responses when required
by that model. A new request selects its destination from the current model
selector and resolves effort from that request's fields and history. An admitted
request keeps its routing and effort snapshot even if configuration changes
while its response or WebSearch passes are in flight.
tests/integration/model-effort-switching.test.ts replays actual relay JSON/SSE
output into subsequent requests while switching both directions, including tool
results, long tool names, zero-argument tools, thinking text, inline effort
controls and translated WebSearch continuation. It checks destination model,
effort, output ceilings, tool-call/result pairing and stream completion against
strict mocked upstream requests. This is offline verification of relay-local
translation and the encoded wire-format invariants, not live Copilot acceptance
proof, including for tool IDs carried from another model. It does not establish
unchanged cache hit rates.
Native Messages protocol migration remains outside this coverage. Choose an effort supported by the destination model; authentication, rate limits, network failures, context limits and upstream rejection can still fail a request. The relay does not silently substitute a model or effort to conceal those failures. Merging or building a fix does not update an already running installed relay; that runtime must be updated and restarted separately.
thinkEffort is the default, not an override. The initial effort is the first non-null value:
- Claude Code's native
output_config.effort. - The legacy request field
reasoning_effort. -
thinkEffortinconfig.yaml, or the shipped default when unset.
For example, output_config: {"effort": "low"} uses low even when
thinkEffort: max. Missing or null request fields use the fallback.
thinking.budget_tokens is not an effort level and is not converted into one.
Malformed explicit effort returns 400 rather than silently choosing the default.
When capabilities.supports.reasoning_effort is false or a valid empty list, or
a well-formed supports object omits the key, the model advertises no effort
support: an implicit default is omitted, while an explicit request (including
none) returns local HTTP 400 relay_unsupported_effort before inference or
SSE. Copilot lists older chat models such as gpt-4o, gpt-4.1 and
claude-haiku-4.5 the last way, and each rejected any effort with
invalid_reasoning_effort. Activated inline effort also counts as explicit.
Native history forwards controls, so its pending or clear_at effort controls
are rejected too rather than silently discarded. A missing supports object, or
a reasoning_effort that is true, null or malformed, means unknown, not no
support. With a nonempty tier list,
normal requests keep the chosen level and upstream rejection remains an error;
the relay does not substitute another level.
You can change effort during a Claude Code conversation without clearing its
history. An in-message system output_config whose sole key is effort may use
low, medium, high, xhigh, or max. The latest marker before the latest
user turn selects the current effort; a user turn containing tool results counts
too. A marker after that user turn remains pending until the next user turn.
Older markers may have different levels and stay in the original history.
On translated chat/Responses routes, the relay maps the current effort to the
upstream request field, retaining system text and order and omitting empty
control-only messages. Extra/unknown control keys, malformed values, inline
none, and clear_at still return HTTP 400 JSON before inference or SSE.
Native Messages retains the initial setting and inline controls for upstream to
interpret. See Internals.
The selected effort is used for translated JSON/SSE and all WebSearch passes;
config reload cannot change it within an admitted request. Native follow-ups
retain control history, so a pending marker can activate when a later user/tool-result
turn is appended. Startup preflight checks the configured default for models that
accept effort and sends no effort for explicitly non-reasoning models, logging
think_effort=omitted rather than claiming an effort was tested.
Preserving translated message prefixes avoids unnecessary history changes, but
request-level effort changes may still invalidate upstream caches. Native
per-message effort is the protocol mechanism for preserving that cache; see the
official effort guide.
Choose a default supported by every effort-capable target among gptModel,
opusModel and webSearchBackend when set. Search retrieval still requires the
Responses built-in search operation; selecting an ordinary chat model does not
establish search-tool support. Configured defaults accept only low, medium,
high, xhigh, and max.
Legacy config value minimal is normalized to low; it is not a separate tier.
Missing effort metadata means "not advertised," not "all tiers supported."
thinkEffort: none is not a valid relay-wide default. It and any other malformed
explicit value fail before authentication, upstream probes, or config write-back:
Invalid thinkEffort. Valid values: low, medium, high, xhigh, max. "none" is not allowed as a configured default.
The invalid file is left unchanged so you can correct it; the relay never hides
an invalid value by writing max. Missing keys still use the shipped default.
An invalid hot reload logs its reason once and keeps the last valid runtime
settings. After correcting the file, normal reload resumes.
This restriction is on the configured fallback, not on request-level overrides.
Explicit request none is still passed through for models that support it, such
as GPT-5.6 Sol. It is not an instruction to use the fallback.
The live catalog checked on 2026-09-05 advertised low, medium, high, xhigh,
and max for both gpt-6-astra and claude-opus-5; none is not advertised for
Astra. Higher effort generally trades latency and token use for more reasoning.
Recheck the picker when changing either model instead of assuming every model
accepts max.
Fresh installs use opusModel: claude-opus-5.5; existing configurations keep their
saved value, including claude-opus-5. Check copilot-relay models before changing
it: account entitlement, organization policy, and gateway availability still apply.
The authenticated catalog and isolated relay checks on 2026-09-23 verified the
exact claude-opus-5.5 ID, JSON/SSE, automatic tool use and tool-result continuation,
conversation continuation, low/max effort, and WebSearch final-answer recomposition.
The catalog advertises all five configured efforts; no [1m] suffix is added to
this Opus ID. WebSearch retrieval remains on webSearchBackend or gptModel.
The 2026-09-23 checks used the translated path: tool_choice types tool
and any returned HTTP 400 for this model, while automatic selection worked.
That observation is not a native-API capability measurement. The relay preserves
upstream errors rather than silently converting a required tool call to auto.
Native bridge-search restrictions are described above. These are request-level
capability limits, not proof of an authentication failure.
An isolated 2026-09-30 check with real Claude Code 2.1.285 completed a native
Read → tool result → OK sequence over two model turns. The first two requests
received HTTP 400 for unsupported safeguards; Claude Code then downgraded its
own request. The relay did not strip that field or bypass a safeguard. This is
narrow compatibility evidence, not current production validation; see
Internals for cache accounting and replay evidence.
Edit the existing keys, preserving unrelated settings. macOS or Linux:
${EDITOR:-vi} ~/.copilot-relay/config.yamlWindows PowerShell:
notepad "$env:USERPROFILE\.copilot-relay\config.yaml"To switch the GPT route while keeping your Opus choice, use Mike Farah's yq
v4 on macOS/Linux (optional; the editor needs no extra dependency):
cp -p ~/.copilot-relay/config.yaml ~/.copilot-relay/config.yaml.bak
yq -i '.gptModel = "gpt-6-astra" | .thinkEffort = "max"' ~/.copilot-relay/config.yamlUse max only after checking the other configured model. The fresh-install pair is:
gptModel: gpt-6-astra
opusModel: claude-opus-5.5
thinkEffort: maxThese keys hot-reload. To validate both routes immediately, wait for active work to
finish and run copilot-relay restart, or restart through your service manager
(see the platform service pages). Startup makes small real upstream requests that
consume tokens; restart runs the relay in the foreground. status --deep makes
one real request to the GPT route, not a validation of both routes.
Existing installations keep their saved model choices; upgrading the package does
not migrate them. If startup reports Astra unavailable, edit the generated
config.yaml to a model your account supports, such as gpt-5.6-sol if listed,
and choose a compatible effort. The relay fails explicitly rather than silently
falling back. Restore config.yaml.bak if you need to undo the edit.
Keep the canonical gpt-6-astra in relay config. The relay exposes
gpt-6-astra[1m] to Claude Code and sends only gpt-6-astra to Copilot. [1m]
controls Claude Code's context accounting; it does not enlarge upstream capacity.
The live catalog checked on 2026-09-09 reported:
| Model | Total context | Maximum prompt | Maximum output |
|---|---|---|---|
gpt-6-astra |
1,000,000 | 872,000 | 128,000 |
claude-opus-5 |
1,000,000 | 936,000 | 64,000 |
claude-opus-5.5 (checked 2026-09-23) |
1,000,000 | 1,000,000 | 128,000 |
gpt-5.6-sol |
1,050,000 | 922,000 | 128,000 |
These are discovered values, not hardcoded runtime limits or results of a
million-token production request. Leave room for output, including reasoning,
within the total window. The relay never truncates input or expands an explicit
smaller output budget. Upstream remains the authority on whether a prompt fits.
Local /v1/messages/count_tokens is advisory, not billing. Text uses the available
tokenizer; each image contributes a fixed 4096-token allowance without tokenizing,
decoding or fetching its base64/URL data. Native usage separates noncached input,
cache reads and cache writes, so native input_tokens alone is not total prompt
size. See Internals for measured counts and denominators.
Both streamed and completed JSON responses can use the model's full output limit. Opus 5.5 advertises a native non-streaming ceiling of 16,000 tokens; above it the relay requests upstream SSE and buffers it for JSON callers. The 1M prompt ceiling does not mean a 1M prompt plus 128K output fits in the 1M total window.
For an existing Claude Code setup, select the new identity explicitly:
CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000 claude --model 'gpt-6-astra[1m]'Or enter /model gpt-6-astra[1m] inside Claude Code. claudeSetup: true seeds a
model only when no primary override exists; it preserves existing model selections
and shell wrappers. Update a wrapper that pins a different --model if necessary.
Use the configured model identity rather than assuming a built-in alias has the
same client-side context budget.
For a discovered GPT window other than exactly 1M, managed setup uses the plain
model ID and seeds CLAUDE_CODE_MAX_CONTEXT_TOKENS with the actual window.
Claude's [1m] suffix would otherwise override that numeric setting. For example,
manual Sol setup with the catalog above is:
CLAUDE_CODE_MAX_CONTEXT_TOKENS=1050000 CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000 \
claude --model gpt-5.6-solOn PowerShell, assign each value with $env:CLAUDE_CODE_MAX_OUTPUT_TOKENS = "128000"
(and CLAUDE_CODE_MAX_CONTEXT_TOKENS when needed), then run the same claude
command. Use values from your catalog, not from another account's model limits.
Claude settings, shell environment, auto-compaction, and provider limits can
still constrain the effective context; managed setup never disables compaction.
See Claude's context override rules.
For responses that may take longer than the saved deadline, explicitly set:
upstreamTimeoutSeconds: 0This hot-reloads and removes only the relay's total deadline. Client cancellation
and client/provider/transport timeouts still apply. The fresh-install default
remains 180, and existing saved values are never migrated.
For the documented 1M-context Opus 5.5 / GPT-6 Astra pair, start conservatively with an 800K auto-compact window, keeping auto-compaction enabled. This is a relay recommendation with headroom, not a measured optimum or a guarantee against API errors. The dated catalog above gives Astra an 872K prompt limit and 128K maximum output; reserving 128K from a 1M total also leaves 872K. An 800K target keeps roughly 72K additional room for tool output, compaction and token-estimation error. The client can compact earlier after its own output/buffer reservations.
Set it for the current session and save it to user settings:
/autocompact 800k
Or use it for one launch without changing saved settings:
claude --autocompact 800kThe equivalent user-settings fragment is:
{ "autoCompactWindow": 800000 }For an environment override, use plain token counts:
CLAUDE_CODE_AUTO_COMPACT_WINDOW=800000 claudeCLAUDE_CODE_AUTO_COMPACT_WINDOW takes precedence over the command, flag and saved
setting. Do not use 800k in that environment variable. /autocompact auto returns
to the model-tuned default after removing any environment override;
claude --autocompact auto does so for one launch. Managed settings can override
what the interactive command saves. See the official
auto-compact window guidance
and environment variables.
Use a smaller value when your gateway advertises lower limits. A useful upper
bound for choosing a window is min(maximum prompt, total context - planned output reserve) - extra headroom; use the most restrictive model you switch to. Large
tool/image outputs or uncertain local token counts call for more headroom, not a
larger window. A native near-1M default (roughly 967K on some client models) can
already exceed a gateway's 872K prompt allowance. [1m] and client window settings
do not increase upstream limits. Do not disable compaction or automatically
rewrite existing settings to apply this recommendation.
copilotBaseUrl is validated when the config is loaded, and startup fails if it
is not:
-
Absolute
http://orhttps://. A relative value (/tenant/v1), a bare host (api.githubcopilot.com), or another scheme (ftp://,file://) is rejected. Plain HTTP is allowed, so a local gateway such ashttp://127.0.0.1:8080is a valid value. -
No credentials in the URL.
https://user:password@hostis rejected. The upstream HTTP client refuses these at request time anyway, so accepting one would only turn a clear startup error into a confusing request failure. -
No raw quotes, angle brackets, whitespace, or control characters. These
are what marks the end of a URL in a log line, so a value containing one
cannot be recognised as a whole URL afterwards and its tail would be printed
unredacted. Percent-encode them instead:
%27for',%22for",%60for a backtick,%3C/%3Efor</>,%20for a space,%09for a tab. The encoded form is accepted and used exactly as written. Spaces or tabs around the value are just trimmed, as with every other config key.
The error names the key and the rule; it never repeats the value you configured, because that message can end up on a terminal or in a log file.
If your copilotBaseUrl has a path, query string, or fragment — for example a
custom gateway like https://gateway.example/tenant/abc123 — only its origin is
shown:
copilot base url: https://gateway.example (path/query/fragment hidden)
The same applies to the copilotBaseUrl row in copilot-relay status and in
copilot-relay status --json, and to upstream URLs that appear in error
messages in ~/.copilot-relay/logs/, which are written as
https://gateway.example[redacted].
This matters 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. A base URL with no path,
such as the default https://api.githubcopilot.com, is shown in full — there is
nothing in it to hide.
By default the relay authenticates no caller. With the default host: 127.0.0.1
only local processes can connect. With 0.0.0.0 or a LAN address, any client that
reaches the port can send requests, and each one uses your Copilot account. Set
apiKey before you bind host beyond loopback:
apiKey: <a long random value>One way to generate a value:
node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"With apiKey set, every request except GET /healthz and GET/HEAD /api/hello
must carry the key as x-api-key: <key> or Authorization: Bearer <key>. Without
it, or with a different key, the relay answers HTTP 401 with an Anthropic-style
authentication_error, before it reads the body or runs any route. An empty
apiKey, the default, disables the check.
- A key is at least 16 visible ASCII characters, with no spaces. Generated from
letters and digits, like the value above, it never needs quotes. Quote a key
that starts with
#: unquoted, YAML reads it as a comment, which leaves the key empty and the check off. - The key is never logged or displayed.
copilot-relay statusshows it as[redacted], in text and in--json, and sends it on itsGET /v1/modelsprobe and on the--deepPOST /v1/messagesprobe.copilot-relay modelsprints[redacted]wherever a model ID, a display name, a configured model or a search holds it, and--deepdoes not probe an ID that holds it. - Debug captures leave out the header that carries the key, but keep request and response bodies unredacted, so a key that a client puts in a prompt is stored there; see Logs and troubleshooting.
-
config.yamlthen holds the key. A newconfig.yamlis created0600, readable only by you; an existing one keeps its mode, and the relay never changes it. On Linux and macOS, whenapiKeyis set andconfig.yaml, or the file it links to, gives other local users any access, startup and each reload that sets or changes the key log one error that names the file and its mode and says to runchmod 600on it. Windows has no such mode bits. - With
claudeSetup: true,startwrites the key as Claude Code'sANTHROPIC_AUTH_TOKEN; see Claude Code settings. - When
hostis not a loopback address andapiKeyis empty, startup logs one warning that namesapiKey. The relay still starts, so an existing0.0.0.0setup keeps working. - Admission also checks the request's Host. A remote client must use the address
set in
host; a wildcard bind such as0.0.0.0admits only loopback names. See Architecture. - The relay speaks plain HTTP. On a network you do not trust, the key and your prompts can be read in transit.
apiKey hot-reloads: once the relay applies a valid edit, the next request must
carry the new key. An edit that fails validation keeps the previous key and logs
the reason, never the value. A client keeps the token it loaded, so after a
change:
- Edit
apiKeyin~/.copilot-relay/config.yaml. - Update each client. With
claudeSetup: true, runcopilot-relay restart, or restart the service, so startup rewritesANTHROPIC_AUTH_TOKEN; the settings writer runs only at startup. Otherwise set the new key in each client yourself. - Restart running Claude Code sessions. Until then they send the old key and get
401.
upstreamProxy sends the relay's outbound calls through an HTTP proxy: every
Copilot request, the startup check, token refresh, GitHub sign-in, and the
copilot-relay usage request. Calls to the relay's own listener, such as the
status probes, never use it, nor a proxy from the environment, even when
NODE_USE_ENV_PROXY=1 makes Node's own fetch use HTTP_PROXY.
| Value | Route |
|---|---|
| empty (default) | Direct. HTTPS_PROXY and HTTP_PROXY are ignored. |
env |
HTTPS_PROXY or HTTP_PROXY, read when the relay starts. With only HTTP_PROXY set, it is used for HTTPS too. Hosts listed in NO_PROXY connect directly. Lower-case names work too. |
| a URL | That proxy, such as http://proxy.example:3128. |
The relay tunnels each call through the proxy with CONNECT, so its HTTPS calls
stay encrypted end to end. It reads no other proxy setting, such as the operating
system's proxy configuration or a PAC file.
copilot-relay usage reads upstreamProxy from config.yaml without writing the
file. Without a config.yaml it connects directly; with one it cannot read, or
one that is invalid, it stops before sending anything. See
Logs and troubleshooting.
upstreamProxy is validated when the config is loaded, and startup fails if it
is not:
-
Empty,
env, or an absolutehttp://orhttps://URL.envmust be lower case.socks5://and other schemes, a bareproxy.example:3128, and a URL without a host are rejected. -
No path, query or fragment.
http://proxy.example:3128/is accepted;http://proxy.example:3128/pathis not. -
A user name and a password together, or neither.
http://user:[email protected]:3128sends aBasicProxy-Authorization. A user name without a password, or the reverse, is rejected, because no credentials would be sent at all. Percent-encode reserved characters in either, such as%40for@and%23for#.
As with copilotBaseUrl, the error names the key and the rule and never repeats
the value. These rules apply to the URL in config.yaml. With env, undici
reads HTTPS_PROXY and HTTP_PROXY as they are, without these checks: a value
with a path, query or fragment, a malformed percent-escape, or no scheme stops
startup with an error that names the variables, not their value, and a user name
without a password is accepted and sends no credentials.
The user name and password are credentials. Once the relay has set up its
upstream connections, its logs show user:password, as written or decoded, and
the Basic value it sends as [redacted]. A user name or password alone is not
redacted there: redaction applies to every log line, and a short or common one,
such as copilot, would also be cut out of ordinary text, such as
copilot-relay. For the same reason, a form shorter than 8 characters is not
redacted. The error of an upstream request that fails without a response is
cleaned before anything logs it: every form of the proxy's credentials in it is
replaced, short ones and a user name or password alone included, and the raw
bytes of a reply the relay could not parse are dropped. copilot-relay status
and the startup log show only the proxy's origin:
Upstream proxy: http://proxy.example:3128 (credentials hidden)
readAppConfig() writes every key back to config.yaml, so a default of env
would be written into every existing install on upgrade, and an install whose
shell sets HTTPS_PROXY for other tools would change route without being asked.
Opt in with env or a URL instead.
If an upstream request fails without a response while upstreamProxy is empty
and HTTPS_PROXY or HTTP_PROXY is set, the relay logs this once per process:
Upstream request failed without a response. HTTPS_PROXY or HTTP_PROXY is set, but upstreamProxy is empty, so copilot-relay connected directly. To use that proxy, set upstreamProxy: env in <path to config.yaml>, then run the command again or restart the relay.
A service manager (launchd, Task Scheduler, systemd) does not start the relay
from your shell, so env does not see variables exported in your shell profile.
Prefer a URL in config.yaml, or set the variables in the service definition.
See the service page for your platform: macOS,
Windows, Linux.
Hot-reloaded, applying to work that starts after the change:
logLevellogRetentionDaysthinkEffortupstreamTimeoutSecondscopilotBaseUrlwebSearchBackendclaudeUpstreamApigptModelopusModelapiKey
Requires restart:
hostportclaudeSetupupstreamProxy
host and port require restart because the listening socket is already bound.
claudeSetup is read once during startup, so toggling it changes nothing until
the relay starts again. upstreamProxy sets up the relay's upstream connections
once, at startup, and a reload leaves them as they are.
The relay checks config.yaml once per second. An edit that fails validation
keeps the previous runtime settings and logs one error that names the file and
the reason:
Could not reload config (<path to config.yaml>): Invalid upstreamTimeoutSeconds: expected a non-negative integer (0 disables the deadline). Keeping the previous runtime settings.
The relay keeps retrying every second without repeating that line. Saving the
file again, or a different reason, logs again. When a corrected file applies,
the relay logs Config reloaded: at info.
Each admitted request snapshots routing, protocol mode, timeout, search backend, effort fallback, and the catalog view before reading its body. Reloads affect new requests, not retries or later passes of an active turn. Credentials are the exception: each attempt reads the live refreshed token.
Changing gptModel reroutes new upstream requests, but does not rewrite the model
or token settings already saved in ~/.claude/settings.json. Check those settings
when switching models; managed setup only seeds absent budgets.
With claudeSetup: true, copilot-relay start writes:
ANTHROPIC_BASE_URL=http://127.0.0.1:4142
ANTHROPIC_AUTH_TOKEN=<apiKey, or a dummy local token if the file has none>
CLAUDE_CODE_MAX_CONTEXT_TOKENS=<discovered GPT context window>
CLAUDE_CODE_MAX_OUTPUT_TOKENS=<largest discovered output budget of the model pair>
CLAUDE_CODE_AUTO_MODE_SERVER=0
into:
~/.claude/settings.json
With apiKey set, the token is that key, replacing a dummy value or an older key,
because the relay requires it on every request; see
Require a client key. start then publishes the file
readable only by you (mode 0600; Windows has no such mode bits), and publishes it
again when its mode lets other local users read it. Without apiKey, start
keeps an existing ANTHROPIC_AUTH_TOKEN, even a key an earlier apiKey wrote, and
leaves the file's mode as it is; it sets the token to dummy only when the token
is missing or empty. Any value works then, because copilot-relay authenticates
to GitHub Copilot with your cached GitHub/Copilot tokens, not with Claude's token.
The two budget variables are written only when absent. Smaller explicit values
are preserved. The relay bounds each request to the actual routed model's output
ceiling, so the common client setting cannot exceed the Opus limit when switching
from Astra. When a gateway omits valid limit metadata, startup logs that limits
are unavailable and leaves explicit budgets unchanged rather than inventing them.
With claudeSetup: false, configure these client variables yourself.
The settings writer uses the same snapshot/atomic replacement boundary; malformed
or existing empty settings files are not overwritten, and unrelated values remain.
CLAUDE_CODE_AUTO_MODE_SERVER=0 is also written only when the key is absent. In
auto mode, Claude Code asks the server to check actions such as shell commands.
Through the relay those checks did not reach Claude Code on the chat-completions
or messages route, so it made its own classifier requests and printed a notice
that the session isn't eligible for auto mode's no-charge classifier requests.
With 0, Claude Code skips the server check and the notice; the classifier
requests stay as they were. The value stays in ~/.claude/settings.json: remove
or change it when Claude Code connects without the relay, or through a gateway
that supports the server checks. Claude Code documents the variable as temporary;
see auto mode classifier billing.
The dummy token is not network authentication. Host/Origin checks and JSON
content-type validation reduce browser-origin misuse, not access by an arbitrary
network client. Keep the listener on loopback, or set apiKey before binding
beyond it; see Require a client key and
Architecture.
For Claude Code 2.1.242 or newer, merge this example into your user
~/.claude/settings.json, keeping the existing relay connection, budgets,
permissions, hooks, and other settings:
{
"model": "gpt-6-astra[1m]",
"availableModels": ["opus", "gpt-6-astra[1m]"],
"modelPicker": {
"options": [
{ "model": "opus", "label": "Opus" },
{ "model": "gpt-6-astra[1m]", "label": "GPT-6 Astra" }
],
"replaceBuiltInOptions": true
},
"env": {
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-5.5"
}
}This uses the default relay pair, opusModel: claude-opus-5.5 and
gptModel: gpt-6-astra. Keep the Opus override aligned with your configured
upstream ID. The Astra client ID carries [1m] for a discovered one-million-token
window; if your gateway reports a different window, use the relay's exposed plain
ID consistently in model, availableModels, and modelPicker.options instead.
The suffix is client context accounting, not a way to enlarge upstream capacity.
modelPicker replaces the built-in lineup with these two named choices instead
of suggesting unrelated Sonnet, Haiku, or Fable routes. Claude Code may still show
Default and a row for the current session's model; it does not guarantee exactly
two visible rows. availableModels is the selection allowlist and also filters
picker entries. model only selects the startup default. Use /model to select
Opus or GPT-6 Astra, or /model gpt-6-astra[1m] to select Astra explicitly.
This picker setting belongs in user or managed settings (or --settings), not
project/local settings. Managed allowlists cannot be expanded by this example;
without a managed list, user/project/local availableModels arrays can merge.
For an exact effective pair, remove stale entries in settings you control and
check any managed restrictions. With normal prefix matching, Default is not
constrained by the allowlist alone; organizational enforcement additionally uses
enforceAvailableModels in managed settings. See the official
model configuration and
modelPicker reference.
An explicit --model, an exported ANTHROPIC_MODEL, or a saved resumed-session
model can override the startup selection; inspect shell wrappers and higher-priority
settings if the chosen model does not change. claudeSetup preserves existing
model, availableModels, and modelPicker choices; it does not install or reset
this lineup. Claude Code can substitute an allowed model for an excluded alias,
so verify the selected identity instead of assuming a disallowed alias must error.
This example is opt-in and does not change your running relay.
~/.copilot-relay/
config.yaml
github_token
copilot_token.json
logs/copilot-relay.2026-07-25.log <- active, rotates at local midnight
logs/copilot-relay.2026-07-24.log
captures/<local-date>/<request-id>/ <- debug only; private full bodies
github_token is the login source. copilot_token.json is a short-lived Copilot
bearer-token cache with refresh metadata.