Skip to Content
These docs describe donkey-kit v0.1.3. Release notes
Troubleshooting

Live

Troubleshooting

Start with donkey doctor. It makes one real governed call and tells apart the failures that look the same from your code. The sections below explain each line it prints, then cover the problems doctor can’t see.

pip install "donkey-kit[cli]" donkey doctor # or: donkey doctor --model <model-id> --json

Reading donkey doctor

Each line starts with a level. Only [!!] makes doctor exit non-zero, so you can run it as a CI preflight.

GlyphLevelMeans
[ok]okThe check passed.
[!!]failThe check failed. The next line is the remediation text from the typed exception for that failure.
[i] infoContext, such as where an endpoint came from or the budget reading.
[--]skipNot checked, because an earlier check failed.

The lines, in the order they appear:

LineFails whenWhat to do
configA required LLM-proxy field is missing, or the proxy URL may not receive the configured credentials.The remediation lists every missing field and the env var for each. For the credentials case, see Which credentials a URL receives.
llm endpoint, control planeNever (info).Check the host and its source: env, project file, local overlay, user file or default. A value from an unexpected source usually means a stale config file.
plain httpNever (info). Shown only when DONKEY_ALLOW_HTTP is on.Turn it off outside local testing. See Endpoints must use https://.
gatewayNo HTTP response at all (DNS, refused connection, TLS, timeout), or an empty 404 with no proxy on the base path.See Wrong URL or no proxy.
credentialsThe gateway answered 401, or 403 with www-authenticate.See 401 and 403.
modelThe provider rejected the model (the model_not_found pass-through).Request the model from your platform team, or pass another one with --model.
responses, chat completionsThe route’s upstream does not serve /responses (an Azure OpenAI route). The run still exits 0 if /chat/completions answers.Call the route with client.chat.completions.create(...).
policyNever (info). A policy refused the probe itself, such as a spent budget.Read the typed refusal. See Typed refusals.
budgetNever (info).The reading is only as fresh as the last response, because the proxy has no budget-query endpoint. observed … ago says how old it is.

The CLI page has sample output for a healthy proxy and for an Azure OpenAI route.

Failures that look alike

Wrong URL or no proxy

SymptomCauseFix
GatewayUnavailable and doctor prints gateway unreachableDNS, a refused connection, TLS or a timeout. No HTTP response came back.Check the host in DONKEY_LLM_PROXY_URL, your network egress and any proxy between you and the gateway.
An empty 404, and doctor prints no proxy on this base pathThe gateway answered, but nothing is deployed at that path. Credentials and model were not checked.Check the path for a typo, remove a trailing /v1 (the governed proxy has no /v1 segment), and confirm the proxy is Active in API Manager.
A 404 that carries x-llm-proxy-* headersThe proxy routed the model, but the upstream does not serve the API you called.Use the API the upstream serves. On an Azure OpenAI route, use Chat Completions.

DONKEY_LLM_PROXY_URL is the proxy base with a trailing slash: https://<ingress-gw>/<instance>/. See Governed model access.

401 and 403

SymptomCauseFix
401 with www-authenticate: Client-ID-EnforcementThe gateway rejected the client_id / client_secret pair.Check DONKEY_LLM_PROXY_CLIENT_ID and DONKEY_LLM_PROXY_CLIENT_SECRET, and ask your platform team to confirm that this app has access to the proxy.
401 in jwt modeThe gateway rejected the JWT, even after one refresh.See A 401 in JWT mode.
403 with a nested type: "pii_detected"A PII policy blocked the content. This is PIIDetected, not an auth failure.Redact the values listed in .entities.
403 with a nested code: "agent_killed"The agent is quarantined. This is AgentKilled.Ask an administrator to restore it.
403 with an x-llm-proxy-<vendor>-…-action: reject headerA content-safety policy blocked the content. This is ContentSafetyBlocked.Revise the content listed in .categories.

The SDK reads the body and headers to pick the exception, so catch the typed exception rather than matching on the status code. The full list is in The rejection shapes classify() types.

429: budget or request rate

Both are 429, and neither is retried by the SDK.

SymptomExceptionFix
Empty body with x-token-limit, x-token-remaining and x-token-reset headersTokenBudgetExceededWait for .retry_after, or pace calls with donkey.budget.pace().
Body {"error":"Too Many Requests"} with x-ratelimit-* headers and no x-token-* headersRequestRateLimitExceededWait for .retry_after, or send fewer requests per window.

CrewAI retries any 429 three times inside its own code. See the CrewAI page.

5xx

UpstreamModelError is a provider failure. The SDK retries a 503, and retries a 502 or 504 on a model call only with retry_model_calls_on_gateway_errors. If it persists, the provider is down: retry later or fall back.

donkey.last_call says UNAVAILABLE

UNAVAILABLE means the SDK can’t see this call’s response, so the gateway’s metadata can’t be read. It does not mean the call failed.

donkey.last_call is filled in from the response as it passes through the SDK’s shared HTTP client. CrewAI builds its own client, so its calls never pass through. On a Donkey whose only resolved adapter is CrewAI, last_call reports UNAVAILABLE, and surface names the adapter.

statusMeansWhat to do
OBSERVEDA governed response filled in the record.Nothing.
UNOBSERVEDNo governed call has returned in this context yet.Read it after the call, in the same task or donkey.run() block.
UNAVAILABLEThe adapter named in surface sends outside the SDK.Use an adapter whose capabilities().observes_last_call is True (see the table), or read the gateway’s own logs.

See last_call fields and When last_call is unavailable.

A 401 in JWT mode

In jwt mode the SDK gets the token from the llm_auth provider you pass to Donkey(llm_auth=…). On a 401 from a model call, the async client calls invalidate() on that provider, sends the request once more with the new token, and raises AuthError if that fails too.

Work through these in order:

  1. No provider. Without llm_auth, every adapter’s connection_kwargs() raises ConfigError in jwt mode. Pass a provider.
  2. A blocking call. The blocking client (sync=True, or a framework’s invoke() / complete()) raises ConfigError in jwt mode, because the token is async-only. Use the async API.
  3. The token never changes. StaticToken does not refresh, so the retry sends the same expired token. Use a provider that fetches a new token when invalidate() is called.
  4. The wallet client ID. DONKEY_LLM_PROXY_WALLET_CLIENT_ID is sent as X-Client-Id and selects the wallet. It is required in jwt mode and is not the IdP’s client ID.
  5. CrewAI. CrewAI builds its own client, so the JWT never reaches its requests and donkey.crewai raises ConfigError in jwt mode. Use client-id auth with CrewAI.

See JWT / model-wallet auth mode and Auth providers.

Still stuck? Run donkey --json doctor, remove any secrets from the output, and include it in an issue on GitHub .

Last updated on