Skip to Content
ReferenceConfiguration

Configuration

Donkey.from_env() builds its configuration from environment variables, then the config files, then defaults. Donkey(DonkeyConfig(...)) uses only what you pass. See Building a config for both paths, Precedence for the exact order, What the SDK sends where for which credential goes to which host, and Which credentials a URL receives for the rule that ties a URL from a config file to credentials from the same files.

Building a config

There are two ways to build a Donkey, and they resolve values differently:

You writeWhat it reads
Donkey.from_env(...) or Donkey()Environment variables, then the config files, then defaults. Donkey.from_env() takes only the cost tags (team, project, env, enduser_id) and on_model_substitution as kwargs, and applies them on top.
Donkey(DonkeyConfig(...))Only the values you pass. Every other field is its default. No environment variable and no config file is read.

Building DonkeyConfig(...) yourself does not fill the gaps from the environment. With DONKEY_TIMEOUT_S=5 set, Donkey(DonkeyConfig(llm_proxy_url=…)) still uses the default timeout_s of 60.0. To change a few fields and keep everything else from the environment and the files, resolve first and then override:

from donkey_kit import Donkey, DonkeyConfig donkey = Donkey( DonkeyConfig.from_env().with_overrides( telemetry_capture_content=True, registry_cache_ttl_s=60, ) )

DonkeyConfig.from_env() takes no arguments. with_overrides(...) accepts any DonkeyConfig field name, the names in the “DonkeyConfig field” columns below, and returns a new config.

Governed model access

The three required values for the LLM proxy:

Env varMeaning
DONKEY_LLM_PROXY_URLProxy base URL: https://<ingress-gw>/<instance>/ — no /v1. Must be https://; plain http:// is accepted for loopback hosts such as the local simulator, and for other hosts only with DONKEY_ALLOW_HTTP.
DONKEY_LLM_PROXY_CLIENT_IDConsumer client ID (the per-agent identity).
DONKEY_LLM_PROXY_CLIENT_SECRETConsumer client secret.

Auth is a client_id / client_secret header pair (consumer auth), not a bearer token, and separate from any Anypoint control-plane credential. The OpenAI SDK still requires a non-empty api_key slot, which the proxy ignores.

The stock gateway also accepts a single colon-joined header — authorization: Bearer <client_id>:<client_secret> or apikey: <client_id>:<client_secret> — which its dataweave-headers-transformation policy splits back into the pair. DDK doesn’t use that form: it always sends the two-header pair, because client_id is the per-agent attribution unit. The colon-joined value is not an alternative once a client_id header is present — the policy ignores it.

Missing required fields are reported all at once with their env-var names, so you can fix configuration in a single pass.

Optional env varDonkeyConfig fieldMeaning
DONKEY_LLM_PROXY_KEYllm_proxy_keyValue for the client’s API-key slot, sent to the LLM proxy: Authorization: Bearer <key> from OpenAI-compatible clients, x-api-key from the Anthropic client, x-goog-api-key from ADK’s gemini(). Unset, the SDK sends the placeholder client-id-enforced. A client-id proxy ignores this value, so leave it unset unless your proxy authenticates on an API key.

JWT / model-wallet auth mode

A model-wallet proxy identifies the caller from an IdP-issued JWT plus a durable wallet-selector client ID, with Client ID Enforcement disabled and no client_secret. Select it with DONKEY_LLM_PROXY_AUTH=jwt:

Env varDonkeyConfig fieldMeaning
DONKEY_LLM_PROXY_AUTHllm_proxy_authData-plane auth mode: client-id (default), jwt, or bearer.
DONKEY_LLM_PROXY_WALLET_CLIENT_IDllm_proxy_wallet_client_idThe wallet’s system-generated client ID, sent as the X-Client-Id header. Required in jwt mode.

In jwt mode the required fields are llm_proxy_url and llm_proxy_wallet_client_id — not client_id / client_secret. The rotating JWT is not a config value: supply it through an AuthProvider passed as Donkey(llm_auth=…), so the SDK can re-fetch it as it rotates and refresh it once on a 401:

from donkey_kit import Donkey, DonkeyConfig from donkey_kit.core.auth import StaticToken # or your own rotating AuthProvider donkey = Donkey( DonkeyConfig( llm_proxy_url="https://<ingress-gw>/<instance>/", llm_proxy_auth="jwt", llm_proxy_wallet_client_id="<wallet-client-id>", ), llm_auth=StaticToken("<jwt>"), # rides as Authorization: Bearer <jwt> )

jwt mode is async-only — the rotating credential is fetched from an async AuthProvider, so the blocking client (sync=True) is refused with an actionable error. Use client-id auth for a synchronous caller.

