Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

When a provider won't connect

Work from the symptom you have to the field that explains it and the config key that fixes it.

How-to guideLast updated

On this page

You have a symptom and not much else: a 401 from somebody, a row with no cost on it, or traffic going somewhere you did not configure. This page starts from those, not from the model.

Run the three checks first. Most provider problems are caught by one of them before a request is ever sent, and they take a few seconds together.

$ tokentriage validate --config tokentriage.yaml   # shape, params, auth pairing
$ tokentriage doctor --config tokentriage.yaml     # variables actually set, ports, pricing
$ curl -s localhost:9090/tt/api/status             # a running instance's view

Start from what you have

You are looking at Go to
a validation error config that will not load
validate turning away two settings that are each legal two features that will not load together
401 or 403 from the provider credential problems
404 from the provider url problems
HTTP 200 and no cost on the row a row with no dollar
two surfaces disagreeing about how many rows are unpriced a row with no dollar
a stream that stopped early, or one upstream 400ing on every stream when a stream misbehaves
traffic reaching an upstream you did not pick routing surprises
everything intermittently failing cooldown and reachability

Config that will not load

Validation errors name the YAML path, so read the path before the prose.

Message contains Cause Fix
does not name a known provider the descriptor id is not in the registry check the id in the reference
base_url: must not be empty provider: set without a host descriptors carry relative paths only
is not a template variable of provider a params: key the descriptor does not use the message lists the allowed set
requires template variable … (missing) a required params: key absent supply it
params.model or params.region putting either in params: model is per-tier; region has its own field
requires a region a Bedrock upstream with no region: add it; it signs and prices
the multi-part auth.* fields are not valid here an auth: block on bearer or header use api_key_env
this looks like a secret value a key literal in a *_env field put the variable name there
unknown variable {…} at offset an inline paths template only six variables are allowed
REFUSED while routing.allow_cross_dialect_streaming two features that will not run together flip one of them

Two features that will not load together

Stream-phase guardrail inspection and translated streams cannot both be armed in this build, and the config is turned away at validation rather than at the first request. Arm the guardrail layer, give any guard the stream phase, set routing.allow_cross_dialect_streaming: true, and validate exits 1:

$ tokentriage validate --config t7-guard-stream-vs-translated-stream.yaml
Error: configuration is invalid: config D:/tt-w01-neutral/work/t7-guard-stream-vs-translated-stream.yaml: guardrails.guards[0].phases[1]: guard "pii-stream" inspects at the "stream" phase, which is REFUSED while routing.allow_cross_dialect_streaming is true (BB14). The stream-framing gate supports exactly (openai, chat) and is armed from the ROUTE before the first byte relays (internal/proxy/stream_framing.go:34-97,127-129); it assumes streams never cross dialects, so on a translated stream it would arm an inspector over frames it cannot decode and produce a "guarded, clean" row. Turn off one of the two: drop the "stream" phase from this guard, or set routing.allow_cross_dialect_streaming: false

The absolute path is the capture machine’s and yours carries your own; the array index and the guard id belong to the config that was written to trip the check; the sentence is the binary’s. The second copy the binary prints is cut here, for the reason the note above gives. Grep your terminal for REFUSED while routing.allow_cross_dialect_streaming and you have found this.

Either single flip loads clean, and the two are alternatives rather than a ranked pair. Each was run through validate as a one-line edit of the config that produced the refusal above, and each printed OK: configuration is valid.

# A: keep stream-phase inspection, turn translated streams off.
routing:
  allow_cross_dialect_streaming: false

# B: keep translated streams, take the stream phase off the guard.
guardrails:
  guards:
    - id: pii-stream
      phases: [input]

A guard with no stream phase does not trip the check, and neither does a guardrails.enabled: false layer. The check reads the config rather than your traffic, so it is wider than the conflict it protects against, which is the safe direction for a gate that fails closed. Translated streams has the reason it is drawn that wide.

Credential problems

Work down this list; the first two are the answer most of the time.

The variable is not set. doctor reports it by name, and the boot banner prints a line at startup:

! keys — 1 upstream(s), 1 *_env reference(s): 1 unset (GROQ_API_KEY) — missing_env
    set: export GROQ_API_KEY=…   (referenced by an upstream but unset)

With the variable unset, requests are forwarded carrying whatever credential the client sent. A client that sends nothing gets the provider’s own 401, which is why this looks like an authentication bug rather than a configuration one.

The variable is set but empty. Counted as unset. Reporting a set-but-empty variable as clean would hide the case the check exists to find.

A multi-part credential is unset. The boot banner covers api_key_env only. An upstream whose credential arrives through auth.credentials_file_env, the AWS pair or auth.private_key_file_env boots silently. doctor’s keys line counts five of the six credential fields and leaves auth.private_key_file_env out, so for a key-file upstream the status payload is the surface that answers:

"vertex": { "auth_scheme": "gcp_oauth",
            "auth_health": { "configured": false, "fresh": false,
                             "state": "missing_env" } }

The credential file is unreadable. state: unreadable_credentials means the path resolved and the file did not parse. Check what the variable resolves to: it holds a path, not a key. A passphrase-encrypted private key lands here too, in both PEM spellings, because neither key-file scheme takes a passphrase. Generate the key with -nocrypt and point the variable at that file.

A token expired. refresh_failed means the last good material is still serving and a refresh is failing; expired means it lapsed. Both point at provider reachability or clock skew.

