Skip to main content

Troubleshooting

Start at /admin/health. One call names the failing leg.

Check deployment health

/admin/health probes every leg of the deployment live and tells not configured apart from configured but unreachable:

Check deployment health
curl -fsS "https://<your-gateway>/admin/health" \
-H "Authorization: Bearer $ANYRAY_ADMIN_TOKEN"

200 means every required leg is up. 503 means a required leg is down, and its name is in the response body. Only required legs can turn the check red; a down optimizer or a lagging schema never fails it.

LegWhat it checksRequiredIf it's the failing leg
gatewayThe process is serving requestsNoA response at all means it's up
observabilityTrace store reachable, or intentionally not configuredWhen configuredCheck ANYRAY_OBSERVABILITY_DB_URL and the Langfuse stack
spendSpend store (Postgres) answers a live SELECT 1. spend.auth names how this process authenticates: password or rds-iamYesCheck ANYRAY_SPEND_DB_URL and that Postgres is reachable
schemaBoot migrations applied. state says where the database sits relative to this imageNo · informationalSee the schema leg reports drift below
optimizerOptimizer /health reachable, plus configStore for settings and retrievalStore for durable handle writesNo · fails openCheck the optimizer service and ANYRAY_OPTIMIZER_URL
inferenceThis deployment has a working way to serve requests: a provider key (configuredProviders), an upstream you run (byoUpstream), or successful inference already served (servedRequests). defaultRoute names whether requests that send no routing header have a routeNo · informationalSee inference.ok is false below
portalBilling app metering, entitlement lease status, and where this replica's lease came from (leaseSource, leaseTrust, leaseAcquire)NoCheck the Billing app connection (configure); see a replica answers 503 on every request below

inference.ok is false

inference.ok: false means this deployment has no working lane at all: no server-side provider key, no upstream you run, and no successful inference served since the replica booted. A deployment can be green everywhere else and still refuse every request. Developers on the org-key lane get:

424 no provider key configured for anthropic: the request authenticated with
a gateway client key, but the gateway holds no server-side anthropic API
key to forward; add one in the console Providers page

It is a 424, not a 401, on purpose: nothing is wrong with the developer's credential, so re-running anyray-connect will not help. Add a provider key in the console → Providers. If some developers work and others do not, the failing ones are on the org-key lane; same fix.

  • A subscription-only deployment is healthy here, not broken. Seats riding GitHub Copilot or their own Claude subscriptions never touch an org key; once such a deployment has served anything, servedRequests keeps ok true.
  • servedRequests counts 2xx on inference paths since this replica booted, so zero on a fresh or idle replica is not evidence of a problem; it can only ever make ok true, never false.
  • configuredProviders is a count, not a list. The endpoint never names your vendors, and it reads null when this probe could not open the provider store.
  • defaultRoute is a separate question from ok. A deployment with two provider keys and no default routing config can serve any request that names a provider, so ok stays true while every header-free client is refused. See Requests are refused for a missing routing header below.

Common issues

Scan the titles, then expand the one that matches.

Optimizer settings revert after a deploy

A change on the console Optimizer page saves cleanly, then comes back as it was after the next image bump; or two browser tabs disagree about a strategy.

Check optimizer.configStore on /admin/health:

ValueWhat it meansWhat to do
sharedSettings live in Postgres, read by every replicaNothing; healthy
per-podSettings live on whichever replica served the save, discarded on the next rollSet ANYRAY_SPEND_DB_URL on the optimizer service, or check that the gateway can reach the optimizer
refusedA store was supplied but the optimizer cannot read it, so saves are rejectedCheck the optimizer can reach the gateway's database (security groups, network policy, credentials)
probingStore just handed over, being verifiedWait a few seconds, re-check
absentOptimizer unreachable, or its image predates this fieldCheck the optimizer leg, then the image tag

The gateway hands its database URL to the optimizer every 60 seconds, so most deployments reach shared on their own shortly after boot; a value that stays per-pod means those pushes are not arriving or not accepted. The Optimizer page shows the same state as a banner before you save.

Claude Desktop will not route, and setup says a machine policy owns it

Claude Desktop treats a managed configuration source as the whole configuration: once a profile sets any recognized Claude policy key beyond the three app-behavior-only ones (disableAutoUpdates, autoUpdaterEnforcementHours, disableWslSessions), local values are ignored, so Connect declines to write settings the app would silently discard.