The JWT is attached only by the SDK’s shared async HTTP client, so it reaches the proxy only from async calls through adapters with transport injection built from a Donkey that has llm_auth: the raw client, LangGraph, Strands, OpenAI Agents SDK, the Anthropic SDK, ADK’s model() and gemini(), LlamaIndex and MS Agent Framework.

Sync calls through an adapter, such as LangGraph’s invoke() or LlamaIndex’s complete(), go through the blocking client, so they raise the same ConfigError as sync=True before sending anything.

These can’t send the JWT, so they raise ConfigError before sending anything, rather than sending the client-id-enforced placeholder as the bearer:

  • CrewAI, in jwt mode at all: its native OpenAI provider builds its own HTTP clients, which the SDK can’t add the JWT to. Use client-id auth with CrewAI;
  • every adapter on a Donkey without llm_auth, and the module-level factories (for example from donkey_kit.integrations.langgraph import chat_model), which have no llm_auth. Use Donkey(llm_auth=…) in jwt mode.

See Injection depth.

Bearer-token auth mode

A proxy that authenticates model calls on a plain bearer token, with no wallet selector and no client_id / client_secret pair, uses DONKEY_LLM_PROXY_AUTH=bearer. The only required field is llm_proxy_url. The token comes from an AuthProvider passed as Donkey(llm_auth=…):

from donkey_kit import Donkey, DonkeyConfig from donkey_kit.core.auth import StaticToken # or your own rotating AuthProvider donkey = Donkey( DonkeyConfig( llm_proxy_url="https://<ingress-gw>/<instance>/", llm_proxy_auth="bearer", ), llm_auth=StaticToken("<token>"), # rides as Authorization: Bearer <token> )

The SDK’s shared client adds Authorization: Bearer <token> to every model call, replacing whatever the framework client put there. It sends no X-Client-Id and no client_id / client_secret. This is the way to send a bearer token from the Anthropic client and ADK’s gemini(), which have no OpenAI-style API-key slot. A 401 refreshes the token once, and the endpoint rule covers it.

bearer mode has the same reach as jwt mode: async calls through the raw client, LangGraph, Strands, OpenAI Agents SDK, the Anthropic SDK, ADK’s model() and gemini(), LlamaIndex and MS Agent Framework, on a Donkey built with llm_auth. The forms that can’t carry the token raise ConfigError instead of sending an unauthenticated request: CrewAI’s connection_kwargs() and llm(), the blocking client (sync=True), sync calls through an adapter such as LangGraph’s invoke() or LlamaIndex’s complete(), and the module-level factories, which have no llm_auth.

Auth providers

An AuthProvider (in donkey_kit.core.auth) is any object with two async methods: token() returns the current credential and invalidate() drops a cached one.

Each provider belongs to one credential plane:

  • Donkey(llm_auth=…) supplies the LLM-proxy credential. It is used only in jwt and bearer mode.
  • Donkey(auth=…) supplies the Anypoint control-plane credential. By default it is an AnypointConnectedApp built from ANYPOINT_CLIENT_ID / ANYPOINT_CLIENT_SECRET. It is used only for control-plane calls, never for model calls. Its token counts as set in code for the endpoint rule.

What happens on a 401:

RequestOn 401
Model call, client-id modeRaises AuthError straight away. There is no token to refresh.
Model call, jwt or bearer modeCalls invalidate() on the llm_auth provider, re-sends once with the new token, then raises AuthError.
Control-plane callCalls invalidate() on the auth provider, re-sends once, then raises AuthError.
Blocking client (sync=True)Raises AuthError straight away.
ProviderUse it for
StaticToken(token)A token injected out-of-band, for example from CI. Never refreshes.
AnypointConnectedApp(client_id=…, client_secret=…, control_plane_url=…, http_client=…)OAuth2 client credentials against the Anypoint token endpoint. Caches the token in memory and refreshes it 60 seconds before expiry. The token endpoint must be https:// (loopback excepted, or any host with DONKEY_ALLOW_HTTP). AnypointConnectedApp.from_config(config, http_client=…) builds one from a DonkeyConfig and also applies the endpoint check before the first token request.
ChainedAuth(*providers)Tries providers in order; the first that yields a token wins.

For a rotating JWT from your IdP, implement the two methods yourself and pass the object as llm_auth.

Optional attribution

Env varDonkeyConfig fieldSent as
DONKEY_APP_NAMEapplication_nameThe X-Anypoint-Client-Application request header.
DONKEY_BUSINESS_GROUPbusiness_groupThe X-Anypoint-Business-Group request header.

These two values go only on request headers. No span attribute carries them. The gateway attributes traffic to the client_id credential (or, in jwt mode, the JWT’s client_id claim), so you don’t need either value for attribution.

