Contents
- 0.1.2: public API surface, removed control plane and typed errors
- 1. donkey_kit.provisioning and donkey_kit.governance are gone
- 2. Hidden CLI commands validate, plan, apply, drift, lint, generate are removed
- 3. Three errors are removed
- 4. Blocked-surface types moved to donkey_kit.experimental
- 5. Two renamed names, with deprecated aliases
- 6. run_context() is deprecated
- 7. Removed names
- 8. Internal names renamed without an alias
- 9. pydantic and pyyaml are no longer installed for you
- 10. The dev, mcp and a2a extras are gone, and [all] installs no test runner
- 11. The conformance plugin's --agent is now --donkey-agent
- 12. Donkey() no longer installs the global OpenTelemetry provider
- 13. The user config file is merged beneath the project files
- 14. Donkey.from_env() accepts every field
- 15. A 502 or 504 on a model call is no longer retried by default
- 16. Errors leaving donkey.run() and @donkey.governed are typed
- 17. A request-rate-limit 429 is RequestRateLimitExceeded
- Shipped simulator fixtures moved into the package (not breaking)
- New exports (not breaking)
- LlamaIndex, Agent Framework and ADK model() report last_call (not breaking)
- 0.1.1: credential handling, endpoint trust and printed output
- 1. Non-loopback http:// endpoints are refused
- 2. A URL from the project's config files only gets credentials from those files
- 3. .donkey-kit.local.toml is now read
- 4. Each credential stays on its own plane
- 5. Cost-tag headers are off by default
- 6. donkey doctor output
- 7. Some configurable header names are refused
- 8. Config files that link outside the working directory are refused
- 9. URL overrides passed to factories must use https://
- 10. Credentials go only to checked endpoints
- 11. LlamaIndex, MS Agent Framework and ADK's model() send through the SDK's client
- 12. PIIDetected messages no longer contain the flagged values
- 13. repr() / str() of DonkeyConfig omit the secrets
- 14. Printed connection_kwargs() show '*' for secrets
- 15. typed_refusals() no longer chains the LangChain error
- 16. An Agent Kill Switch block raises AgentKilled
- 17. donkey-kit[all] leaves out the seven connection_kwargs() frameworks
Changes since 0.1.1 that can affect existing code. 0.1.2 defines the public
API (#927): a name is public when you can import it from donkey_kit or from
a public module's __all__; submodule paths are not API. It also deletes the
refused provisioning control plane (#730).
- Sections 1 to 3 break code that used the deleted control plane.
- Sections 4 to 6 still work but emit a
DeprecationWarning. - Sections 7 and 8 break code that used removed or internal names.
- Sections 9 and 10 change what
pipinstalls. - Section 11 renames a conformance-plugin option (the old one still works and warns).
- Sections 12 to 17 change runtime behaviour without an import error.
The deprecated names stay for the rest of 0.1.x. The release that removes them
says so here and in its Release notes' Breaking changes section. To find every
call site now, run your tests with -W error::DeprecationWarning.
Who: code that imported donkey_kit.provisioning (spec, planner,
applier, lint, publish, cli, doctor) or donkey_kit.governance
(Governance, GatewayTarget, PolicyBinding) (#730).
Symptom: ModuleNotFoundError: No module named 'donkey_kit.provisioning'
(or 'donkey_kit.governance').
Fix: the declarative provisioning control plane is on the build plan's
Do not build list, and every verb in it raised blocked on verification, so
there is no replacement. Manage gateways and policies with API Manager or
Terraform. The reasoning is ADR 0008 in docs/adr/ (legacy
quarantine and the CLI's home). A few pieces moved instead of going away:
| Was | Now |
|---|---|
donkey_kit.provisioning.cli (app, main) |
donkey_kit.cli |
donkey_kit.provisioning.doctor |
donkey_kit.cli.doctor |
donkey_kit.provisioning.doctor.Check |
donkey_kit.cli.doctor.DoctorCheck |
donkey_kit.provisioning.publish.content_digest / publish_if_changed |
donkey_kit.registry.publication |
The [targets.*] tables in .donkey-kit.toml and the DONKEY_TARGET
variable were read only by GatewayTarget.from_env(). They are now ignored.
The donkey commands you can see in donkey --help are unchanged.
2. Hidden CLI commands validate, plan, apply, drift, lint, generate are removed
Who: scripts that ran one of them. All of them except validate already
exited 3 with blocked on verification.
Symptom: No such command 'plan'. and exit status 2.
Fix: remove the call. donkey status, donkey publish and donkey verify
are still there, hidden, and still exit 3 until Exchange publication (BG §2.5)
is verified.
ProvisioningError, GovernanceDrift and PlatformTeamOnly were raised only
by the deleted modules. Drop them from except clauses and simulate(...)
calls. DonkeyError still catches everything the SDK raises.
Who: code that imported STRICT, AssetRef, AssetType, Contact,
GovernanceCriteria, Publication, PublicationAssetType,
PublicationDrift, RegistryError or ToolInvocationError from donkey_kit
(#730).
Symptom: DeprecationWarning: donkey_kit.AssetRef is deprecated; import it from donkey_kit.experimental. The old spelling still works for now, but the
names are out of donkey_kit.__all__ (so from donkey_kit import * no longer
brings them) and type checkers flag the old import. A later release removes the
alias.
Fix:
- from donkey_kit import STRICT, AssetRef, RegistryError
+ from donkey_kit.experimental import STRICT, AssetRef, RegistryErrorEvery surface these types serve (registry discovery and governed-state checks,
publication, MCP tool calls) still raises blocked on verification. They move
back to donkey_kit when their surface is verified, and until then they may
change in any release. The submodule paths (donkey_kit.registry,
donkey_kit.core.errors) still work.
Who: code that imports any name in the left column (#927).
Symptom: the import still works but emits a DeprecationWarning naming
the replacement.
| Was | Now | Why |
|---|---|---|
donkey_kit.registry.governance |
donkey_kit.registry.criteria, or import the names from donkey_kit.registry |
It shared a name with the old top-level donkey_kit.governance. |
donkey_kit.simulator.scenarios.Scenario |
donkey_kit.simulator.scenarios.FaultScenario |
It clashed with donkey_kit.conformance.Scenario. |
Fix:
- from donkey_kit.registry.governance import GovernanceCriteria, STRICT
+ from donkey_kit.experimental import GovernanceCriteria, STRICT
- from donkey_kit.simulator.scenarios import Scenario
+ from donkey_kit.simulator.scenarios import FaultScenarioWho: code that calls Donkey.run_context() or
donkey_kit.core.run_context() (#926).
Symptom: both still bind a correlation ID for the block, and both emit a
DeprecationWarning.
Fix:
- with donkey.run_context("order-42"):
+ with donkey.run(id="order-42"):Without a Donkey instance, use donkey_kit.core.telemetry.run_scope().
These are gone, with no alias (#926). Each was unused or had no working caller.
- The
governance=keyword ofdonkey.tools.discover().discover()still raises blocked-on-verification, so no working call passed it. Drop the argument. donkey_kit.core.telemetry.span()and the span-name constantsSPAN_REGISTRY_RESOLVE,SPAN_TOOL_CALLandSPAN_PROVISION_APPLY. Nothing in the SDK emitted those spans. If you opened spans withspan(), use your OpenTelemetry tracer directly.- The class attributes behind
Adapter.extra.extrais now a read-only property that reads the adapter roster. Reading it works as before; assigning it raisesAttributeError.
These were never public. They are listed only because the old names were importable in 0.1.1 (#927, #728, #726).
| Was | Now |
|---|---|
donkey_kit.core.config._TOML_NAME / _LOCAL_TOML_NAME |
TOML_NAME / LOCAL_TOML_NAME |
donkey_kit.simulator.inject._resolve |
resolve_fixture |
donkey_kit.conformance.harness._offline_config |
offline_config |
DonkeyAsyncClient._swap_transport() / DonkeyClient._swap_transport() |
client.governed_transport.replace_inner() |
donkey_kit.integrations._httpx2_bridge |
donkey_kit.core.transport.httpx2 |
DonkeyAsyncClient._on_request() (a no-op hook) |
removed; the per-send _inject_headers event hook does request-side work |
Adapter.factory_observes_last_call |
Adapter.capabilities(factory).observes_last_call |
Who: code that imports pydantic or yaml and relied on donkey-kit (or
donkey-kit[cli]) to install it (#730).
Symptom: ModuleNotFoundError: No module named 'pydantic' (or 'yaml')
in your own code after upgrading.
Fix: depend on them directly. The SDK itself imports neither. Framework extras that need pydantic (LangChain, ADK, the OpenAI SDK) still bring it in.
Who: anyone who installs donkey-kit[dev], donkey-kit[mcp],
donkey-kit[a2a], or relies on donkey-kit[all] to bring in pytest (#744).
Symptom: pip warns donkey-kit 0.1.2 does not provide the extra 'mcp'
(or 'a2a', 'dev') and installs nothing for it. After pip install "donkey-kit[all]", pytest --donkey-conformance is not available.
mcpanda2ahad no code behind them: nothing in the SDK importedmcpora2a-sdk. Each comes back with the feature that uses it.devwas contributor tooling (mypy, ruff, import-linter). It is now a PEP 735 dependency group in a source checkout and is not published.[all]is now everything a user runs:llm,langgraph,otel,cliandlocal. The conformance plugin stays in[test].
Fix:
- pip install "donkey-kit[all]"
+ pip install "donkey-kit[all,test]" # if you run the conformance plugin
- pip install -e ".[dev,llm,cli]" # contributors, from python/
+ pip install -e ".[llm,cli]" --group devDrop mcp and a2a from any install line. --group needs pip 25.1 or later.
- pytest --donkey-conformance --agent=myagent:build
+ pytest --donkey-conformance --donkey-agent=myagent:buildThe plugin loads on every pytest run, and pytest refuses to start when two
plugins register the same option, so all its options now start with
--donkey- (#746). --agent still works and emits a DeprecationWarning,
unless another plugin registered --agent first. donkey test --agent is
unchanged; it now forwards --donkey-agent to pytest.
Reference: DDK leaves the global provider to you.
Who: anyone who sets OTEL_EXPORTER_OTLP_ENDPOINT, doesn't configure an
OpenTelemetry TracerProvider of their own, and relied on Donkey() making
its provider the global one (#732). For example, spans from other libraries
reached the collector only because DDK had installed its exporter globally.
Symptom: DDK's own spans still reach the collector, but
trace.get_tracer_provider() is unchanged after Donkey(), so spans that
other code creates through the global provider are no longer exported. A
trace.get_tracer_provider().force_flush() call no longer flushes DDK's spans
either; they are flushed when the interpreter exits.
Fix: set DONKEY_TELEMETRY_INSTALL_GLOBAL=true (or
telemetry_install_global = true in .donkey-kit.toml) to get the old
behaviour, or configure your own TracerProvider. DDK's spans go to a provider
you set even if you set it after Donkey(). ADR 0010 in
docs/adr/ has the reasoning.
Who: anyone with a user file ($XDG_CONFIG_HOME/.donkey-kit.toml, or
~/.config/.donkey-kit.toml) who also has a .donkey-kit.toml or
.donkey-kit.local.toml in the working directory.
Symptom: keys from the user file now apply wherever the working-directory
files leave them unset. Before, the user file was read only when neither
working-directory file existed
(#727).
A credential from the user file is still never sent to an llm_proxy_url or
base_url read from the working-directory files, so a project file that names
a URL but no credentials now raises the endpoint ConfigError instead of a
missing-field one.
Fix: remove from the user file any key you don't want applied to every
project, or set it in the project's own files. donkey doctor labels values
from it user file.
Donkey.from_env(...) and the new DonkeyConfig.resolve(...) accept any
DonkeyConfig field as a keyword argument, plus path= to read a named
config file in place of ./.donkey-kit.toml (#727). An unknown name raises
TypeError. on_model_substitution=None is no longer accepted; leave the
argument out instead.
Who: code that relies on the transport re-sending a model call (a POST
whose body, or native Gemini path, names a model) after a 502 or 504
(#728).
Symptom: the 502 or 504 comes back on the first attempt, as an
UpstreamModelError once classified. Either status can follow an upstream
call that already completed and was billed, so a re-send could bill twice, and
the gateway has no verified idempotency key (ADR 0009 in
docs/adr/). A 503, and every request that is not a model
call, is still retried as before.
Fix: if a second charge is acceptable, opt back in with
DONKEY_RETRY_MODEL_CALLS_ON_GATEWAY_ERRORS=1 in the environment, or set
retry_model_calls_on_gateway_errors in code:
from donkey_kit import Donkey
donkey = Donkey.from_env(retry_model_calls_on_gateway_errors=True)Who: code that catches a framework's or HTTP SDK's own error (for example
openai.APIStatusError) around a block wrapped in donkey.run() or a
function decorated with @donkey.governed (#724).
Symptom: a gateway refusal now leaves the block as the SDK's typed error,
such as PIIDetected, instead of the framework's exception. The framework's
error is on exc.framework_error. Errors that are not governed refusals pass
through unchanged.
Fix: catch the typed error (or DonkeyError), or pass
typed_refusals=False to donkey.run() or @donkey.governed to get the
framework's errors as before. See
Typed refusals at the framework boundary.
Who: code that catches TokenBudgetExceeded to handle every 429 from a
proxy, or that checks exc.policy == "token-rate-limit" (#974).
Symptom: on a proxy with the stock rate-limiting policy (request count,
exposeHeaders: true), the 429 now raises RequestRateLimitExceeded. It is
a sibling of TokenBudgetExceeded, not a subclass, so an except TokenBudgetExceeded: block no longer catches it. Its policy is
"request-rate-limit", and its retry_after comes from x-ratelimit-reset.
The telemetry attribute donkey.policy.type reads request_rate_limit. A
token-rate-limit 429, and any 429 without the x-ratelimit-* headers, is
still TokenBudgetExceeded.
Fix: catch both, or catch their common base PolicyViolation:
from donkey_kit import RequestRateLimitExceeded, TokenBudgetExceeded
try:
...
except (TokenBudgetExceeded, RequestRateLimitExceeded) as exc:
wait = exc.retry_afterdonkey.budget.pace() now also refuses when the request window is spent, and
BudgetReserveReached.window says which window it was ("tokens" or
"requests"). Pass it to donkey.budget.wait_for_reset(window=exc.window).
The captured gateway responses that simulate(), donkey mock and the
gateway fixture replay now ship inside the wheel, under
donkey_kit/simulator/_fixtures/, instead of being copied in from
tests/fixtures/ at build time (#746). Nothing changes for installed
code. If you read those files from a source checkout, use the new path.
The types that public Donkey members return can now be imported from
donkey_kit: LastCall, LastCallStatus, RunScope, ToolsFacade (was the
private _ToolsFacade), CacheScope, DonkeyAsyncClientView,
DonkeyClientView, LLMClient, ExchangeRegistry and ConfigOverrides.
Import them from donkey_kit rather than from their submodules. The
typed-refusal bridge is new as donkey_kit.typed_refusals and
donkey_kit.TypedRefusals (#724), and donkey_kit.integrations exports
AdapterProtocol and AdapterCapabilities (#726). RequestRateLimitExceeded
and RequestWindow (the type of donkey.budget.requests) are new in
donkey_kit (#974).
donkey.last_call on a Donkey used only through LlamaIndex, Microsoft Agent
Framework or ADK's model() now reads UNOBSERVED before a call and
OBSERVED after it, instead of UNAVAILABLE. Code that branched on
UNAVAILABLE for these adapters no longer sees it. Only CrewAI still reports
UNAVAILABLE.
donkey.llamaindex.typed_refusals() is new. It turns the openai
APIStatusError that LlamaIndex raises on a proxy refusal into the SDK's typed
error, such as PIIDetected, and keeps the original on .framework_error
(#740).
Changes since 0.1.0 that can affect existing code, grouped by what you
would notice. Sections 1 to 11 change what the SDK sends or accepts; sections
12 to 15 change only what it prints or how errors are chained; sections 16 and
17 change how a refusal is typed and what donkey-kit[all] installs. The
reference for each rule is linked from its section.
Who: anyone whose llm_proxy_url / DONKEY_LLM_PROXY_URL, base_url /
ANYPOINT_BASE_URL or connected-app token endpoint uses http:// on a host
other than localhost, 127.0.0.0/8 or ::1. This applies wherever the URL is
set: environment, file or code. It includes a simulator reached from another
machine or container (http://<lan-ip>:8080, http://simulator:8080,
http://0.0.0.0:8080).
Symptom: building a client raises:
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.
Fix: use the service's https:// address. For the simulator, run the
client on the same host and use http://127.0.0.1:<port>. If the endpoint is
on a network you control and has no TLS (a simulator in another container, an
in-cluster proxy), set DONKEY_ALLOW_HTTP=1 (or true, yes, on) in the
environment. It covers base_url, llm_proxy_url and the token endpoint.
Credentials then travel unencrypted to that host, and the SDK emits a
ConfigWarning naming the key and the host, and quoting the value as you set
it:
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.
DONKEY_ALLOW_HTTP is read from the environment only; a config file can't set
it. It applies to http:// URLs with a host only (other schemes are still
refused), and it doesn't relax the credential rule in section 2.
Reference: Which credentials a URL receives.
Who: anyone with llm_proxy_url or a non-standard base_url in
./.donkey-kit.toml or ./.donkey-kit.local.toml, while a credential for that
endpoint comes from somewhere else. Loopback URLs are included. The usual cases:
- The URL is in
.donkey-kit.tomland the secret is in an environment variable. This is the splitdonkey initproduced before this release. - The URL is in
.donkey-kit.tomland the secret is set in code, for examplewith_overrides(llm_proxy_client_secret=vault.read(...))or the same throughdataclasses.replace(...). - A custom
base_urlin.donkey-kit.tomlwith aDonkey(auth=…)provider. Its token never comes from a file. This one fails only at the first control-plane call. jwtmode withllm_proxy_urlin either file. The JWT fromllm_authnever comes from a file, so this case always fails.- A
base_urlon a host other than the four standard Anypoint hosts in.donkey-kit.toml, withANYPOINT_CLIENT_SECRETin the environment. This one fails only at the first control-plane call.
Not affected: URL and credentials all in environment variables (the usual
CI setup); config built entirely with DonkeyConfig(...); URLs in the user
config file; a base_url on anypoint.mulesoft.com, eu1.anypoint.mulesoft.com,
ca1.platform.mulesoft.com or jp1.platform.mulesoft.com (before 0.1.3 the
CA and JP entries were ca1.anypoint. and jp1.anypoint., which do not exist, #22).
Symptom: building a client (donkey.llm.client(), a framework factory,
connection_kwargs()) raises, before anything is sent:
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
donkey doctor shows the same message on its config line (for llm_proxy_url) or its control plane line (for base_url).
Fix: pick one.
-
Move the secret next to the file (local development; not possible in
jwtmode):# .donkey-kit.toml (committed) [donkey] llm_proxy_url = "https://llm-proxy.example.com/my-proxy/" llm_proxy_client_id = "my-client-id"
- export DONKEY_LLM_PROXY_CLIENT_SECRET="<secret>" + # .donkey-kit.local.toml (gitignored) + [donkey] + llm_proxy_client_secret = "<secret>"
-
Set the URL in the environment too (CI,
jwtmode, secrets set in code). The environment wins over the file:export DONKEY_LLM_PROXY_CLIENT_SECRET="<secret>" + export DONKEY_LLM_PROXY_URL="https://llm-proxy.example.com/my-proxy/" -
Opt in if you trust the directory's config files:
export DONKEY_TRUST_PROJECT_CONFIG=1. It is read from the environment only; a config file can't set it.
A value you change on a resolved config counts as set in code (labelled code
in the error), however you change it: with_overrides(...),
dataclasses.replace(...) or any other copy. A copy that keeps the value keeps
its source, including DonkeyConfig(**dataclasses.asdict(cfg)) in the same
process. A config rebuilt in another process keeps where its URLs came from,
but its credentials count as set in code. So a URL you override in code is no
longer bound to the file's credentials, and a secret you override in code no
longer passes as a file credential.
This matters if you hand a config to a worker in another process
(multiprocessing with the spawn start method, Celery, Ray). A worker that gets
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, or
set the URL in the environment.
Who: anyone with a .donkey-kit.local.toml in the working directory. The
previous donkey init template suggested creating one, but the SDK didn't read
it.
Symptom: its values now apply. They merge into .donkey-kit.toml key by
key, including nested tables such as [donkey.cost]; a scalar or array in the
local file replaces the project file's value. Environment variables still win
over both. While it exists, the user file $XDG_CONFIG_HOME/.donkey-kit.toml
isn't read, even without a .donkey-kit.toml
(#727).
donkey doctor labels values from it local overlay.
Fix: review the file, and delete keys you don't want applied. Make sure it is gitignored.
A secret (client_secret, llm_proxy_client_secret, llm_proxy_key) in the
committed .donkey-kit.toml now emits a ConfigWarning
(donkey_kit.core.errors.ConfigWarning). Under -W error or
pytest filterwarnings = error that warning fails the run; move the secret to
.donkey-kit.local.toml.
Reference: What the SDK sends where.
Donkey now keeps one HTTP client for the LLM proxy and one for the Anypoint
platform. Model calls never request, send or refresh the connected-app token,
so they keep working when the Anypoint token endpoint is unreachable. The
features that use the control plane (registry, tool discovery, publication,
governance, provisioning) are still on the roadmap, so in this release the
token endpoint is contacted only if your own code calls
AnypointConnectedApp.token(). See
Which features use the control plane.
4a. Donkey(auth=…) no longer reaches the LLM proxy.
Who: callers whose LLM proxy authenticated model calls with the bearer token
from Donkey(auth=…). The token went out only on requests whose client set no
Authorization of its own: the Anthropic client, ADK's gemini(), and raw
requests through the shared client. OpenAI-compatible clients were already
sending their API-key slot instead.
Symptom: the first model call raises AuthError (a 401 from the proxy),
with no retry.
Fix: supply the token on the data plane.
-
OpenAI-compatible clients, static key: set
llm_proxy_key/DONKEY_LLM_PROXY_KEY. It is sent asAuthorization: Bearer <key>. (The Anthropic client sends it asx-api-key, and ADK'sgemini()asx-goog-api-key.) -
Any client with transport injection, rotating token: use
jwtmode. This requiresllm_proxy_wallet_client_id(sent asX-Client-Id) and is async-only:- donkey = Donkey(cfg, auth=StaticToken(token)) + donkey = Donkey( + replace(cfg, llm_proxy_auth="jwt", llm_proxy_wallet_client_id="my-wallet-id"), + llm_auth=StaticToken(token), + )
(
cfg.with_overrides(...)works the same way.) -
Any client with transport injection, rotating bearer token, no wallet: use
bearermode. Onlyllm_proxy_urlis required; the token fromllm_authis sent asAuthorization: Bearer <token>with noX-Client-Idand noclient_id/client_secretpair, refreshed and re-sent once on a401, and covered by the endpoint check. Likejwtmode it is async-only, and CrewAI, which builds its own HTTP client, raisesConfigErrorin this mode:- donkey = Donkey(cfg, auth=StaticToken(token)) + donkey = Donkey(replace(cfg, llm_proxy_auth="bearer"), llm_auth=StaticToken(token))
This covers the Anthropic client and ADK's
gemini(), which have no OpenAI-style API-key slot.
4b. In client-id mode, a 401 from the LLM proxy is no longer retried.
Who: client-id mode with ANYPOINT_CLIENT_ID / ANYPOINT_CLIENT_SECRET set.
Symptom: the same AuthError as before, one request sooner. The old retry
refreshed a token the proxy doesn't check, so it could never succeed. Fix:
none needed. If you see the error, check DONKEY_LLM_PROXY_CLIENT_ID /
DONKEY_LLM_PROXY_CLIENT_SECRET and the contract in API Manager. In jwt mode
the wallet JWT is still refreshed and retried once.
4c. In jwt mode, control-plane calls carry the connected-app token. They
no longer carry the wallet JWT or X-Client-Id, so registry and tool discovery
need ANYPOINT_CLIENT_ID / ANYPOINT_CLIENT_SECRET (or your own
Donkey(auth=…)) in jwt mode too. Those features still raise
NotImplementedError before sending anything, so nothing changes on the wire
in this release.
simulate(), donkey mock and the conformance kit still act on the LLM-proxy
client, and aclose() closes both clients.
Reference: Cost-attribution tags.
Who: anything of yours that reads X-Anypoint-Cost-* request headers. The
LLM Gateway reads none of them (docs/verified-apis.md §3).
Symptom: the headers are no longer sent. The donkey.cost.* span
attributes are unchanged, so dashboards built on spans need no change.
Fix: turn them back on with any one of these:
+ DONKEY_SEND_COST_HEADERS=true[donkey]
send_cost_headers = truecfg = DonkeyConfig.from_env().with_overrides(send_cost_headers=True)When they are on, the headers, including X-Anypoint-Cost-Enduser-Id, go only
on model requests to the LLM proxy. Anypoint control-plane requests and the
connected-app token request carry none of them, nor the attribution or
x-cache-* headers
(#833).
The cost_*_header name overrides apply as before.
doctor now prints llm endpoint and control plane lines (host and where the
value came from) before the probe. It reports an endpoint error on the config
line and skips the probe. With DONKEY_ALLOW_HTTP=1 set it also shows:
[i] plain http allowed to non-loopback hosts (DONKEY_ALLOW_HTTP=1 in env)
If you parse donkey doctor --json, expect the new entries named llm endpoint, control plane and, when the switch is on, plain http.
Reference: Header names you can't use.
Who: anyone who sets correlation_header, call_id_header or a
cost_*_header key (in the environment, a file or code) to a name that:
- doesn't start with
X-(for exampleTeamorOrigin); - routes or frames the request (
Host,X-Forwarded-*,Content-Length, …); - can change the request's method or path (
X-HTTP-Method-Override,X-Original-URL,X-Rewrite-URL, …); - carries credentials (
Authorization,Cookie,x-api-key,client_secret, …); - is already set by the SDK or a framework's SDK (
Content-Type,User-Agent, the attribution,x-cache-*andx-stainless-*headers, …); - is already used by another of these keys;
- or isn't a valid header name.
Names compare case-insensitively. All the default names start with X-.
Symptom: building the config raises, whichever way it is built:
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.
Fix: choose a different header name that starts with X-.
Who: anyone whose .donkey-kit.toml or .donkey-kit.local.toml is a link
to a file outside the working directory.
Symptom: loading the config raises:
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.
Fix: replace the link with a regular file (a link to a file inside the working directory is fine), or set the values in the environment.
Reference: Credentials go only to checked endpoints.
Who: code that passes a URL to a factory: base_url to
donkey.llm.client() or an adapter factory, api_base (LlamaIndex, ADK's
model(), CrewAI), openai_api_base (LangGraph) or Strands'
client_args["base_url"].
Symptom: a non-loopback http:// URL, another scheme, or a URL without a
host raises the same ConfigError as in section 1, naming the keyword (for
example base_url must be an https:// URL …).
Fix: use the https:// address, or set DONKEY_ALLOW_HTTP=1 as in
section 1. An accepted override receives the configured credentials, as before.
Who: code that sends an SDK-built client somewhere other than the
configured proxy or a URL passed to a factory, for example by changing
base_url after the fact (ChatOpenAI(**{**kwargs, "base_url": other})), or
that follows redirects across origins.
Symptom: the request is still sent, but without credentials: no
client_id / client_secret, Authorization, x-api-key or X-Client-Id,
including credential headers the framework set itself. The proxy at the other
address answers 401. The SDK's clients don't follow redirects; a redirect hop
your code follows to another origin also carries no credentials. CrewAI gets
the same rule through an interceptor in its connection_kwargs().
Fix: pass the URL to the factory instead (section 9), or set it as
llm_proxy_url.
Reference: Injection depth.
Who: users of these adapters, and anyone who spreads connection_kwargs()
into a constructor.
Symptom: connection_kwargs() has new keys when the framework's
dependencies are installed:
| Adapter | New keys |
|---|---|
| LangGraph | http_client (the blocking client, used by invoke()) |
| LlamaIndex | http_client, async_http_client |
| MS Agent Framework | async_client (an AsyncOpenAI on the SDK's client) |
ADK model() |
client (an AsyncOpenAI on the SDK's client) |
| CrewAI | interceptor |
Calls through LlamaIndex, MS Agent Framework and ADK's model() now carry the
run's correlation ID, get the SDK's retries and spans, populate
donkey.last_call in the context that made the call, and carry the JWT in
jwt mode (async calls only). donkey.last_call on a Donkey that resolved
only these adapters reads UNOBSERVED before a call and OBSERVED after it
(#740).
Fix: none needed if you spread connection_kwargs() whole. If you pick
keys out of it, add the new ones. If you replace one (for example your own
http_client), that client doesn't get the SDK's headers, retries or checked
endpoints.
Who: code that parses str(exc). Symptom: the message now names the
entity types, their count and their offsets, for example
… 1 entity (Email at chars 12-32) …. Fix: read .gateway_message (the
gateway's text) or .response (the raw body); both contain the flagged values.
.entities is unchanged.
client_secret, llm_proxy_client_secret and llm_proxy_key no longer appear.
Attribute access and equality are unchanged.
Who: tests that compare printed output. Fix: compare the mapping
instead. Unpacking, lookups, equality and json.dumps are unchanged (and
json.dumps writes the real values). dict(kwargs), {**kwargs} and
kwargs.items() still show the top-level api_key, and some framework objects
built from the kwargs print credentials in their own repr(): LangGraph's
ChatOpenAI (client_secret), LlamaIndex's OpenAILike (client_secret and
api_key) and CrewAI's OpenAICompletion (api_key). Details:
What printed output hides.
Who: code that reads __cause__ on an error raised by LangGraph's
typed_refusals().
Symptom: exc.__cause__ is None. The LangChain error repeats the
gateway's rejection text (for a PII block, the flagged values), and a traceback
prints every chained exception.
typed_refusals() is now a class-based context manager, so no frame in the
error's traceback holds the LangChain error as a local variable either. Error
reporters that print frame locals (Sentry, pytest -l) don't show it. The
error itself is still on exc.framework_error, and Python keeps it on the
suppressed exc.__context__; both hold the gateway's text.
Fix: read exc.framework_error for the original LangChain error. See
Refusal messages don't repeat blocked content.
Reference: The rejection shapes classify() types.
Who: code that catches the 403 the gateway returns when the Agent Kill Switch has blocked the agent.
Symptom: that 403 (nested error code agent_killed) now raises
AgentKilled, a PolicyViolation. In 0.1.0 it raised UpstreamRequestError,
so an except UpstreamRequestError no longer catches it. It is still terminal
and not retried.
Fix: catch AgentKilled (or PolicyViolation), and ask an administrator to
restore the agent's model access in Governance > Security.
Who: anyone who installs donkey-kit[all] to get a framework other than
LangGraph.
Symptom: all now installs llm, langgraph, mcp, otel, cli,
local and test only. In 0.1.0 it also listed adk, strands,
agent_framework, openai-agents, anthropic, crewai and llamaindex,
whose current releases can't be installed together, so pip failed with
resolution-too-deep.
Fix: add the framework you use: pip install "donkey-kit[all,crewai]".