| description | Every DDK configuration value — LLM proxy URL and credentials, JWT model-wallet auth, attribution, cost tags, telemetry, and Anypoint control-plane settings — as env vars, config file, or code. |
|---|---|
| status | live |
import { Callout } from 'nextra/components' import { Badge } from '../../components'
Donkey.from_env(...) builds its configuration from the values you pass,
then 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.
There are two ways to build a Donkey, and they resolve values differently:
| You write | What it reads |
|---|---|
Donkey.from_env(...), Donkey() or DonkeyConfig.resolve(...) |
The keyword arguments you pass, then environment variables, then the config files, then defaults. Any DonkeyConfig field is a keyword argument, and so is path (a config file to read in place of ./.donkey-kit.toml). Donkey.from_env() also takes the cost-tag shorthands team, project, env and enduser_id. |
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, pass them to from_env or resolve:
from donkey_kit import Donkey, DonkeyConfig
donkey = Donkey.from_env(telemetry_capture_content=True, registry_cache_ttl_s=60)
# The same config, without building a Donkey:
cfg = DonkeyConfig.resolve(telemetry_capture_content=True, registry_cache_ttl_s=60)The keyword arguments are the DonkeyConfig field names, the names in the
"DonkeyConfig field" columns below. An unknown name raises TypeError, and
the values are checked like any other (see Invalid values).
DonkeyConfig.from_env() takes no arguments; it is DonkeyConfig.resolve().
with_overrides(...) accepts the same names on a config you already have, and
returns a new config.
{/* BEGIN GENERATED: settings-index (edit python/scripts/generate_docs.py, then run it) */}
Every DonkeyConfig field except cost (set per dimension, see
Cost-attribution tags), in the order the SDK reads
them. The config-file key goes in the [donkey] table of .donkey-kit.toml.
This table is generated from the field table in core/config.py.
| Env var | DonkeyConfig field |
Config-file key | Default |
|---|---|---|---|
ANYPOINT_CLIENT_ID |
client_id |
client_id |
unset |
ANYPOINT_CLIENT_SECRET |
client_secret |
client_secret |
unset |
ANYPOINT_ORG_ID |
org_id |
org_id |
unset |
ANYPOINT_ENV |
environment |
environment |
Sandbox |
ANYPOINT_REGION |
region |
region |
us |
ANYPOINT_BASE_URL |
base_url |
base_url |
from region |
DONKEY_LLM_PROXY_URL |
llm_proxy_url |
llm_proxy_url |
unset |
DONKEY_LLM_PROXY_CLIENT_ID |
llm_proxy_client_id |
llm_proxy_client_id |
unset |
DONKEY_LLM_PROXY_CLIENT_SECRET |
llm_proxy_client_secret |
llm_proxy_client_secret |
unset |
DONKEY_LLM_PROXY_KEY |
llm_proxy_key |
llm_proxy_key |
unset |
DONKEY_LLM_PROXY_AUTH |
llm_proxy_auth |
llm_proxy_auth |
client-id |
DONKEY_LLM_PROXY_WALLET_CLIENT_ID |
llm_proxy_wallet_client_id |
llm_proxy_wallet_client_id |
unset |
DONKEY_APP_NAME |
application_name |
application_name |
unset |
DONKEY_BUSINESS_GROUP |
business_group |
business_group |
unset |
DONKEY_CORRELATION_HEADER |
correlation_header |
correlation_header |
X-Correlation-Id |
DONKEY_CALL_ID_HEADER |
call_id_header |
call_id_header |
X-Donkey-Request-Id |
DONKEY_COST_TEAM_HEADER |
cost_team_header |
cost_team_header |
X-Anypoint-Cost-Team |
DONKEY_COST_PROJECT_HEADER |
cost_project_header |
cost_project_header |
X-Anypoint-Cost-Project |
DONKEY_COST_ENV_HEADER |
cost_env_header |
cost_env_header |
X-Anypoint-Cost-Env |
DONKEY_COST_ENDUSER_HEADER |
cost_enduser_header |
cost_enduser_header |
X-Anypoint-Cost-Enduser-Id |
DONKEY_TIMEOUT_S |
timeout_s |
timeout_s |
60.0 |
DONKEY_MAX_RETRIES |
max_retries |
max_retries |
3 |
DONKEY_REGISTRY_CACHE_TTL_S |
registry_cache_ttl_s |
registry_cache_ttl_s |
300 |
DONKEY_TELEMETRY |
telemetry |
telemetry |
true |
DONKEY_TELEMETRY_CAPTURE_CONTENT |
telemetry_capture_content |
telemetry_capture_content |
false |
DONKEY_TELEMETRY_INSTALL_GLOBAL |
telemetry_install_global |
telemetry_install_global |
false |
DONKEY_ON_MODEL_SUBSTITUTION |
on_model_substitution |
on_model_substitution |
off |
DONKEY_SEND_COST_HEADERS |
send_cost_headers |
send_cost_headers |
false |
DONKEY_RETRY_MODEL_CALLS_ON_GATEWAY_ERRORS |
retry_model_calls_on_gateway_errors |
retry_model_calls_on_gateway_errors |
false |
| {/* END GENERATED: settings-index */} |
The sections below explain each setting.
The three required values for the LLM proxy:
| Env var | DonkeyConfig field |
Meaning |
|---|---|---|
DONKEY_LLM_PROXY_URL |
llm_proxy_url |
Proxy 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_ID |
llm_proxy_client_id |
Consumer client ID (the per-agent identity). |
DONKEY_LLM_PROXY_CLIENT_SECRET |
llm_proxy_client_secret |
Consumer client secret. |
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 var | DonkeyConfig field |
Meaning |
|---|---|---|
DONKEY_LLM_PROXY_KEY |
llm_proxy_key |
Value 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. |
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 var | DonkeyConfig field |
Meaning |
|---|---|---|
DONKEY_LLM_PROXY_AUTH |
llm_proxy_auth |
Data-plane auth mode: client-id (default), jwt, or bearer. |
DONKEY_LLM_PROXY_WALLET_CLIENT_ID |
llm_proxy_wallet_client_id |
The 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>
)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
jwtmode 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
Donkeywithoutllm_auth, and the module-level factories (for examplefrom donkey_kit.integrations.langgraph import chat_model), which have nollm_auth. UseDonkey(llm_auth=…)injwtmode.
See Injection depth.
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.
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 injwtandbearermode.Donkey(auth=…)supplies the Anypoint control-plane credential. By default it is anAnypointConnectedAppbuilt fromANYPOINT_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:
| Request | On 401 |
|---|---|
| Model call, client-id mode | Raises AuthError straight away. There is no token to refresh. |
Model call, jwt or bearer mode |
Calls invalidate() on the llm_auth provider, re-sends once with the new token, then raises AuthError. |
| Control-plane call | Calls invalidate() on the auth provider, re-sends once, then raises AuthError. |
Blocking client (sync=True) |
Raises AuthError straight away. |
| Provider | Use 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.
| Env var | DonkeyConfig field |
Sent as |
|---|---|---|
DONKEY_APP_NAME |
application_name |
The X-Anypoint-Client-Application request header. |
DONKEY_BUSINESS_GROUP |
business_group |
The 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.
To set the business group per request rather than per Donkey, wrap the calls
in donkey.attribution(business_group=…) or pass business_group= to
donkey.run(...). The scoped value wins over this field for its block. See
Per-request attribution.
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-Idcarries the run ID. The gateway reads this header and echoes it on the responsex-correlation-id, so a client log line joins to the gateway's record.X-Donkey-Request-Idcarries 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 var | DonkeyConfig field |
Default | Meaning |
|---|---|---|---|
DONKEY_CORRELATION_HEADER |
correlation_header |
X-Correlation-Id |
Request header that carries the per-run correlation ID. |
DONKEY_CALL_ID_HEADER |
call_id_header |
X-Donkey-Request-Id |
Request header that carries the per-call ID. |
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 names | Why |
|---|---|
Host, Forwarded, any X-Forwarded-*, X-Real-IP, Content-Length, Transfer-Encoding, Connection, Upgrade, TE, Trailer, Expect |
They 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-URL |
Some 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-Id |
They 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-Agent |
The SDK, its HTTP client or a framework's SDK already sets them. |
| A name another of these keys already uses, including its default | Two 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.
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.
These are the cost field (a CostTags). Donkey.from_env takes each
dimension as a shorthand kwarg; DonkeyConfig.resolve(cost=CostTags(...)) sets
them together. Either way, a dimension you pass replaces only that dimension.
| Env var | Donkey.from_env kwarg |
Meaning |
|---|---|---|
DONKEY_COST_TEAM |
team |
Owning team. |
DONKEY_COST_PROJECT |
project |
Project / workload. |
DONKEY_COST_ENV |
env |
Deployment environment (e.g. prod). |
DONKEY_COST_ENDUSER_ID |
enduser_id |
End-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 merges across the config files key by key: a local
"enduser.id" adds to the project file's team and project instead of
replacing them, and the user file fills any dimension neither sets. 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 var | DonkeyConfig field |
Default | Meaning |
|---|---|---|---|
DONKEY_SEND_COST_HEADERS |
send_cost_headers |
false |
Also 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, within the
names you can't use:
| Env var | DonkeyConfig field |
Default |
|---|---|---|
DONKEY_COST_TEAM_HEADER |
cost_team_header |
X-Anypoint-Cost-Team |
DONKEY_COST_PROJECT_HEADER |
cost_project_header |
X-Anypoint-Cost-Project |
DONKEY_COST_ENV_HEADER |
cost_env_header |
X-Anypoint-Cost-Env |
DONKEY_COST_ENDUSER_HEADER |
cost_enduser_header |
X-Anypoint-Cost-Enduser-Id |
In code, Donkey.from_env(send_cost_headers=True).
| Env var | DonkeyConfig field |
Meaning |
|---|---|---|
DONKEY_TELEMETRY |
telemetry |
Emit OTel spans at all (default true). |
DONKEY_TELEMETRY_CAPTURE_CONTENT |
telemetry_capture_content |
Put prompt/completion text on spans (default false). |
DONKEY_TELEMETRY_INSTALL_GLOBAL |
telemetry_install_global |
Install DDK's OTLP provider as the process-global OpenTelemetry provider (default false). See DDK leaves the global provider to you. |
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.
| Env var | DonkeyConfig field |
Default | Meaning |
|---|---|---|---|
DONKEY_TIMEOUT_S |
timeout_s |
60.0 |
HTTP timeout for governed calls, in seconds. |
DONKEY_MAX_RETRIES |
max_retries |
3 |
Retries for transient upstream failures (502 / 503 / 504) with backoff. A 502 or 504 on a model call is retried only with retry_model_calls_on_gateway_errors (below). Policy refusals are never retried, and a gateway fallback is never retried twice. |
DONKEY_RETRY_MODEL_CALLS_ON_GATEWAY_ERRORS |
retry_model_calls_on_gateway_errors |
false |
Also retry a 502 or 504 on a model call (a POST whose body, or native Gemini path, names a model). Off by default, because the provider may already have completed and billed the call, and the gateway offers no verified idempotency key. A 503, and any other request, is retried either way. |
DONKEY_ON_MODEL_SUBSTITUTION |
on_model_substitution |
off |
off surfaces a model substitution on donkey.last_call; raise turns it into ModelSubstituted. See Telemetry & cost. |
DONKEY_REGISTRY_CACHE_TTL_S |
registry_cache_ttl_s |
300 |
How long registry lookups (used by tool access) are cached in memory, in seconds. |
DONKEY_NO_CACHE |
— | unset | Set to 1, true or yes to bypass that in-memory registry cache. |
DONKEY_TRUST_PROJECT_CONFIG |
— | unset | Set 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 |
— | unset | Set 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://. |
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:
| Field | Accepted |
|---|---|
timeout_s |
A number greater than 0 |
max_retries, registry_cache_ttl_s |
A whole number, 0 or more |
telemetry, telemetry_capture_content, telemetry_install_global, send_cost_headers, retry_model_calls_on_gateway_errors |
True or False in code; 1, true, yes, on, 0, false, no or off (any case) in the environment or a config file |
region |
us, eu, ca or jp |
llm_proxy_auth |
client-id, jwt or bearer (any case in the environment or a config file) |
on_model_substitution |
off 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.
A separate credential from the LLM proxy, for features that call the Anypoint
platform: registry and tool discovery, and publication. 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.
| Env var | DonkeyConfig field |
Meaning |
|---|---|---|
ANYPOINT_CLIENT_ID |
client_id |
Connected-app client ID. |
ANYPOINT_CLIENT_SECRET |
client_secret |
Connected-app client secret. |
ANYPOINT_ORG_ID |
org_id |
Anypoint organization ID. |
ANYPOINT_ENV |
environment |
Anypoint environment (default Sandbox). |
ANYPOINT_REGION |
region |
Control-plane region: us (default), eu, ca, or jp. |
ANYPOINT_BASE_URL |
base_url |
Explicit control-plane base URL; overrides the region. Must be https:// (loopback hosts excepted, or any host with DONKEY_ALLOW_HTTP). |
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 bydonkey.llmand every framework adapter. - Control plane: the Anypoint platform at
base_url(default: the host forregion). Used for the connected-app token request and for registry and tool discovery.
| Destination | Client-id mode (default) | jwt mode |
bearer mode |
Never 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.
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_urlon the data plane,base_urlon the control plane; - a URL you pass to a factory in code:
base_urltodonkey.llm.client()or an adapter factory,api_base(LlamaIndex, ADK'smodel(), CrewAI),openai_api_base(LangGraph), orclient_args["base_url"](Strands). It must pass the samehttps://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.
| Feature | Control-plane requests | In this release |
|---|---|---|
Model calls: donkey.llm, donkey.openai(), every adapter |
None | Available |
donkey doctor |
None. 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() |
None | Available |
Registry: donkey.registry.search(), resolve_mcp(), resolve_agent(), explain(), warm() |
Token request, then Exchange reads | Roadmap: raises NotImplementedError before sending |
Tool discovery: donkey.tools.discover(), donkey.tools.lock() |
Token request, then Exchange reads | Roadmap: raises NotImplementedError before sending |
Publication: Publication.preview() / verify() |
Token request, then platform calls | Roadmap: raises NotImplementedError before sending |
Hidden commands donkey status, publish, verify |
Token request, then platform calls | Roadmap: exits with "blocked on verification" before sending |
Your own code calling AnypointConnectedApp.token() |
Token request | Available |
So in this release the SDK sends the ANYPOINT_* credentials only if your own
code asks for a connected-app token.
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) | Value | When | Sent to |
|---|---|---|---|
X-Correlation-Id (correlation_header) |
The run ID | Always | Both planes |
X-Donkey-Request-Id (call_id_header) |
A per-call ID | Always | Both planes |
X-Anypoint-Client-Application |
application_name |
When set | The LLM proxy only |
X-Anypoint-Business-Group |
business_group, or the group from an enclosing donkey.attribution(...) / donkey.run(business_group=…) block |
When set | The LLM proxy only |
X-Anypoint-Cost-Team, -Project, -Env, -Enduser-Id |
Your cost tags, including the end-user ID | Only with send_cost_headers; see Cost-attribution tags |
The LLM proxy only, or a receiver of yours in front of it |
x-cache-* |
Your cache controls, including principal_id |
Inside a donkey.cache(...) block |
The 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, a per-request business group or cache controls: it
always sends the configured business_group.
Instead of env vars you can put values in a [donkey] table. Keys are the
DonkeyConfig field names.
| File | Where | Meant for |
|---|---|---|
.donkey-kit.toml |
The current working directory (not searched upward), or the file you name with path= |
Non-secret project config you commit: URLs, client IDs, attribution. |
.donkey-kit.local.toml |
The same directory as .donkey-kit.toml |
Secrets 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, empty or a relative path |
Your user config directory | Personal defaults for every project. Always read, beneath the two files above. When XDG_CONFIG_HOME is set to an absolute path, ~/.config isn't read. |
The three files merge key by key: .donkey-kit.local.toml over
.donkey-kit.toml over the user file, so a lower file fills whatever a higher
one leaves unset. Nested tables such as [donkey.cost] merge the same way. A
scalar or an array in a higher file replaces the lower file's value; it isn't
appended to. Each key remembers which file it came from.
One table in these files is not a DonkeyConfig field: [donkey.mcp_servers]
declares how each MCP server your tools name is reached, and only donkey scan
reads it, from the scanned directory's two project files. Donkey.from_env()
ignores it. See
Declaring how an MCP server is reached.
Donkey.from_env(path=...) and DonkeyConfig.resolve(path=...) read the named
file in place of ./.donkey-kit.toml. The .donkey-kit.local.toml beside that
file, if there is one, still overlays it, and the user file still sits beneath.
A path that names no file raises ConfigError. The named file and its
overlay count as project files for the
endpoint rule.
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.
Highest first, per key:
- Values set in code: the keyword arguments of
Donkey.from_env(...)andDonkeyConfig.resolve(...), and anything you change on a resolved config, withwith_overrides(...),dataclasses.replace(...)or any other copy. - Environment variables.
./.donkey-kit.local.toml(beside thepath=file, when you pass one)./.donkey-kit.toml(or thepath=file)- The user file (
$XDG_CONFIG_HOME/.donkey-kit.toml, or~/.config/.donkey-kit.toml). - Defaults.
A keyword argument replaces the field outright: the environment and the files
are not read for it, so an invalid DONKEY_TIMEOUT_S is ignored when you pass
timeout_s. A cost=CostTags(...) argument replaces only the dimensions it
sets. Every invalid value, from any layer, is reported in one ConfigError.
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.
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).
The two checks, in the order a credential meets them:
flowchart TD
subgraph C1["Check 1 · endpoints must use https://"]
H["Scheme and host"]
AH["DONKEY_ALLOW_HTTP<br/>in the environment?"]
end
subgraph C2["Check 2 · project-file URLs get project-file credentials"]
P["URL read from .donkey-kit.toml<br/>or .donkey-kit.local.toml?"]
T["DONKEY_TRUST_PROJECT_CONFIG<br/>in the environment?"]
R["base_url on a regional<br/>anypoint.mulesoft.com host?"]
C["Every credential it would receive<br/>also read from those two files?"]
end
H -->|"https://, or http:// to loopback"| P
H -->|"http:// to another host"| AH
H -->|"other scheme, or no host"| E1["ConfigError"]
AH -->|"yes, with a ConfigWarning"| P
AH -->|no| E1
P -->|"no: env, user file, code or region default"| OK(["Sent"])
P -->|yes| T
T -->|yes| OK
T -->|no| R
R -->|yes| OK
R -->|no| C
C -->|yes| OK
C -->|no| E2["ConfigError<br/>names each credential and its source"]
A token from llm_auth (jwt and bearer mode) or from a Donkey(auth=…)
provider never comes from a file, so it always fails the last check. With its
URL in a project file, only DONKEY_TRUST_PROJECT_CONFIG (or, for base_url, a
regional Anypoint host) lets it through.
This applies wherever the URL comes from: environment, file or code.
| URL | Result |
|---|---|
https://… |
Accepted |
http:// on a loopback host: localhost, 127.0.0.0/8, ::1 |
Accepted |
http:// on any other host, including 0.0.0.0, LAN addresses and container hostnames such as http://simulator:8080 |
ConfigError |
http:// on any other host, with DONKEY_ALLOW_HTTP=1 in the environment |
Accepted, with a ConfigWarning |
| Any other scheme, or a URL with no host | ConfigError, 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.
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:
| Endpoint | Credentials checked |
|---|---|
base_url |
client_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 mode |
llm_proxy_client_id, llm_proxy_client_secret, llm_proxy_key (the ones that are set) |
llm_proxy_url, jwt mode |
llm_proxy_wallet_client_id, and the JWT from llm_auth, which always counts as coming from outside the files |
llm_proxy_url, bearer mode |
llm_proxy_key if set, and the token from llm_auth, which always counts as coming from outside the files |
| The URL comes from | A credential it would receive comes from | Result |
|---|---|---|
| Environment, user file, code, or the region default | Anywhere | Sent |
.donkey-kit.toml / .donkey-kit.local.toml |
.donkey-kit.toml / .donkey-kit.local.toml |
Sent |
.donkey-kit.toml / .donkey-kit.local.toml |
An environment variable | ConfigError |
.donkey-kit.toml / .donkey-kit.local.toml |
The user file | ConfigError |
.donkey-kit.toml / .donkey-kit.local.toml |
Code (a value you set or changed on the config) | ConfigError |
.donkey-kit.toml / .donkey-kit.local.toml, jwt mode |
The JWT from llm_auth (always) |
ConfigError, every time |
.donkey-kit.toml / .donkey-kit.local.toml, bearer mode |
The 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.platform.mulesoft.com or jp1.platform.mulesoft.com |
Anywhere | Sent |
Any of the above, with DONKEY_TRUST_PROJECT_CONFIG set in the environment |
Anywhere | Sent |
Any of the above, with only DONKEY_ALLOW_HTTP set |
As in the rows above | Unchanged: 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 project 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 the project config files by setting DONKEY_TRUST_PROJECT_CONFIG=1
The source labels are env, user file and code.
- 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. - Keep the credentials in
.donkey-kit.local.tomlin the same directory. Not available for thellm_authtoken injwtorbearermode or for aDonkey(auth=…)token, because none of them comes from a file. - Opt in: set
DONKEY_TRUST_PROJECT_CONFIGto1(ortrue,yes,on) in the environment if you trust the project 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.
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. Each form records
# the value as set in code:
cfg = DonkeyConfig.resolve(send_cost_headers=True)
cfg = DonkeyConfig.from_env().with_overrides(send_cost_headers=True)
cfg = dataclasses.replace(DonkeyConfig.from_env(), send_cost_headers=True)
# From a named config file instead of ./.donkey-kit.toml:
cfg = DonkeyConfig.resolve(path="deploy/staging.toml")
# Or from the environment, with lifecycle:
async with Donkey.from_env() as donkey:
...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.
| Object | Hidden when printed | Still shown |
|---|---|---|
DonkeyConfig |
client_secret, llm_proxy_client_secret and llm_proxy_key are left out |
Every 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 values | Entity types, count and character offsets; see Errors |
The masked mappings are ordinary dicts. The table shows how common operations behave:
| Operation | Result |
|---|---|
**kwargs, kwargs["api_key"], ==, passing it to a framework |
Real values |
.copy(), copy.copy, copy.deepcopy, kwargs | other |
Still 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 |
| Object | Prints |
|---|---|
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, soasdict()holds each secret once, in its own field.) - Objects built from the kwargs, such as a framework client or the
openai_clientreturned by the OpenAI Agents adapter, and whatever those objects log. attribution_headers()andcost_headers(), which return plaindicts..responseon anyDonkeyError:exc.response.request.headersholds the realclient_secretandAuthorizationheaders.PIIDetected.gateway_messageand.response, which contain the flagged values.UpstreamRequestErrormessages, 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 whentelemetry_capture_contentis on.