Both header names are unconfirmed guesses. The gateway hasn’t been seen reading them (see §3 of the verification ledger ). The first time the SDK sends one, it emits an UnverifiedValueWarning. Unlike the correlation and cost-tag headers, these names have no config key, so you can’t override them. To stop the warning, leave both values unset.

Correlation headers

Per-call and per-run correlation IDs ride on request headers. The IDs also appear on spans and exceptions regardless of the header names. See Telemetry & cost.

  • X-Correlation-Id carries the run ID. The gateway reads this header and echoes it on the response x-correlation-id, so a client log line joins to the gateway’s record.
  • X-Donkey-Request-Id carries the per-call ID. The name is the SDK’s own convention, and the gateway doesn’t read it.

Both are confirmed names, so neither emits a warning. Override one only if a proxy of your own in front of the gateway expects a different name:

Env varDonkeyConfig fieldDefaultMeaning
DONKEY_CORRELATION_HEADERcorrelation_headerX-Correlation-IdRequest header that carries the per-run correlation ID.
DONKEY_CALL_ID_HEADERcall_id_headerX-Donkey-Request-IdRequest header that carries the per-call ID.

Header names you can’t use

correlation_header, call_id_header and the four cost_*_header keys (Cost-attribution tags) are checked whenever a config is built, whether from the environment, a file or code. Names compare case-insensitively. A name must start with X- (all the defaults do), and a key can’t name:

Header namesWhy
Host, Forwarded, any X-Forwarded-*, X-Real-IP, Content-Length, Transfer-Encoding, Connection, Upgrade, TE, Trailer, ExpectThey decide where the request goes or how its body is framed.
X-HTTP-Method-Override, X-HTTP-Method, X-Method-Override, X-Original-URL, X-Original-URI, X-Rewrite-URLSome servers and gateways read them to change the request’s method or path.
Authorization, Proxy-Authorization, Cookie, x-api-key, api-key, apikey, api_key, x-goog-api-key, client_id, client_secret, X-Client-IdThey carry or select credentials.
X-Anypoint-Client-Application, X-Anypoint-Business-Group, the x-cache-* headers, any x-stainless-*, Content-Type, Accept, Accept-Encoding, User-AgentThe SDK, its HTTP client or a framework’s SDK already sets them.
A name another of these keys already uses, including its defaultTwo values would share one header.
Anything that isn’t a valid HTTP header name (for example, one with a space)It can’t be sent.
Any other name that doesn’t start with X- (for example Team, Origin, Range)Standard and framework headers (Content-Encoding, Via, anthropic-version, …) change how the request is handled.
ConfigError: cost_team_header names the header 'Host', which can't be used: it controls where the request goes or how it is framed. cost_team_header is set in the environment (DONKEY_COST_TEAM_HEADER). Choose a different header name.

Cost-attribution tags

A fixed set of dimensions set once and emitted on every call as donkey.cost.* span attributes (and, only if you opt in, as request headers). Override them per run with donkey.run(team=…, project=…, env=…, enduser_id=…). The key set is fixed — an unknown dimension is a configuration error, not a silently dropped tag. See Telemetry & cost.

Env varDonkey.from_env kwargMeaning
DONKEY_COST_TEAMteamOwning team.
DONKEY_COST_PROJECTprojectProject / workload.
DONKEY_COST_ENVenvDeployment environment (e.g. prod).
DONKEY_COST_ENDUSER_IDenduser_idEnd-user ID (the enduser.id tag).

In a config file these live under a [donkey.cost] table (the end-user dimension keeps its dotted key):

[donkey.cost] team = "support" project = "triage-v2" env = "prod" "enduser.id" = "user-42"

A [donkey.cost] table in .donkey-kit.local.toml merges into the one in .donkey-kit.toml key by key: a local "enduser.id" adds to the project file’s team and project instead of replacing them. DONKEY_COST_* variables then override single dimensions.

The LLM Gateway doesn’t read cost tags from request headers, so by default DDK sends no cost-tag header. The donkey.cost.* span attributes carry every tag either way. To also send the tags as request headers, for example to a proxy of your own that reads them, opt in:

Env varDonkeyConfig fieldDefaultMeaning
DONKEY_SEND_COST_HEADERSsend_cost_headersfalseAlso send the cost tags as request headers.

With it enabled, the headers use the SDK’s own names (X-Anypoint-Cost-Team, …). Because no gateway reads them, they’re a convention rather than a placeholder for some real name, and they emit no warning. Rename them to match whatever receiver you send them to: DONKEY_COST_TEAM_HEADER, DONKEY_COST_PROJECT_HEADER, DONKEY_COST_ENV_HEADER, DONKEY_COST_ENDUSER_HEADER (or the matching cost_*_header config keys), within the names you can’t use. send_cost_headers is not a Donkey.from_env kwarg; in code, use DonkeyConfig.from_env().with_overrides(send_cost_headers=True).

