Skip to content

Latest commit

 

History

History
897 lines (726 loc) · 50 KB

configuration.mdx

File metadata and controls

897 lines (726 loc) · 50 KB
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'

Configuration

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.

Building a config

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.

All settings

{/* 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.

Governed model access

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.
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 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.

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 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>
)
`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:

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.

Optional attribution

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.

Both header names are unconfirmed guesses. The gateway hasn't been seen reading them (see §3 of the [verification ledger](https://github.com/Donkey-Development-Kit/donkey-development-kit/blob/main/docs/verified-apis.md)). 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 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.

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 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.

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.

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).

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#cost-attribution-tags).

Telemetry

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.

Behaviour

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://.

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:

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.

Anypoint control plane

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.

The features that call the Anypoint platform are Roadmap in this release and stop before sending anything. [Which features use the control plane](#which-features-use-the-control-plane) lists 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).

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

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.

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

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.

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) 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.

Config files, secrets and trust

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.

Precedence

Highest first, per key:

  1. Values set in code: the keyword arguments of Donkey.from_env(...) and DonkeyConfig.resolve(...), and anything you change on a resolved config, with with_overrides(...), dataclasses.replace(...) or any other copy.
  2. Environment variables.
  3. ./.donkey-kit.local.toml (beside the path= file, when you pass one)
  4. ./.donkey-kit.toml (or the path= file)
  5. The user file ($XDG_CONFIG_HOME/.donkey-kit.toml, or ~/.config/.donkey-kit.toml).
  6. 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.

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).

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"]
Loading

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.

Endpoints must use https://

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.

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:

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.

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 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.

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. 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:
    ...

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.

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
Some framework objects built from the kwargs print credentials in their own `repr()` / `str()`:
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, 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.