URL problems

A 404 from the provider is nearly always the path rather than the host.

  • Azure: the deployment in params.deployment must exist on that resource. The path carries it; the body carries the model.
  • Vertex: params.location must be a location that serves your model, and it appears in both the host and the path.
  • OpenAI-compatible vendors: check whether the vendor’s base URL already ends in /v1. Doubling it is the most common paste error.
  • A vendor whose path is not the driver’s default: declare it under an inline provider’s paths. See adding a provider.

A row with no dollar

HTTP 200 with no cost is the symptom most often mistaken for breakage, and there are three distinct causes with three different fields.

unpriced: true with a reason. Read cost.unpriced_reason. Six values are defined, in two families that want different things from you.

Reason Family First thing to check
model_unknown pricing miss is the upstream naming the descriptor you meant?
region_unknown pricing miss is your region outside us-, eu- and ap-?
endpoint_unpriced pricing miss is the model priced for the endpoint you called?
unit_missing pricing miss did an image call carry a size and quality?
settlement_pending async settlement nothing yet; the provider has not reported the money
settlement_terminal async settlement the job ended without a priceable result

The four pricing misses mean a lookup ran and found nothing usable, and each is a work item for a catalog or a config. The two settlement reasons mean there is no price to look up: not yet, or not at all. No pricing action clears either, and folding them in with the first four reports a catalog gap that does not exist. Reading a settled cost is the page for a pending row you are trying to add up, and providers that take a job and answer later is why the row is written that way in the first place.

No unpriced flag, empty pricing_key, a descriptor@ snapshot. A declared-free upstream. Working as configured.

No cost fields at all. The request took the passthrough path, because its endpoint kind is not one this descriptor declares. Check http.endpoint on the row.

Two unpriced counts that disagree. The Requests list and the Cost view can report different unpriced totals over the same ledger and the same window. The list counts each row as it was written and does not fold a settlement into it, so a job that has settled still reads UNPRICED there while its own record drawer shows the dollar. The folded count is the one to believe. No dashboard surface renders a settlement figure at all, so a pending-settlement diagnosis is a command-line job: tokentriage top prints the exclusion and the pending count as two lines under its table.

The pricing-key page has the full walk and the diagnosis table.

When a stream misbehaves

It stopped early and the last thing through was an error. On a translated stream, a frame the codec cannot encode ends the stream: your client receives an error frame in its own dialect, no terminating [DONE] arrives, finish_reason is empty, and the row carries the same string in outcome.error. That is a named case with a capture behind it, in translated streams.

One upstream 400s on every streamed chat request. On a streaming chat request to an OpenAI-dialect upstream, the gateway adds stream_options.include_usage: true to the outbound body and strips the resulting usage chunk from your copy if you did not ask for it. That is how a streamed row carries provider-reported usage instead of an estimate, and it lands on every upstream riding that driver rather than on a chosen list. An upstream that rejects the field therefore fails every streamed chat request, in a way that reads as the gateway’s fault. Turn it off with ledger.usage_injection: false and note that the switch is global: rows on every OpenAI-dialect upstream then price from an estimated token count, with usage.source saying so. Oracle Cloud is the one upstream where this is a watch item rather than a settled question, because its published documentation says nothing about the field.

Routing surprises

A tier is never chosen. If its upstream speaks a different dialect than the request and routing.allow_cross_dialect is off, the tier is skipped and the request escalates to the same-dialect default. That is the default configuration.

A cross-dialect tier is chosen and the body is not translated. The target upstream needs both provider: and dialect: named. With provider: alone the request is forwarded untranslated; the cross-dialect page shows both captures, and the dialect matrix is the table of pairs that can translate at all.

decision.fail_safe: true. Something degraded to a passthrough rather than failing: a model rewrite that could not be applied, a translation that hit an unmodelled parameter, a tier naming an upstream that no longer exists.

Cooldown and reachability

Nothing here probes your providers. Reachability is inferred from requests that actually failed, and it surfaces as one boolean per upstream:

"openai": {
  "base_url": "https://api.openai.com",
  "dialect": "",
  "in_cooldown": false,
  "auth_scheme": "bearer",
  "auth_health": {
    "configured": true,
    "fresh": true,
    "state": "ok"
  }
}

That entry came off GET /tt/api/status on the admin port, re-indented from the single line curl returns. The upstream name and its base_url are yours; the field set is the payload’s.

in_cooldown: true means routing is currently avoiding that upstream after failures. The policy that puts it there and takes it out belongs to routing limits.

Two things this payload does not tell you. It does not say whether the provider is up — only whether requests to it have been failing. And dialect reads empty for every upstream configured with provider:, because the derived dialect is not written back into the config this payload reads. Use the descriptor id to know which wire you are on.

What /health does and does not mean

/health on the admin port answers OK while the process is running. It performs no provider check, no ledger check and no credential check, so a green /health alongside a failing provider is not a contradiction. For provider state, read /tt/api/status; for setup state, run doctor.

When it is not you

Some catalog prefixes cannot be reached from any configuration: a credential mechanism sitting outside the shipped authentication schemes, a wire with no primary documentation to build a mock against, or a bespoke contract with no verifiable shape. Submit-and-poll is no longer one of those blockers; the class that held it is closed, and providers that take a job and answer later is where those rows are explained. What is not covered lists every prefix with its blocker, so you can tell a misconfiguration from an absence in about a minute.