With send_cost_headers on, the headers, including the end-user ID, go only on model requests to the LLM proxy, never to the Anypoint platform. Where the tags go is described in Telemetry & cost.

Telemetry

Env varDonkeyConfig fieldMeaning
DONKEY_TELEMETRYtelemetryEmit OTel spans at all (default true).
DONKEY_TELEMETRY_CAPTURE_CONTENTtelemetry_capture_contentPut prompt/completion text on spans (default false).

telemetry_capture_content defaults to false on purpose: spans are emitted inside your process, upstream of the gateway’s PII masking, so capturing content re-exports the very text the platform masks. Enable it only for a trusted collector. See Telemetry & cost.

Behaviour

Env varDonkeyConfig fieldDefaultMeaning
DONKEY_TIMEOUT_Stimeout_s60.0HTTP timeout for governed calls, in seconds.
DONKEY_MAX_RETRIESmax_retries3Retries for transient upstream failures (502 / 503 / 504) with backoff. Policy refusals are never retried, and a gateway fallback is never retried twice.
DONKEY_ON_MODEL_SUBSTITUTIONon_model_substitutionoffoff surfaces a model substitution on donkey.last_call; raise turns it into ModelSubstituted. See Telemetry & cost.
DONKEY_REGISTRY_CACHE_TTL_Sregistry_cache_ttl_s300How long registry lookups (used by tool access) are cached in memory, in seconds.
DONKEY_NO_CACHE—unsetSet to 1, true or yes to bypass that in-memory registry cache.
DONKEY_TRUST_PROJECT_CONFIG—unsetSet to 1 (or true, yes, on) to let a URL from the working directory’s config files receive credentials from elsewhere. Read only from the environment. See Which credentials a URL receives.
DONKEY_ALLOW_HTTP—unsetSet to 1 (or true, yes, on) to allow plain http:// endpoints on non-loopback hosts. Read only from the environment. See Endpoints must use https://.

Invalid values

Every DonkeyConfig is checked when it is built, including by with_overrides(), and an invalid value raises ConfigError before any request is sent. One error lists every bad field and where each was set:

FieldAccepted
timeout_sA number greater than 0
max_retries, registry_cache_ttl_sA whole number, 0 or more
telemetry, telemetry_capture_content, send_cost_headersTrue or False in code; 1, true, yes, on, 0, false, no or off (any case) in the environment or a config file
regionus, eu, ca or jp
llm_proxy_authclient-id or jwt (any case in the environment or a config file)
on_model_substitutionoff or raise (any case in the environment or a config file)

A misspelt switch such as DONKEY_TELEMETRY=flase is an error, not false:

ConfigError: Configuration is invalid: - timeout_s is 'abc', set in the environment (DONKEY_TIMEOUT_S); expected a number - telemetry is 'flase', set in the environment (DONKEY_TELEMETRY); expected one of 1, true, yes, on, 0, false, no, off - max_retries is -1, set in the environment (DONKEY_MAX_RETRIES); expected a whole number, 0 or more Fix each value where it is set: in code, an environment variable, or .donkey-kit.toml.

The environment-only switches above (DONKEY_NO_CACHE, DONKEY_TRUST_PROJECT_CONFIG, DONKEY_ALLOW_HTTP) are not checked: any value other than the ones listed leaves them off.

Anypoint control plane

A separate credential from the LLM proxy, for features that call the Anypoint platform: registry and tool discovery, publication and provisioning. The SDK exchanges ANYPOINT_CLIENT_ID / ANYPOINT_CLIENT_SECRET for a connected-app token at the Anypoint token endpoint and sends only that token to the platform. You don’t need these values for governed model access. Model calls never use them.

The features that call the Anypoint platform are Roadmap in this release and stop before sending anything. Which features use the control plane lists them.

Env varMeaning
ANYPOINT_CLIENT_IDConnected-app client ID.
ANYPOINT_CLIENT_SECRETConnected-app client secret.
ANYPOINT_ORG_IDAnypoint organization ID.
ANYPOINT_ENVAnypoint environment (default Sandbox).
ANYPOINT_REGIONControl-plane region: us (default), eu, ca, or jp.
ANYPOINT_BASE_URLExplicit control-plane base URL; overrides the region. Must be https:// (loopback hosts excepted, or any host with DONKEY_ALLOW_HTTP).

What the SDK sends where