1
Re-run for Claude Desktop alone
npx anyray-connect@latest --tools claude-desktop

Connect before 0.11.147 could report a machine policy on a Mac carrying no Claude profile at all. Most machines stop here, and naming the tool leaves everything else untouched.

2
Install the profile Connect generated

If it still declines, the policy is real, and Connect already wrote the fixing profile: ~/.anyray/claude-desktop-anyray.mobileconfig (macOS) or %USERPROFILE%\.anyray\claude-desktop-anyray.reg (Windows). No MDM needed: on macOS, double-click it and approve under System Settings → General → Device Management (an administrator password, nothing else). The profile carries no credential; it points at shared helpers, installed once:

sudo anyray-connect desktop helper --write --platform posix --bin /usr/local/bin/anyray-connect

Then fully quit and reopen Claude Desktop (Cmd+Q); it reads configuration once at launch.

3
Or hand the same file to whoever manages the devices

The identical profile deploys through Jamf, Intune, or Kandji fleet-wide; to keep it editable without repushing MDM, generate a bootstrap profile instead (anyray-connect desktop fleet bootstrap <platform> <https-url>). Both modes: Managed fleets (MDM).

Nothing else is blocked

Claude Code, Codex, and Cursor are configured per user and route immediately; run the normal setup rather than waiting on IT.

“Connection closed mid-response” or an API error naming your gateway host

The host in the error is just the client's ANTHROPIC_BASE_URL (or OPENAI_BASE_URL) rendered into a fixed template; it is not evidence the gateway caused the failure. The exact wording names the origin:

The client saysWhat actually happenedWhere to look
Server error mid-responsethe provider dropped the stream and the gateway relayed and labelled itthe provider's status page
Connection closed mid-responsethe TCP connection between the tool and the gateway diedyour own edge (below)
Response stalled mid-streamthe client's own idle watchdog firedthe provider, then latency
request failed, with no other detailthe upstream refused the request and its message did not survive translationupgrade past v1.10.319

The last row is the one case where the wording is the gateway's fault: translating a Claude-format client to an OpenAI-format upstream, builds through v1.10.319 read the upstream's failure message from only one of the places providers put it, so an out-of-credits notice or model-not-found arrived as the bare words request failed. Until you upgrade, read the upstream's logs.

When the provider closes a stream after it started, the gateway appends a final SSE error frame naming the origin; a client with nothing rendered yet shows it in full, and otherwise summarises it as Server error mid-response, still provider-origin:

What the client renders on a provider-side close
Anthropic (api.anthropic.com) closed the response stream before it finished. This is an
upstream provider failure, not an Anyray gateway failure: the gateway relayed the stream as
it arrived and the connection was closed from the provider side. Check https://status.claude.ai/
for provider status. Retry the request.

Fix: retry; these are transient. If they cluster, check the provider's status page. Confirm the origin in gateway logs, where a provider-origin close writes a single line:

Gateway log: provider closed the stream
passthrough stream closed by upstream: upstream=Anthropic class=TypeError code=UND_ERR_SOCKET

A failure that never got a first byte returns a 502 with a status-page hint and logs passthrough fetch failed. Both are upstream-origin.