Donkey keeps one HTTP client per credential plane, so a credential never rides the other plane’s requests:

  • Data plane: the LLM proxy at llm_proxy_url. Used by donkey.llm and every framework adapter.
  • Control plane: the Anypoint platform at base_url (default: the host for region). Used for the connected-app token request and for registry and tool discovery.

Credentials

DestinationClient-id mode (default)jwt modebearer modeNever sent here
LLM proxy (llm_proxy_url)client_id: <llm_proxy_client_id> and client_secret: <llm_proxy_client_secret> headers. The API-key slot1 carries llm_proxy_key, or the placeholder client-id-enforced.X-Client-Id: <llm_proxy_wallet_client_id> and Authorization: Bearer <JWT from llm_auth>2. No client_secret.Authorization: Bearer <token from llm_auth>2. The API-key slot1 as in client-id mode. No X-Client-Id, no client_secret.ANYPOINT_CLIENT_ID, ANYPOINT_CLIENT_SECRET, the connected-app token, anything from Donkey(auth=…)
Anypoint token endpoint (<base_url>/accounts/api/v2/oauth2/token)Form body: grant_type=client_credentials, client_id, client_secret (the ANYPOINT_* values).Same.Same.Every llm_proxy_* value, the JWT or bearer token, X-Client-Id
Registry and tools (base_url)Authorization: Bearer <connected-app token> (or the token from Donkey(auth=…)).Same. No X-Client-Id.Same.Every llm_proxy_* value, the JWT or bearer token, X-Client-Id

1 The framework client decides the header: Authorization: Bearer for OpenAI-compatible clients, x-api-key for the Anthropic client, x-goog-api-key for ADK’s gemini(). In jwt and bearer mode the SDK’s shared client replaces Authorization with the token from llm_auth.

2 Only from async calls through adapters with transport injection; see the jwt mode note and the bearer mode note.

Model calls never request, send or refresh the connected-app token.

Credentials go only to checked endpoints

Each of the SDK’s HTTP clients attaches credentials only to the endpoints it was checked for, compared by scheme, host and port:

  • the configured endpoint: llm_proxy_url on the data plane, base_url on the control plane;
  • a URL you pass to a factory in code: base_url to donkey.llm.client() or an adapter factory, api_base (LlamaIndex, ADK’s model(), CrewAI), openai_api_base (LangGraph), or client_args["base_url"] (Strands). It must pass the same https:// rule as the config, and then receives the same credentials.

A request to any other origin is still sent, but without credentials: the SDK adds none and removes the credential headers the framework set (Authorization, x-api-key, client_id, client_secret, X-Client-Id and the other names in What printed output hides). The correlation, attribution and other headers in the next table still go. This covers:

  • a URL changed outside the factories, for example ChatOpenAI(**{**kwargs, "base_url": other});
  • every redirect hop. The SDK’s clients don’t follow redirects; if your code asks for it on a request, a hop to another origin carries no credentials.

CrewAI builds its own HTTP client, so its connection_kwargs() includes an interceptor that removes the credential headers the same way.

Which features use the control plane

FeatureControl-plane requestsIn this release
Model calls: donkey.llm, donkey.openai(), every adapterNoneAvailable
donkey doctorNone. It checks the control-plane settings without sending, and makes one model call to the LLM proxy.Available
donkey init, donkey mock, donkey test, donkey.simulate()NoneAvailable
Registry: donkey.registry.search(), resolve_mcp(), resolve_agent(), explain(), warm()Token request, then Exchange readsRoadmap: raises NotImplementedError before sending
Tool discovery: donkey.tools.discover(), donkey.tools.lock()Token request, then Exchange readsRoadmap: raises NotImplementedError before sending
Publication and governance: Publication.preview() / verify(), Governance.resolve() / apply() / simulate()Token request, then platform callsRoadmap: raises NotImplementedError before sending
Hidden provisioning commands (donkey plan, apply, drift, lint, generate, status, publish, verify)Token request, then platform callsRoadmap: exits with “blocked on verification” before sending
Your own code calling AnypointConnectedApp.token()Token requestAvailable

So in this release the SDK sends the ANYPOINT_* credentials only if your own code asks for a connected-app token.

Other headers

The correlation and per-call IDs go on every request the SDK’s HTTP clients send, on both planes. The other headers go only on model requests to the LLM proxy: the connected-app token request and the registry and tool requests never carry them.

Header (default name)ValueWhenSent to
X-Correlation-Id (correlation_header)The run IDAlwaysBoth planes
X-Donkey-Request-Id (call_id_header)A per-call IDAlwaysBoth planes
X-Anypoint-Client-Applicationapplication_nameWhen setThe LLM proxy only
X-Anypoint-Business-Groupbusiness_groupWhen setThe LLM proxy only
X-Anypoint-Cost-Team, -Project, -Env, -Enduser-IdYour cost tags, including the end-user IDOnly with send_cost_headers; see Cost-attribution tagsThe LLM proxy only, or a receiver of yours in front of it
x-cache-*Your cache controls, including principal_idInside a donkey.cache(...) blockThe LLM proxy’s semantic cache only

CrewAI, which gets only a header snapshot, sends the auth headers, the attribution headers and, with send_cost_headers, the configured cost tags. It doesn’t send per-run values or cache controls.

Config files, secrets and trust

Instead of env vars you can put values in a [donkey] table. Keys are the DonkeyConfig field names.

FileWhereMeant for
.donkey-kit.tomlThe current working directory (not searched upward)Non-secret project config you commit: URLs, client IDs, attribution.
.donkey-kit.local.tomlThe current working directorySecrets and personal overrides. Keep it gitignored. Same keys. Read with or without .donkey-kit.toml.
$XDG_CONFIG_HOME/.donkey-kit.toml, or ~/.config/.donkey-kit.toml when XDG_CONFIG_HOME is unset or emptyYour user config directoryPersonal defaults for every project. Read only when the working directory has neither file above. When XDG_CONFIG_HOME is set, ~/.config isn’t read.

.donkey-kit.local.toml merges into .donkey-kit.toml key by key, and nested tables such as [donkey.cost] merge the same way. A scalar or an array in the local file replaces the project file’s value; it isn’t appended to. Each key remembers which file it came from.

Both working-directory files must be regular files, or links whose target is inside the working directory. A link that points elsewhere is refused, because its keys would be treated as the working directory’s:

ConfigError: /home/me/my-agent/.donkey-kit.toml links to /home/me/shared/donkey.toml, outside the working directory. Replace the link with a regular file in the working directory, or set those values in the environment.