No such line, and the wording was Connection closed mid-response? The stream died on your side. Check the timestamps against a rollout or restart (kubectl get pods restart counts; lastState.terminated.reason for an OOMKilled), then the read and idle timeouts on every hop in front of the gateway; they must outlast a single completion (the chart's Ingress values are streaming-safe by default). The gateway drains in-flight requests for ANYRAY_SHUTDOWN_DRAIN_MS (default 90s), so give each edge a stop budget above that: stop_grace_period in Compose, terminationGracePeriodSeconds in the chart, StopTimeout plus deregistration delay on ECS, RAILWAY_DEPLOYMENT_DRAINING_SECONDS on Railway. Size each service against its own drain: the optimizer reads the same variable with a 15s default, and end-point control force-exits at 35s.

Non-streaming requests still truncate

The error frame exists only for streaming responses. A non-streaming request that dies mid-body surfaces as a bare truncation; the log line above is how you attribute it.

Replies come back blank, or an error says the body has no choices

Your own gateway answered 200 with a body Anyray could not read as an OpenAI chat completion, so there was no reply to return:

upstream returned 200 with no choices; the body is not an OpenAI chat completion (keys=[data,request_id])

The keys list names the top level of what your gateway sent back, so you can see which format it answered in. Values are never included.

Earlier builds showed this as an empty reply with zero tokens and no error, which also recorded the request as a success. Anyray already forwards a native Claude response untouched, so the usual cause is a third shape, such as a wrapper around the completion.

Fix: if your gateway serves the Claude Messages API, set the provider to litellm and point custom_host at that endpoint. Both directions then pass through unchanged. If it does not, check that custom_host names the chat-completions endpoint itself.

Requests are refused for a missing routing header

A request that names no provider is refused before it is routed:

Send either x-anyray-config or x-anyray-provider, or ask the gateway operator to set a
default route for headerless requests.

Tools configured by anyray-connect send the header themselves, so this is almost always a hand-written request, or a deployment meant to run without headers whose default route is missing.

Check inference.defaultRoute on /admin/health:

ValueWhat it meansWhat to do
configuredHeader-free requests have a routeNothing; healthy
no-configured-providersNothing is configured to route toAdd a provider in the console → Providers
multiple-providers-no-default-routeSeveral providers are configured, so none is the defaultSet the default on the console → Routing page
provider-store-unreadableThis replica could not open the provider storeCheck the spend leg; the store is shared

A single configured provider becomes the default on its own. Adding a second one is what removes it, which is why header-free requests can start failing after a change that looks unrelated.

An upstream error says Not Found, or names a status without a cause

Errors relayed from an upstream carry the status and the endpoint Anyray called:

litellm error (HTTP 404 from https://llm.example.internal/v1/messages): Not Found

The URL is the request Anyray actually made, with any query string removed. Compare it against what your upstream serves:

  • The path is right and your upstream does not serve it. The route is missing on that build. LiteLLM serves the Claude Messages API at /v1/messages on current versions only.
  • The path is doubled, or has a prefix you did not expect. custom_host is being appended to. Set it to the full endpoint URL instead, which is used as-is when it names the endpoint.
  • Your upstream serves the endpoint somewhere else entirely, or serves two of them at unrelated paths. Name the path yourself with custom_endpoints (Endpoint paths on the provider key in the console) and Anyray stops inferring it. See Naming the paths yourself.
Traces show “—” in Tokens Saved: optimization is paused

Every request is served, but the console's Traces list shows in Tokens Saved, savings sit at zero, and an “Optimizations are paused” banner sits on every page.

A billing state, not a bug: when an Anyray trial ends with no active subscription, the signed entitlement lease pauses the optimizer (optimizerEnabled: false). The gateway keeps proxying every request unchanged; only the optimize hook is skipped. An image bump can surface it, because older gateways kept optimizing while lapsed.

Fix: add a payment method in the Anyray billing portal. Optimization resumes on the next lease refresh, usually within minutes, no redeploy. If your workspace is on a contract and shouldn't be on the self-serve trial clock, contact Anyray.

One gateway replica answers 503 on every /v1 request, and its readiness check fails

Symptom. After a rollout, requests landing on one replica fail with 503; the pod is Running but not Ready, and GET / on it returns AI Gateway awaiting entitlement lease.

A metered gateway serves /v1/* only while it holds a signed entitlement lease. Replicas share it through the spend database, so a new pod normally adopts its peer's lease within seconds. Readiness stays 503 only while the pod holds no lease at all; an expired or suspended lease keeps it Ready (answering the right 402/403).

Check the portal leg on that pod (sh -c so the token expands from the container's environment; on Compose, docker compose exec gateway in place of the kubectl exec):

Lease provenance on one replica
kubectl -n "$ANYRAY_NAMESPACE" exec <gateway-pod> -- sh -c \
'wget -qO- --header "Authorization: Bearer $ANYRAY_ADMIN_TOKEN" localhost:8787/admin/health' \
| jq .portal
FieldMeaning
leaseactive, grace, suspended, or null (none held)
leaseSourceWhere the held lease came from: db (shared by a peer replica), file (this pod's cache), cp (the Billing app)
leaseTrustVerdict on the last lease read: trusted, bad_signature, or deployment_mismatch
leaseAcquire.lastVia / lastOutcome / lastStatusThe last Billing app attempt: meter or entitlement, ending in lease, throttled, rejected (untrusted lease), http_error (status in lastStatus), or network (never connected)
leaseAcquire.retryInMsWhen the next attempt is armed after a throttled or leaseless outcome (null on the regular cadence)
  • leaseTrust: deployment_mismatch. The stored lease is bound to another deployment (two gateways sharing one adt_ token, or one spend database shared between deployments); give each its own token and database. bad_signature: the lease does not verify against the pinned Billing app key.
  • lastOutcome: throttled with lease: null that does not clear. The Billing app is shedding load; the pod retries at the Retry-After it was given. Leaseless for more than a few minutes: send a support bundle.
  • lastOutcome: network. No path to the Billing app; check egress and any TLS-inspecting proxy (accordion below). http_error with lastStatus: 401: the deployment token is invalid or revoked.

These fields carry only the lease status and provenance, never content.

/admin/health returns 503: a required leg is down

Only spend (always) and observability (when configured) can turn the check red. The body names the one whose ok is false.

  • spend. Confirm ANYRAY_SPEND_DB_URL points at a reachable Postgres that accepts connections.
  • observability. A trace store is configured but unreachable: confirm ANYRAY_OBSERVABILITY_DB_URL and the Langfuse stack. If you don't run observability, leave it unconfigured; the leg reports not configured and never gates.

Re-run once the leg is reachable; the check flips back to 200. Configured, reachable, and still failing: send a support bundle.

IAM database auth is not taking effect

spend.auth is derived from ANYRAY_SPEND_DB_URL, not configured separately, so it is how you confirm a switch to RDS IAM authentication landed:

Check the database auth mode
curl -fsS "https://<your-gateway>/admin/health" \
-H "Authorization: Bearer $ANYRAY_ADMIN_TOKEN" | jq '.spend.auth'
  • password. A password was found, and a password always wins. The gateway looks everywhere Postgres itself looks: the URL, a ?password= parameter, PGPASSWORD, and a matching line in the password file (PGPASSFILE, otherwise ~/.pgpass). Remove it, leaving the user (postgresql://appuser@mydb.abc123.us-east-1.rds.amazonaws.com:5432/anyray). A URL with no user at all also reads password, since IAM tokens are minted per database user.
  • rds-iam. Tokens are minted from the pod's AWS identity. If connections still fail, the IAM role needs rds-db:connect on arn:aws:rds-db:<region>:<account>:dbuser:<db-resource-id>/<db-user>, and the database user needs GRANT rds_iam TO <db-user>.
  • null. The gateway is not backed by Postgres.

A connection string that asks for IAM but cannot be honoured never shows up here: the gateway refuses to start rather than serve traffic it cannot attribute. Look for the FATAL line in the startup logs:

CodeWhat to change
RDS_IAM_NO_REGIONThe endpoint carries no region AWS recognizes. Set AWS_REGION on the service.
RDS_IAM_SSL_DISABLEDThe URL sets sslmode=disable. Remove it: RDS IAM requires TLS, and the gateway configures it for you.
RDS_IAM_BAD_PORTThe port in the URL is not a valid port number.
RDS_IAM_EMPTY_TOKENAWS returned no token. Check that the pod has working AWS credentials.
The schema leg reports drift after an update

The gateway self-migrates its schema on boot, so an image bump is all a schema change needs. The schema leg reports version (newest migration applied), expected (newest this image ships), and state. After an update:

Assert the schema is current
curl -fsS "https://<your-gateway>/admin/health" \
-H "Authorization: Bearer $ANYRAY_ADMIN_TOKEN" | jq -e '.schema.state == "converged"'
stateWhat it meansWhat to do
convergedThe applied schema is the one this image shipsNothing
aheadAnother replica on a newer image already migrated. Normal during a rolling deploy or after a rollback, and safe: each release reads and writes the neighbouring version's schemaNothing. It clears once every replica runs the new image
behindThis replica's own migration is pending or failedGive a fresh deploy a moment and re-check. If it persists, read the gateway logs for the migration error
unknownThe database could not be read, so the applied version is not knowableConfirm Postgres is reachable (spend.ok)
noneNo Postgres, so nothing to migrateNothing

The leg stays informational: no state fails the overall health check, because a mixed-version window is expected during every rolling deploy.

schema.converged is still published as a boolean for older scripts. It reads true for both converged and ahead.

Externalized output can't be pulled back (retrieval is dark)

When Anyray trims an oversized tool output it leaves a · retrieve ctx_… marker and stashes the original; the model pulls it back through the anyray_retrieve MCP tool. New handle-producing trims run only while the gateway has a fresh authenticated retrieval lease and the optimizer has confirmed the span's durable write. If either side is unavailable, Anyray keeps the original span and safely stands the handle-producing strategy down. A settled decision pin may still replay an older marker byte-for-byte for provider-cache safety.

If a managed integration remains dark, diagnose it on the affected developer's machine:

Check the whole retrieval loop
anyray-connect doctor --verify
  • Tools shows each tool's retrieval MCP-server registration. Not registered usually means connect last ran from a one-off npx cache (registration needs a durable install); binary is gone means the registered executable moved. Both: re-run anyray-connect.
  • Retrieval loop (with --verify) sends one probe-marked request with a synthetic handle; the healthy verdict is the gateway answering "unknown handle", proving reachability, key auth, and the optimizer's retrieve wiring in one shot. Failures name the leg: auth (enroll / set a personal key), unsupported (gateway image predates the loop; update it), unavailable (optimizer down or ANYRAY_OPTIMIZER_URL unset), unreachable (network).

The probe is side-effect-free: the synthetic handle reads nothing, and its x-anyray-retrieve-probe header never counts as model retrieve capability. Open a marker by hand with anyray-connect retrieve ctx_…; find one that scrolled away with anyray-connect recall "describe it".

Operators can inspect optimizer.retrievalStore on /admin/health without touching a developer machine. ready permits new durable handles. cooldown means the bounded backend circuit is open and will retry automatically after five minutes. unconfigured and content_disabled keep new handles off until durable encrypted storage is available. An absent value means the optimizer is unreachable or predates this field.

Fix drift in one step

anyray-connect doctor --repair re-applies any tool whose gateway routing drifted (a tool update resetting its config is the usual cause). It only touches tools a previous apply recorded; a tool you explicitly reverted is never resurrected. The same repair runs automatically whenever Connect's key refresher notices drift.

Savings look low and the hook lane may be dark

Connect's PostToolUse hook trims oversized tool output before it reaches the model. When that lane dies nothing errors: the hook exits 0 and config-based checks stay green. So anyray-connect doctor probes the lane itself and prints a Hook lane section:

StateMeaning and fix
liveThe gateway accepted this machine's key and answered the probe.
key_rejectedUsually a hand-pasted key that lapsed. On an enrolled machine an expired key needs no action: the next scheduled refresh re-mints from the enrollment certificate and rewrites the tool configs; doctor --repair does the same immediately. It mints a credential, which plain doctor never does (ANYRAY_REFRESH_DISABLE=true forbids it). Only a missing or expired key is ever re-minted, and only when the gateway names the class: a revoked key, an unknown key, and a deactivated user are never re-minted, by --repair either; re-enroll instead. An older gateway sends no class, so nothing re-mints; re-run anyray-connect.
target_nullNo gateway target resolves, so the hook silently sends nothing. Typical on a machine templated by hand; run anyray-connect once.
never_invokedInstalled and the gateway answers, but nothing ran the hook in over a week. Restart the tool so it re-reads its settings. On OpenCode check the binary path in the generated plugin: it converts a failed launch into empty output silently.
undeterminedThe state could not be established (old gateway protocol, a proxy or WAF, an unreachable host). Informational only: never a problem, never repaired, never reported to the fleet.

Claude Code's machine-wide managed-settings.json can also kill the lane: disableAllHooks: true skips every hook, and allowManagedHooksOnly: true drops user hooks (healthy only when the managed hooks block itself carries the Anyray PostToolUse hook). Neither is repairable from user scope: ask the MDM owner to drop the key, or deploy the credential-free block that anyray-connect managed --print --sso <link> --bin <path> prints. Fleet check id: hook_policy (disabled or restricted_dropping_ours); a clean, absent, unreadable, or malformed managed file is never a fault.

The probe sends no tool output and carries the same x-anyray-retrieve-probe marker as the retrieval probe, so it can never book a saving.

Claude Desktop lost its MCP servers, skills, or plugins after switching to Anyray

Third-party inference gives Claude Desktop its own profile, so anything configured in the first-party one is unread, not deleted. Connect copies local MCP servers, user-created skills, and user-installed plugins across on every apply lane, MDM reconciles included; skills and plugins land the next time Claude Desktop is fully quit. Run anyray-connect doctor and read the Claude Desktop profile section: pending means the copy is queued, not_routed means this machine does not route Desktop through Anyray at all. Every state is listed in the Connect reference.

To bring conversation history across as well, fully quit Claude Desktop and run anyray-connect migrate. Fleet check id: desktop_config_carry; it never counts against compliance.

Console shows an old version after an upgrade

The deployment version (GET /admin/update-statusversion) is baked into the running gateway image, so a stale number means the new image never rolled, not that reporting is behind: a helm upgrade or docker compose up -d only restarts the gateway when the rendered spec actually changes. (schema.version on /admin/health is different: the newest migration applied to your database.)

Confirm and force the running image (Kubernetes)
# What image is live right now?
kubectl -n "$ANYRAY_NAMESPACE" get deployment anyray-gateway \
-o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
# Pin the build and roll it out
helm upgrade anyray ./helm -f my-values.yaml --namespace "$ANYRAY_NAMESPACE" \
--set image.tag=vX.Y.Z
kubectl -n "$ANYRAY_NAMESPACE" rollout status deployment/anyray-gateway

On a moving tag (image.tag: latest) pair it with pullPolicy: Always and kubectl -n "$ANYRAY_NAMESPACE" rollout restart deployment/anyray-gateway, or the node keeps its cached image. Full flow: Kubernetes → Upgrade. The console catches up within a couple of minutes of the new pod turning Ready.

One user's tools fail, but health is green

The failure usually lives on that machine: a stale base URL, an expired enrollment, a missing key-renewal schedule. None of that is visible to the gateway, so run the client doctor there:

Generate a client report (in the user's failing terminal)
npx anyray-connect@latest doctor --json > anyray-client-report.json
Run it in the terminal that's failing

doctor inspects that shell's own environment. A shell that was open when the user reverted keeps exporting the gateway with no credential, so every tool launched from it returns 401 valid client key required while every file on disk reads clean. doctor reports that as a Shell problem with the exact unset to run.

The auth smoke probe runs by default: one tiny request whose verdict distinguishes key rejected from enrollment required from provider rejected. Config checks alone cannot see a rejected upstream credential, so an all-clear with --no-verify means "nothing looks misconfigured", not "this works". The report carries config metadata only: versions, gateway origin, per-tool pointing state, cert validity window, exported variable names, key presence; never the key, the cert signature, or any prompt content.

Endpoint security quarantined the key-renewal schedule

doctor reports the key-renewal schedule enabled but missing on a machine whose gateway, enrollment, and personal key all read clean; tools may also be left unconfigured because the install was killed partway.

That is an EDR product acting on a persistence heuristic (SentinelOne's persistence_deception, and equivalents in CrowdStrike and Defender): a login item that restarts itself is the shape an implant uses. Recent doctor builds name it under Endpoint security with the quarantined files. Re-running the installer writes the same login item back into quarantine, and signing does not clear it (the heuristic reads behavior). Unblock the seat first, then allowlist:

Unblock now, then allow-list
cd ~ && anyray-connect --no-key-refresh # no background service at all
anyray-connect doctor --security-review # the sheet for your security team

--no-key-refresh points tools straight at the gateway, so optimization, metering, and spend attribution are unchanged; the only cost is a re-run when the short-lived key lapses.

Connect installs the scheduler only where keys rotate faster than a week, or where no configured tool triggers renewal itself. A seat enrolled before that check may still carry one, and Connect now retires it on the next background tick, so an alert predating the upgrade clears itself. Once the paths are allowlisted, anyray-connect --yes restores renewal on a seat that needs it; on one that does not, it installs nothing and doctor reports Retired. Approve by path and service name: a file-or-hash approval does not carry, because the binary's hash changes each release. Full footprint: Key renewal.

Certificate errors, or could not reach the control plane, behind a TLS-inspecting proxy

npx anyray-connect@latest … never reaches the registry: npm exits with UNABLE_TO_GET_ISSUER_CERT_LOCALLY (or SELF_SIGNED_CERT_IN_CHAIN) against registry.npmjs.org, on a machine whose browser opens that URL fine.

That is a TLS-inspecting corporate proxy: it re-signs traffic with a private root CA your MDM installed in the OS trust store. Browsers and curl read that store; Node ships its own CA list and never reads the OS store, so every Node program (any npm install) fails identically.

Unblock now with the standalone binary channel: no Node, and it reads the OS trust store in every process it starts. Every flag passes through (doctor, migrate, a plain gateway URL):

macOS / Linux
curl -fsSL https://app.anyray.ai/connect.sh | sh -s -- --sso https://app.anyray.ai/sso/<tenantId> --yes

Older standalone binaries carried only a bundled CA list: the download succeeded, then the very next step failed with could not reach the control plane at https://app.anyray.ai. That split (curl and the binary disagreeing about the same host seconds apart) is the fingerprint; reinstall with the line above, because the self-updater's check fails the same way.

Then fix Node: Claude Code, the Codex CLI, npm, and the Anthropic and OpenAI SDKs all carry Node's CA list, and the moment inspection covers your gateway host they fail the same way, naming your gateway instead of the registry. Export the proxy's root CA to a PEM and point NODE_EXTRA_CA_CERTS at it; the variable adds to the built-in list, so public certificates keep verifying:

macOS / Linux
# macOS: MDM-installed roots live in the System keychain
security find-certificate -a -p /Library/Keychains/System.keychain > ~/corp-ca.pem
echo 'export NODE_EXTRA_CA_CERTS="$HOME/corp-ca.pem"' >> ~/.zshrc
export NODE_EXTRA_CA_CERTS="$HOME/corp-ca.pem"

Your security team can usually name the PEM (most proxy vendors publish one); a Connect run never installs a CA or changes proxy settings itself. Re-run anyray-connect doctor in a new shell: open terminals and already-started background services keep the old environment.

Never disable certificate verification to get past this

npm config set strict-ssl false, NODE_TLS_REJECT_UNAUTHORIZED=0, and per-tool equivalents turn off verification for every connection that process makes, including package downloads, precisely the channel a supply-chain attack uses. Trusting the CA your organization already installed costs the same effort.

The other half of the fix is an allowlist

Adding the CA solves trust, not reachability. If the proxy blocks hosts outright, ask for registry.npmjs.org (npm), github.com plus release-assets.githubusercontent.com (the binary channel redirects from the first to the second), app.anyray.ai (enrollment and metering), and your own gateway origin. anyray-connect doctor --security-review prints the same hosts with exact URLs and paths.

The installer stops mid-download with a connection error

The one-liner downloads a 100 MB binary. On a proxied, VPN'd or otherwise unreliable link the connection can drop part-way through, and the installer stops before it ever enrolls:

Windows PowerShell
anyray-connect: downloading anyray-connect-windows-x64.exe...
Invoke-WebRequest : The request was aborted: The connection was closed unexpectedly.

On macOS and Linux the same drop reads curl: (18) transfer closed with N bytes remaining to read.

Re-run the same command. Current installers resume from the bytes already downloaded and print transfer interrupted at … resuming (attempt 2 of 4), so a drop costs seconds rather than the whole download. Nothing unverified ever runs: the binary is checked against the release SHA-256 either way, and a truncated or spliced file is refused.

If every attempt dies at the same point, a proxy is cutting the transfer rather than the network. Allowlist github.com and release-assets.githubusercontent.com (the download redirects from the first to the second), or install through npm, which fetches in smaller pieces:

npm fallback
npx anyray-connect@latest --enroll <your-enrollment-link> --yes
anyray-connect login cannot start or expires

Run the current Connect version and name the same public gateway origin your tools use:

Start a fresh self-service SSO login
npx anyray-connect@latest login --gateway https://<your-anyray-gateway>
What you seeCause and fix
Could not start SSO loginThe gateway is not connected to the Billing app, SSO is not configured for that tenant, or the shared spend database is unavailable. Ask an operator to check /admin/health, the adt_ connection, and the SSO setup.
Login expiredThe browser flow exceeded ten minutes. Start again; the code and poll secret are one-session capabilities.
Multiple teams are available (non-interactive shell)Pass --team <name>; the gateway accepts only a team returned by the verified identity policy.
The gateway rejected this login sessionThe poll secret did not match or the session was already consumed. Start again; never put the poll secret in a URL or support report.
anyray-connect finishes but the verify step returns HTTP 404

connect wrote the config, then its own smoke test came back 404: an enrollment link was used as a gateway URL, so the tools point at …/sso/<tenantId>/v1/…, an enrollment path that serves no inference. Two causes:

  • A pinned, outdated connect that predates the --sso flag, so the link fell through to the gateway slot. Always run the current version (connect.sh or npx anyray-connect@latest).
  • The link passed to --gateway. An /sso/… or /enroll/… link goes to --sso (SSO) or --enroll (invite link); connect resolves the real gateway URL from the link itself.

Re-run with the link in the right slot; enrollment runs, mints the key, and the verify step returns 200:

macOS / Linux
curl -fsSL https://app.anyray.ai/connect.sh | sh -s -- --sso https://app.anyray.ai/sso/<tenantId> --yes --force
Enrollment succeeded but Claude Code still talks to Bedrock or Vertex directly

CLAUDE_CODE_USE_BEDROCK (or CLAUDE_CODE_USE_VERTEX) puts Claude Code in direct-cloud mode: it signs requests to AWS/GCP itself and never reads the ANTHROPIC_BASE_URL connect writes, so enrollment "succeeds" while nothing reaches the gateway.

connect repairs the sources it can own: at apply time (and on doctor --repair) it pins the switch to '' in the ~/.claude/settings.json env block, Claude Code's documented unset, which outranks a shell export. Restart Claude Code and the route engages; the pin drops once the original export is gone. The one source connect never fights is the machine's managed-settings.json (IT policy): apply prints a leading WARNING:, doctor reports the routing as bypassed, and the admin who deploys that file removes the switch.

To keep serving the same models from AWS, configure the gateway's bedrock provider: the gateway signs SigV4 with the org credential, and per-developer attribution comes from the enrolled key.

Enrollment succeeded but a managed-settings.json overrides the route

managed-settings.json outranks every other Claude Code scope (CLI args, project settings, the ~/.claude/settings.json connect writes, shell exports), so an MDM file carrying its own ANTHROPIC_BASE_URL wins while apply reports success and user scope reads back healthy.

anyray-connect doctor flags any key connect owns that the managed file overrides, naming the key, the managed file's path, and, for the base URL, the origin requests actually reach (reduced to scheme, host, and port; values are never printed).

doctor --repair leaves it alone: the file is IT policy on an admin-owned path. Two endings: the admin drops the key so per-user config governs, or, on a centrally managed fleet, deploys Anyray's own block with anyray-connect managed, the stronger setup (it survives a developer editing their own settings). Once both scopes name the same gateway, doctor goes quiet.

A SCIM-provisioned user's key returns user_deactivated

The key is valid, but its bound email is inactive in the durable SCIM identity store. Confirm the IdP user is active, then inspect with GET /scim/v2/Users?filter=userName eq "dev@example.com" using the SCIM bearer. Reactivate the user in the IdP so it sends active:true; do not mint a new key, because verification blocks every key bound to that inactive email.

If multiple users fail at once, check the spend leg on /admin/health: SCIM access reads fail closed when the shared Postgres store is unavailable, so restoring ANYRAY_SPEND_DB_URL connectivity restores verification without changing keys.

Send Anyray a support bundle

When the checks above don't explain it, /admin/support/bundle assembles a one-shot diagnostic snapshot to send to Anyray support:

Generate a support bundle
curl -fsS "https://<your-gateway>/admin/support/bundle" \
-H "Authorization: Bearer $ANYRAY_ADMIN_TOKEN" \
-o anyray-support-bundle.json
SectionWhat it carries
gatewayGateway version, product release, Node runtime, platform, uptime, memory use
healthThe same per-leg probes as /admin/health, captured at bundle time
configEvery set ANYRAY_* variable: plain values verbatim, URLs stripped to scheme://host, secrets shown as [set]
optimizerWhether an optimizer is wired up, and its live strategy configuration
telemetryRequest counts by status class and route since boot, plus the last 100 error events (route, status, error class, duration)
manifestThe section names, generation time, and approximate serialized size shown before send
Redacted by construction

Secrets are redacted to presence ([set]), connection URLs lose their credentials, and errors are recorded as class names, never messages. The console never sends the bundle. It builds it, shows it to you, and you copy it into your own support thread.

1
Reproduce

Trigger the failing request once if you can, so the error lands in the telemetry ring.

2
Generate and review

Open the account menu, select Diagnostics, then Generate & review, and read the JSON. Generating needs config:read and sends nothing; the curl above produces the same local artifact.

3
Send it yourself

Select Copy, then paste the JSON into your support thread. The console does not send it for you, so nothing leaves your deployment until you paste it.