The user file isn’t merged with the working-directory files. If either .donkey-kit.toml or .donkey-kit.local.toml exists, the user file is not read at all, even when only .donkey-kit.local.toml exists (#727 ).

Precedence

Highest first, per key:

  1. Values set in code: anything you change on a resolved config, with with_overrides(...), dataclasses.replace(...) or any other copy, and the cost-tag and on_model_substitution kwargs of Donkey.from_env(...).
  2. Environment variables.
  3. ./.donkey-kit.local.toml
  4. ./.donkey-kit.toml
  5. The user file ($XDG_CONFIG_HOME/.donkey-kit.toml, or ~/.config/.donkey-kit.toml), only when 3 and 4 don’t exist.
  6. Defaults.

A value counts as set in code once it differs from the value that was loaded, whichever way you changed it; a copy that keeps the value keeps its source. That includes DonkeyConfig(**dataclasses.asdict(cfg)) in the same process. A config rebuilt in another process, for example from a serialized asdict(), keeps where its URLs came from, but its credentials count as set in code. So a worker in another process (multiprocessing spawn, Celery, Ray) that receives a config with a URL from .donkey-kit.toml and credentials from .donkey-kit.local.toml refuses to send them; call DonkeyConfig.from_env() in the worker instead. DonkeyConfig(...) built directly reads no environment variable and no file: every value is the one you pass or the field default. donkey.run(...) cost tags apply on top of all of this for their block. DONKEY_TRUST_PROJECT_CONFIG and DONKEY_ALLOW_HTTP are read from the environment only.

# .donkey-kit.toml (committed) [donkey] llm_proxy_url = "https://<ingress-gw>/<instance>/" llm_proxy_client_id = "my-client-id"
# .donkey-kit.local.toml (gitignored) [donkey] llm_proxy_client_secret = "<secret>"

donkey init generates .donkey-kit.toml from your current environment and never writes a secret into it. If .donkey-kit.toml contains client_secret, llm_proxy_client_secret or llm_proxy_key, the SDK emits a ConfigWarning (donkey_kit.core.errors.ConfigWarning) pointing you to .donkey-kit.local.toml. Under python -W error or pytest’s filterwarnings = error, that warning is raised as an exception.

Which credentials a URL receives

Two checks run before the SDK sends a credential. Both raise ConfigError, and nothing is sent.

When they run: the LLM-proxy check runs when you build a client: donkey.llm.client(), donkey.openai(), any donkey.<framework> factory, or connection_kwargs(). Creating Donkey(...) doesn’t run it. The control-plane check runs at the first control-plane call and again right before each connected-app token request. donkey doctor runs both without sending anything.

Scope: the checks cover llm_proxy_url, base_url and the connected-app token endpoint. A URL you pass to a factory in code gets the https:// check when the factory is called; see Credentials go only to checked endpoints. They don’t cover OTLP exporter endpoints (configured by OpenTelemetry) or governance [targets.*].base_url profiles. No request goes to a target profile today, because resolve(), apply() and simulate() are not available yet (#832 ).

Endpoints must use https://

This applies wherever the URL comes from: environment, file or code.

URLResult
https://…Accepted
http:// on a loopback host: localhost, 127.0.0.0/8, ::1Accepted
http:// on any other host, including 0.0.0.0, LAN addresses and container hostnames such as http://simulator:8080ConfigError
http:// on any other host, with DONKEY_ALLOW_HTTP=1 in the environmentAccepted, with a ConfigWarning
Any other scheme, or a URL with no hostConfigError, whatever DONKEY_ALLOW_HTTP says
ConfigError: llm_proxy_url must be an https:// URL (got http scheme, llm.example.test). Plain http:// is accepted for loopback hosts (localhost, 127.0.0.0/8, ::1), such as the local gateway simulator. Change it to the https:// address of the service, or set DONKEY_ALLOW_HTTP=1 in the environment to allow plain http:// to other hosts.

DONKEY_ALLOW_HTTP (1, true, yes or on) is for networks you control, such as a simulator in another container or an in-cluster proxy without TLS. Credentials sent over plain http:// travel unencrypted, so don’t use it across a network you don’t trust. It is read from the environment only; a [donkey] key with that name is ignored. It changes only the https:// rule: a URL from the project files still receives only credentials from those files (next section). When it lets a URL through, the SDK emits a ConfigWarning naming the key (base_url, llm_proxy_url or token endpoint) and the host. Python shows each distinct warning once per location by default:

ConfigWarning: llm_proxy_url uses plain http:// to llm.example.test because DONKEY_ALLOW_HTTP=1 is set. Credentials and data sent to it are not encrypted in transit.

The warning quotes the value as you set it, for example DONKEY_ALLOW_HTTP=true is set. donkey doctor adds a plain http line while it is on. See CLI.

A URL from the project files gets credentials from the project files only

An endpoint URL read from ./.donkey-kit.toml or ./.donkey-kit.local.toml only receives credentials read from those two files. This applies to loopback URLs too: they are exempt from the https:// rule only.

These are the credentials checked for each endpoint:

EndpointCredentials checked
base_urlclient_id, client_secret, or the token from a Donkey(auth=…) provider, which always counts as coming from outside the files
llm_proxy_url, client-id modellm_proxy_client_id, llm_proxy_client_secret, llm_proxy_key (the ones that are set)
llm_proxy_url, jwt modellm_proxy_wallet_client_id, and the JWT from llm_auth, which always counts as coming from outside the files
llm_proxy_url, bearer modellm_proxy_key if set, and the token from llm_auth, which always counts as coming from outside the files
The URL comes fromA credential it would receive comes fromResult
Environment, user file, code, or the region defaultAnywhereSent
.donkey-kit.toml / .donkey-kit.local.toml.donkey-kit.toml / .donkey-kit.local.tomlSent
.donkey-kit.toml / .donkey-kit.local.tomlAn environment variableConfigError
.donkey-kit.toml / .donkey-kit.local.tomlThe user fileConfigError
.donkey-kit.toml / .donkey-kit.local.tomlCode (a value you set or changed on the config)ConfigError
.donkey-kit.toml / .donkey-kit.local.toml, jwt modeThe JWT from llm_auth (always)ConfigError, every time
.donkey-kit.toml / .donkey-kit.local.toml, bearer modeThe token from llm_auth (always)ConfigError, every time
.donkey-kit.toml / .donkey-kit.local.toml (base_url)The token from a Donkey(auth=…) provider (always)ConfigError, every time
.donkey-kit.toml / .donkey-kit.local.toml, base_url on anypoint.mulesoft.com, eu1.anypoint.mulesoft.com, ca1.anypoint.mulesoft.com or jp1.anypoint.mulesoft.comAnywhereSent
Any of the above, with DONKEY_TRUST_PROJECT_CONFIG set in the environmentAnywhereSent
Any of the above, with only DONKEY_ALLOW_HTTP setAs in the rows aboveUnchanged: DONKEY_ALLOW_HTTP affects the https:// rule only

What counts as code: any value passed to DonkeyConfig(...); any value on a resolved config that differs from what was loaded, whether you changed it with with_overrides(...), dataclasses.replace(...) or otherwise; the token from a Donkey(auth=…) provider; and in jwt and bearer mode the token returned by llm_auth. A config built entirely with DonkeyConfig(...) has its URLs in code too, so the rule never applies to it. Likewise, a URL you change in code is no longer treated as coming from the file.

The error names every credential that came from elsewhere, where each came from, the host, the key and the file:

ConfigError: Not sending llm_proxy_client_secret (from env) to llm-proxy.example.com: llm_proxy_url is set in /home/me/my-agent/.donkey-kit.toml, and credentials from outside the working directory's config files are only sent to hosts those files name when you opt in. To continue, do one of: - set the URL in the environment instead (DONKEY_LLM_PROXY_URL=https://...) - keep the credentials in /home/me/my-agent/.donkey-kit.local.toml, next to the project file - trust this directory's config files by setting DONKEY_TRUST_PROJECT_CONFIG=1

The source labels are env, user file and code.

Resolving it

  1. Set the URL in the environment (DONKEY_LLM_PROXY_URL / ANYPOINT_BASE_URL). The environment wins over the file, so the URL no longer comes from the file.
  2. Keep the credentials in .donkey-kit.local.toml in the same directory. Not available for the llm_auth token in jwt or bearer mode or for a Donkey(auth=…) token, because none of them comes from a file.
  3. Opt in: set DONKEY_TRUST_PROJECT_CONFIG to 1 (or true, yes, on) in the environment if you trust this directory’s config files. The setting decides whether those files are trusted, so it is read from the environment only; a [donkey] key with that name is ignored.

In jwt or bearer mode with the URL in a project file, and for a Donkey(auth=…) token with base_url in a project file, only 1 and 3 apply; the error lists only those two.

donkey doctor prints each endpoint’s host and where it came from (env, project file, local overlay, user file or default), and reports this error on its config line without sending a request. See CLI.

Programmatic

import dataclasses from donkey_kit import Donkey, DonkeyConfig # Entirely in code: no environment variable or config file is read. donkey = Donkey(DonkeyConfig( llm_proxy_url="https://<ingress-gw>/<instance>/", llm_proxy_client_id="my-client-id", llm_proxy_client_secret="<secret>", )) # Resolved from env and files, with code values on top. Either form records # the changed value as set in code: cfg = DonkeyConfig.from_env().with_overrides(send_cost_headers=True) cfg = dataclasses.replace(DonkeyConfig.from_env(), send_cost_headers=True) # Or from the environment, with lifecycle: async with Donkey.from_env() as donkey: ...

What printed output hides

The SDK keeps credential values out of repr() and str(), so printing, logging, a test-failure diff or a traceback with locals doesn’t show them. It only changes how objects are printed: every value is still there, and the SDK sends the real values.

ObjectHidden when printedStill shown
DonkeyConfigclient_secret, llm_proxy_client_secret and llm_proxy_key are left outEvery other field, including client IDs, llm_proxy_wallet_client_id, URLs and cost=CostTags(…, enduser_id=…)
Every adapter’s connection_kwargs(), ADK’s gemini_connection_kwargs(), donkey_kit.core.transport.proxy_auth_headers()The value under any key named api_key, apikey, api-key, x-api-key, x-goog-api-key, client_secret, authorization, proxy-authorization or cookie (any case), at any depth, shows as '***'base_url, client_id, X-Client-Id, attribution headers, and X-Anypoint-Cost-* headers including the end-user ID
PIIDetected (str, repr, .args)The flagged valuesEntity types, count and character offsets; see Errors

The masked mappings are ordinary dicts. The table shows how common operations behave:

OperationResult
**kwargs, kwargs["api_key"], ==, passing it to a frameworkReal values
.copy(), copy.copy, copy.deepcopy, kwargs | otherStill masked when printed
dict(kwargs), {**kwargs}, kwargs.items()Top level printed in full, including api_key. Nested header mappings stay masked.
json.dumps(kwargs)Writes the real values

Some framework objects built from the kwargs print credentials in their own repr() / str():

ObjectPrints
LangGraph ChatOpenAI (donkey.langgraph.chat_model())client_secret
LlamaIndex OpenAILike (donkey.llamaindex.llm())client_secret and api_key
CrewAI OpenAICompletion (donkey.crewai.llm())api_key

Don’t print or log these objects, and don’t leave them in locals that a traceback renders.

Not covered by the masking:

  • Attribute access and conversions: config.client_secret, dataclasses.asdict(config), vars(...). (Where each value came from is kept as a keyed digest, so asdict() holds each secret once, in its own field.)
  • Objects built from the kwargs, such as a framework client or the openai_client returned by the OpenAI Agents adapter, and whatever those objects log.
  • attribution_headers() and cost_headers(), which return plain dicts.
  • .response on any DonkeyError: exc.response.request.headers holds the real client_secret and Authorization headers.
  • PIIDetected.gateway_message and .response, which contain the flagged values.
  • UpstreamRequestError messages, which include the upstream provider’s own error text and can quote parts of your request.
  • OpenTelemetry spans: cost tags, including donkey.cost.enduser.id, and message content when telemetry_capture_content is on.
Last updated on