From 8a716b7b622384ccb34b6d89950d27c2d34af278 Mon Sep 17 00:00:00 2001 From: Tommaso Bolis Date: Thu, 24 Sep 2026 08:38:11 +0200 Subject: [PATCH 1/4] chore: bump version to 0.1.0.dev9 (#554) --- python/pyproject.toml | 2 +- python/src/donkey_kit/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/pyproject.toml b/python/pyproject.toml index f3ffb99a..1f280faf 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "donkey-kit" -version = "0.1.0.dev8" +version = "0.1.0.dev9" description = "An SDK for consuming Agent Fabric capabilities (governed model and tool access) from your own agent framework." readme = "README.md" requires-python = ">=3.10" diff --git a/python/src/donkey_kit/__init__.py b/python/src/donkey_kit/__init__.py index 644498df..764f7d3e 100644 --- a/python/src/donkey_kit/__init__.py +++ b/python/src/donkey_kit/__init__.py @@ -52,7 +52,7 @@ PublicationAssetType, ) -__version__ = "0.1.0.dev8" +__version__ = "0.1.0.dev9" __all__ = [ "STRICT", From 3efba39e648aa73bdbb7b1aa334605e988526b68 Mon Sep 17 00:00:00 2001 From: Tommaso Bolis Date: Thu, 24 Sep 2026 08:40:19 +0200 Subject: [PATCH 2/4] =?UTF-8?q?docs(verify):=20live-verify=20the=20Anthrop?= =?UTF-8?q?ic-native=20Messages=20ingress=20route=20(=C2=A70.3)=20(#553)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #304 --- docs/verified-apis.md | 4 +- python/examples/anthropic/README.md | 19 +++-- python/examples/anthropic/main.py | 27 +++---- .../src/donkey_kit/integrations/anthropic.py | 50 +++++------- .../anypoint/anthropic_inbound/README.md | 79 +++++++++++++++++++ .../reject.client-id-missing.body.json | 1 + .../reject.client-id-missing.headers.txt | 10 +++ .../reject.openai-shape.body.json | 0 .../reject.openai-shape.headers.txt | 19 +++++ .../anthropic_inbound/request.success.http | 8 ++ .../responses.success.body.json | 1 + .../responses.success.headers.txt | 38 +++++++++ python/tests/unit/test_adapter_ergonomics.py | 12 +-- website/content/concepts/verification.mdx | 2 +- website/content/frameworks/anthropic.mdx | 36 ++++----- website/content/frameworks/index.mdx | 2 +- website/public/concepts/verification.md | 2 +- website/public/frameworks.md | 2 +- website/public/frameworks/anthropic.md | 36 ++++----- website/public/llms-full.txt | 40 +++++----- 20 files changed, 259 insertions(+), 129 deletions(-) create mode 100644 python/tests/fixtures/anypoint/anthropic_inbound/README.md create mode 100644 python/tests/fixtures/anypoint/anthropic_inbound/reject.client-id-missing.body.json create mode 100644 python/tests/fixtures/anypoint/anthropic_inbound/reject.client-id-missing.headers.txt create mode 100644 python/tests/fixtures/anypoint/anthropic_inbound/reject.openai-shape.body.json create mode 100644 python/tests/fixtures/anypoint/anthropic_inbound/reject.openai-shape.headers.txt create mode 100644 python/tests/fixtures/anypoint/anthropic_inbound/request.success.http create mode 100644 python/tests/fixtures/anypoint/anthropic_inbound/responses.success.body.json create mode 100644 python/tests/fixtures/anypoint/anthropic_inbound/responses.success.headers.txt diff --git a/docs/verified-apis.md b/docs/verified-apis.md index 1c569a03..9bfcbf35 100644 --- a/docs/verified-apis.md +++ b/docs/verified-apis.md @@ -91,7 +91,7 @@ request against the deployed gateway. | Item | Where used | Status | Verified value | Date | Source | |---|---|---|---|---|---| | Ingress base URL the SDK targets | `llm/client.py` | VERIFIED (LIVE) | `https:////` — e.g. `https://agent-network-ingress-gw-zovwbn.jeg62f.usa-e2.cloudhub.io/openai-sdk/`. **No `/v1` at the ingress**; OpenAI path segment appended directly (`/responses`, and OpenAI-native routes) | 2026-08-28 | live probe | -| Ingress **Format** (wire protocol the proxy accepts) — Anthropic-native route (#304) | `integrations/anthropic.py`, `llm/client.py` | UNVERIFIED (live) — **but documented** | Model Proxy supports **three selectable ingress Formats**, fixed at creation and unchangeable: **OpenAI** (works with all providers; multi-routing + fallback), **Gemini** (native Gemini format; single-provider), and **Anthropic** (native Anthropic API format for direct Claude requests; single-provider, no multi-routing/fallback). So the Anthropic-native route **exists as a product capability** — correcting the earlier "OpenAI-only, native route maybe-absent" assumption. Caveats keeping this UNVERIFIED for the SDK: (a) it must be *provisioned* that way — every DDK proxy is `Format=OpenAI`, so `donkey.anthropic.client()` pointed at them reaches Claude only as an *upstream provider*, not natively; (b) the **exact Anthropic ingress path** (docs show `/chat/completions` + `/responses` for OpenAI but do not state the Anthropic path — likely `/v1/messages`) and its **live behavior** are not yet captured against a real `Format=Anthropic` proxy. Per §12.8 (LLM data-plane, category 3) this stays behind the adapter's one-time warning until a live capture. Live capture blocked on provisioning a `Format=Anthropic` proxy (#304). The **Gemini** Format is now live-verified — see the next row. | 2026-09-23 | `docs.mulesoft.com/general/model-proxy` ("Supported Endpoint Formats"); live control-plane read of `ddk-bedrock-anthropic-model-routing` (instance `21192625`, DDK/Sandbox) confirming it is `Format=OpenAI`/model-based | +| Ingress **Format** — Anthropic-native route (#304) | `integrations/anthropic.py`, `llm/client.py` | VERIFIED (LIVE) | A proxy provisioned `Format=Anthropic` exposes a **native Anthropic Messages ingress** at `POST //v1/messages`. The Anthropic request shape (header `anthropic-version: 2023-06-01`, body `{"model":…,"max_tokens":N,"messages":[{"role":"user","content":…}]}`) → **200** with a native Anthropic body (`type:"message"`, `role:"assistant"`, `content[].text`, `stop_reason:"end_turn"`, `usage.input_tokens`/`output_tokens`/`service_tier`) plus native `anthropic-ratelimit-*` / `request-id` / `anthropic-organization-id` response headers. It is a **passthrough, not a transcode** — gateway headers `x-llm-proxy-llm-provider: anthropic`, `x-llm-proxy-llm-model: `, `x-llm-proxy-request-success` (`routingType` stays `model-based`; enabled by the per-upstream `routing[].upstreams[].llmConfigs.format: anthropic` field). An **OpenAI-shaped** request to `//chat/completions` → **404** (empty body) — the OpenAI route is simply not served on a native-Anthropic ingress (differs from Gemini's 400; both prove the ingress rejects the OpenAI wire format). Same `client_id`/`client_secret` CIE auth (no `client_id` → 401 "Client ID is not present", `www-authenticate: Client-ID-Enforcement`). Native ingress is **single-route only** (no multi-routing/fallback — that stays OpenAI-only); the upstream must speak native Anthropic (`provider: anthropic`/`api.anthropic.com` — **Bedrock is not usable** here). The `donkey.anthropic` adapter targets this route, but the SDK's own DDK proxies are `Format=OpenAI` — point `base_url` at a `Format=Anthropic` proxy to use the native surface. The roadmap "OAuth Client ID/Secret for configuring LLMs" is an **upstream/control-plane credential** concern and does **not** change this verified consumer `client_id`/`client_secret` request-header pair. | 2026-09-24 | live probe against `ddk-anthropic-inbound` (instance `21194086`, DDK/Sandbox) — captured in `python/tests/fixtures/anypoint/anthropic_inbound/` | | Ingress **Format** — Gemini-native route (#540) | `llm/client.py` (no SDK adapter — see note) | VERIFIED (LIVE) | A proxy provisioned `Format=Gemini` exposes a **native Gemini ingress** at `POST //models/:generateContent`. The Gemini request shape (`{"contents":[{"role":"user","parts":[{"text":…}]}]}`) → **200** with a native Gemini body (`candidates[].content.parts[].text`, `role:"model"`, `usageMetadata` incl. `thoughtsTokenCount`, `modelVersion`, `responseId`). It is a **passthrough, not a transcode** — response header `x-llm-proxy-model-based-routing-success: "Request passed through without model-based routing."` (`routingType` stays `model-based`; enabled by the per-upstream `routing[].upstreams[].llmConfigs.format: gemini` field). An **OpenAI-shaped** request to `//chat/completions` → **400** with a native-Gemini error envelope (a JSON *array* `[{"error":{"code":400,"status":"INVALID_ARGUMENT",…}}]`), confirming the ingress does not accept the OpenAI wire format. Same `client_id`/`client_secret` CIE auth (no `client_id` → 401 "Client ID is not present"). Native ingress is **single-route only** (no multi-routing/fallback — that stays OpenAI-only). **No SDK adapter ships** for this: a `google-genai` adapter is demand-driven (#540/#244, "do not guess it"); Gemini also remains reachable as an *upstream provider* behind an OpenAI ingress (supported-providers row below). | 2026-09-23 | live probe against `ddk-gemini-inbound` (instance `21193369`, DDK/Sandbox) — captured in `python/tests/fixtures/anypoint/gemini_inbound/` | | Endpoint/API surface | `llm/client.py` | VERIFIED (LIVE) | OpenAI **Responses API** works (`POST /openai-sdk/responses`, body `{model, input}`). Upstream registered as `https://api.openai.com/v1/`; `proxyUri http://0.0.0.0:8081/openai-sdk` | 2026-08-28 | `api:describe`, live probe | | Auth: header name / model | `core/transport.py`, `core/config.py` | VERIFIED (LIVE) | **`client_id` + `client_secret` request headers** (NOT bearer) — enforced by `client-id-enforcement` 1.3.3. A consumer **credential pair**, mapped to an Anypoint client application | 2026-08-28 | live probe + `policy:list` | @@ -442,7 +442,7 @@ provisioning API. Exact REST calls behind the CLI are now recorded in §12 | Google ADK | `google.adk.models.lite_llm.LiteLlm(model="openai/…", api_base, extra_headers)` | UNVERIFIED | — | — | — | | MS Agent Framework | `agent_framework.openai.OpenAIChatClient(model, base_url, api_key, default_headers)` | VERIFIED | Class path confirmed; the constructor kwarg is `model=` — `model_id` is **not** accepted. `base_url`/`api_key`/`default_headers` all accepted, so `connection_kwargs()` is unchanged. Pinned to agent-framework 1.19.0. | 2026-09-22 | #520 (`verify_frameworks.py --only agent_framework`) | | OpenAI Agents SDK | `agents.OpenAIChatCompletionsModel(model, openai_client=AsyncOpenAI(...))` | UNVERIFIED | — | — | — | -| Anthropic SDK | `anthropic.AsyncAnthropic(base_url, api_key, default_headers, http_client)` — model id per-call; the proxy's Anthropic-native ingress **Format is documented** but its exact path + live behavior are UNVERIFIED (see §2, #304) | UNVERIFIED | — | — | — | +| Anthropic SDK | `anthropic.AsyncAnthropic(base_url, api_key, default_headers, http_client)` — model id per-call; the proxy's Anthropic-native ingress route is now **LIVE-verified at `POST //v1/messages`** (see §2, #304) and requires a `Format=Anthropic` proxy. Row stays UNVERIFIED for the **constructor signature** only (#34). | UNVERIFIED | — | — | — | | CrewAI | `crewai.LLM(model="openai/…", base_url, api_key, extra_headers)` | UNVERIFIED | — | — | — | | LlamaIndex | `llama_index.llms.openai_like.OpenAILike(is_chat_model=True)` | UNVERIFIED | — | — | — | | Strands | `strands.models.openai.OpenAIModel(client_args={...})` | UNVERIFIED | — | — | — | diff --git a/python/examples/anthropic/README.md b/python/examples/anthropic/README.md index 6158375c..89249598 100644 --- a/python/examples/anthropic/README.md +++ b/python/examples/anthropic/README.md @@ -13,16 +13,15 @@ and the model id is a per-call argument (`c.messages.create(model=..., ...)`), not a constructor one. So this adapter exposes `client()` rather than the `model(...)` factory the OpenAI-compatible adapters use. -**Unverified dependency (docs/verified-apis.md §2, #304).** MuleSoft Model Proxy -offers a native **Anthropic** ingress Format — one of three selectable Formats -(OpenAI / Gemini / Anthropic) fixed at proxy creation — so the Anthropic-native -route is a real product capability. But every DDK proxy is `Format=OpenAI`, so +**Proxy route (docs/verified-apis.md §2, #304).** MuleSoft Model Proxy offers a +native **Anthropic** ingress Format — one of three selectable Formats (OpenAI / +Gemini / Anthropic) fixed at proxy creation. That route is now **LIVE-verified**: +a `Format=Anthropic` proxy serves the Anthropic Messages API natively at +`POST //v1/messages` (an OpenAI-shaped `/chat/completions` request 404s +there). Usage caveat: the SDK's own default DDK proxies are `Format=OpenAI`, so this client pointed at them reaches Claude only as an *upstream provider*, not -natively; and the exact Anthropic ingress path (likely `/v1/messages`) + live -behavior are not yet captured against a `Format=Anthropic` proxy. Override -`base_url` via `**kw` to point at a `Format=Anthropic` proxy to use the native -surface. The first `client()` call emits a one-time unverified-route warning; -this example does not make a live call. +natively. Override `base_url` via `**kw` to point at a `Format=Anthropic` proxy to +use the native surface. This example does not make a live call. > 📖 **Prefer reading to running?** The canonical walkthrough — install, > configure, and the manual equivalent — is in the docs: @@ -52,7 +51,7 @@ import httpx from anthropic import AsyncAnthropic c = AsyncAnthropic( - base_url=DONKEY_LLM_PROXY_URL, # UNVERIFIED path/behavior: use a Format=Anthropic proxy + base_url=DONKEY_LLM_PROXY_URL, # native /v1/messages route LIVE-verified: use a Format=Anthropic proxy api_key="unused", # proxy enforces client_id/client_secret headers default_headers={ "client_id": DONKEY_LLM_PROXY_CLIENT_ID, diff --git a/python/examples/anthropic/main.py b/python/examples/anthropic/main.py index 59a8bf85..51232110 100644 --- a/python/examples/anthropic/main.py +++ b/python/examples/anthropic/main.py @@ -8,16 +8,15 @@ from donkey_kit.integrations.anthropic import client c = client() # the model id is a per-call argument, not a constructor one -UNVERIFIED DEPENDENCY (docs/verified-apis.md §2, #304): MuleSoft Model Proxy -offers a native **Anthropic** ingress Format (one of three — OpenAI / Gemini / -Anthropic — fixed at proxy creation), so the Anthropic-native route is a real -product capability. But every DDK proxy is ``Format=OpenAI``, so this client -pointed at them reaches Claude only as an *upstream provider*, not natively; and -the exact Anthropic ingress path + live behavior are not yet captured against a -``Format=Anthropic`` proxy. Point ``base_url`` (via ``**kw``) at a -``Format=Anthropic`` proxy to use the native surface. The first ``client()`` -call emits a one-time unverified-route warning. This example constructs the -client but does NOT make a live call. +PROXY ROUTE (docs/verified-apis.md §2, #304): MuleSoft Model Proxy offers a +native **Anthropic** ingress Format (one of three — OpenAI / Gemini / Anthropic — +fixed at proxy creation). That route is now **LIVE-verified**: a +``Format=Anthropic`` proxy serves the Anthropic Messages API natively at +``POST //v1/messages``. Usage caveat: the SDK's own default DDK proxies +are ``Format=OpenAI``, so this client pointed at them reaches Claude only as an +*upstream provider*, not natively (``/v1/messages`` 404s there). Point +``base_url`` (via ``**kw``) at a ``Format=Anthropic`` proxy to use the native +surface. This example constructs the client but does NOT make a live call. """ from __future__ import annotations @@ -58,10 +57,10 @@ def main() -> None: print(f"Constructed native object: {type(c).__module__}.{type(c).__name__}") print( - "Construction is the SDK's verified surface. The Anthropic ingress Format " - "is documented but the exact path + live behavior are UNVERIFIED " - "(docs/verified-apis.md §2, #304) — point base_url at a Format=Anthropic " - "proxy, then call c.messages.create(model=..., ...) per Anthropic's own docs." + "The proxy's Anthropic-native route is LIVE-verified at " + "POST //v1/messages (docs/verified-apis.md §2, #304). Point " + "base_url at a Format=Anthropic proxy (DDK defaults are Format=OpenAI), " + "then call c.messages.create(model=..., ...) per Anthropic's own docs." ) diff --git a/python/src/donkey_kit/integrations/anthropic.py b/python/src/donkey_kit/integrations/anthropic.py index fc7bd1cc..647f9f23 100644 --- a/python/src/donkey_kit/integrations/anthropic.py +++ b/python/src/donkey_kit/integrations/anthropic.py @@ -11,36 +11,32 @@ So this adapter exposes ``client()`` rather than the ``model(...)`` factory the OpenAI-compatible adapters use. -UNVERIFIED DEPENDENCY (docs/verified-apis.md §2, #304): MuleSoft Model Proxy -*does* offer a native **Anthropic** ingress Format — it is one of three -selectable ingress Formats (OpenAI / Gemini / Anthropic), fixed at proxy -creation (docs.mulesoft.com/general/model-proxy). So the Anthropic-native route -is a real product capability, **but** two things keep it UNVERIFIED for this -adapter: (1) it must be *provisioned* that way — every DDK proxy is -``Format=OpenAI``, so pointing this client at them reaches Claude only as an -*upstream provider*, not via Anthropic's native surface; (2) the exact ingress -path (likely ``/v1/messages``) and its live behavior have not been captured -against a real ``Format=Anthropic`` proxy. Until that capture lands, point -``base_url`` (via ``**kw``) at a proxy provisioned with ``Format=Anthropic``. -The first ``client()`` call emits a one-time -:class:`~donkey_kit.core._verify.UnverifiedValueWarning`. - -Class names / kwargs UNVERIFIED — docs/verified-apis.md §8. +PROXY ROUTE (docs/verified-apis.md §2, #304): MuleSoft Model Proxy offers a +native **Anthropic** ingress Format — one of three selectable ingress Formats +(OpenAI / Gemini / Anthropic), fixed at proxy creation. That route is now +**LIVE-verified**: a ``Format=Anthropic`` proxy serves the Anthropic Messages API +natively at ``POST //v1/messages`` (200 with a native Anthropic body; +an OpenAI-shaped ``/chat/completions`` request 404s). Captured in +``python/tests/fixtures/anypoint/anthropic_inbound/``. + +Usage caveat (not a verification gap): the route requires a proxy *provisioned* +``Format=Anthropic``. The SDK's own default DDK proxies are ``Format=OpenAI``, so +pointing this client at them reaches Claude only as an *upstream provider*, not +via Anthropic's native surface (``/v1/messages`` 404s there). Point ``base_url`` +(via ``**kw``) at a ``Format=Anthropic`` proxy to use the native ingress. + +Class names / kwargs UNVERIFIED — docs/verified-apis.md §8 (#34). """ from __future__ import annotations -import warnings from typing import TYPE_CHECKING, Any -from ..core import _verify from ._base import Adapter, default_adapter if TYPE_CHECKING: from anthropic import AsyncAnthropic -_ROUTE_KEY = "anthropic.proxy_messages_route" - class AnthropicAdapter(Adapter): extra = "anthropic" @@ -48,19 +44,9 @@ class AnthropicAdapter(Adapter): def connection_kwargs(self) -> dict[str, Any]: """Governed kwargs for an ``AsyncAnthropic(**kwargs)`` you build yourself: proxy ``base_url``, the verified consumer-auth ``default_headers``, the - shared http client, and the ``api_key`` slot. Warns once that the proxy's - Anthropic-native route is unverified (see the module docstring).""" - if _ROUTE_KEY not in _verify._warned: - _verify._warned.add(_ROUTE_KEY) - warnings.warn( - "MuleSoft Model Proxy offers a native Anthropic ingress Format, but every " - "DDK proxy is provisioned Format=OpenAI, and the Anthropic ingress path + " - "live behavior are UNVERIFIED (docs/verified-apis.md §2, #304). Pointed at a " - "Format=OpenAI proxy this reaches Claude only as an upstream provider, not " - "natively; override base_url via **kw to target a Format=Anthropic proxy.", - _verify.UnverifiedValueWarning, - stacklevel=3, - ) + shared http client, and the ``api_key`` slot. The proxy's Anthropic-native + route is LIVE-verified but requires a ``Format=Anthropic`` proxy (see the + module docstring).""" conn = self._openai_connection() # base_url, api_key, default_headers return { "base_url": conn["base_url"], diff --git a/python/tests/fixtures/anypoint/anthropic_inbound/README.md b/python/tests/fixtures/anypoint/anthropic_inbound/README.md new file mode 100644 index 00000000..65137926 --- /dev/null +++ b/python/tests/fixtures/anypoint/anthropic_inbound/README.md @@ -0,0 +1,79 @@ +# Anthropic-native ingress — LIVE capture (docs/verified-apis.md §2, #304) + +Captured **2026-09-24** from a real deployed Agent Network LLM proxy provisioned +with the model-proxy **Format = Anthropic** ("native Anthropic ingress") in the +DDK sandbox, env **Sandbox** (svc id `14d3b31e-4e3b-4d90-b77a-63c9d6b7ea6a`, from +the `x-envoy-decorator-operation` header). Proxy asset `ddk-anthropic-inbound`, +API Manager instance **`21194086`**, deployed to the `shared-omni-gateway` Flex +Gateway, single-route passthrough to an Anthropic upstream +(`https://api.anthropic.com/`, model `claude-haiku-4-5-20251001`). + +This resolves issue **#304** ("verify whether the LLM proxy exposes an +Anthropic-native Messages API route, and at what path"), previously UNVERIFIED / +doc-read only. It is the sibling of the Gemini ingress captured in +`../gemini_inbound/`, the OpenAI ingress in `../llm_proxy/`, and the model-wallet +JWT ingress in `../model_wallet/`. The three selectable Formats (OpenAI / Gemini / +Anthropic) are documented at `docs.mulesoft.com/general/model-proxy`; all three +are now live-confirmed. + +## What is verified (all witnessed live, not from the provisioning YAML's own claim) + +- **The native route exists at `POST //v1/messages`.** The Anthropic + Messages API request shape (header `anthropic-version: 2023-06-01`, body + `{"model":…,"max_tokens":N,"messages":[{"role":"user","content":…}]}`) returns + **HTTP 200** with a genuine native Anthropic body — `type:"message"`, + `role:"assistant"`, `content[].text` (`"PONG"`), `stop_reason:"end_turn"`, + `usage.input_tokens`/`output_tokens`/`service_tier`. See `request.success.http` + + `responses.success.{body.json,headers.txt}`. +- **It is a passthrough, not a transcode.** The 200 carries native Anthropic + response headers (`anthropic-ratelimit-*`, `request-id`, + `anthropic-organization-id`, `anthropic-workspace-id`) forwarded verbatim, plus + the gateway's own `x-llm-proxy-llm-provider: anthropic`, + `x-llm-proxy-llm-model: claude-haiku-4-5-20251001`, and + `x-llm-proxy-request-success`. `routingType` stays `model-based`; the + passthrough is driven by the per-upstream + `routing[].upstreams[].llmConfigs.format: anthropic` field (see the provisioning + repo's `proxies/ddk-anthropic-inbound.yaml`). +- **The ingress is genuinely native Anthropic, not OpenAI.** An OpenAI-shaped + request to `//chat/completions` (`{"model":…,"messages":[…]}`) is + **rejected with HTTP 404** and an **empty body** — the OpenAI route is simply + not served on a native-Anthropic ingress. (Note the difference from the Gemini + ingress, where the same probe returned a native-Gemini **400**; both outcomes + prove the ingress does not accept the OpenAI wire format, but the Anthropic + ingress 404s the path outright rather than forwarding a malformed body + upstream.) See `reject.openai-shape.*`. We record the observed 404 shape; we do + not assert *why* the path is unserved beyond "the OpenAI-shape route is not + accepted". +- **Client ID Enforcement is on.** A request with no `client_id` header → + **HTTP 401** `{"error":"Client ID is not present"}` with + `www-authenticate: Client-ID-Enforcement` — identical to the OpenAI and Gemini + proxies' CIE behavior in `../llm_proxy/` and `../gemini_inbound/`. The consumer + auth pair is the same `client_id` / `client_secret` header pair verified in §2. + +## The roadmap "OAuth Client ID/Secret for configuring LLMs" does NOT change this + +Issue #304's second question: whether the roadmap line "Support OAuth Client +ID/Secret for configuring LLMs" changes the verified consumer +`client_id`/`client_secret` request-header pair. It does not — that item concerns +how a proxy's **upstream provider credential** is configured (a control-plane +concern; this proxy uses a static `x-api-key` to `api.anthropic.com`). The +**consumer-facing data-plane auth** on this native-Anthropic ingress is the same +`client_id`/`client_secret` request-header CIE pair witnessed here (401 without +it), unchanged from §2/§3. + +## How this was captured + +A consumer `client_id`/`client_secret` minted against instance `21194086` via the +`ddk-request-llm-proxy-access` flow was used to issue the three data-plane +requests above directly. Consumer credentials are **redacted** from +`request.success.http` and were never persisted here. + +## The `donkey.anthropic` adapter still requires a `Format=Anthropic` proxy + +Verifying the route lifts the adapter's open verification item, but the SDK's own +DDK proxies are provisioned `Format=OpenAI` — so `donkey.anthropic.client()` +pointed at them reaches Claude only as an *upstream provider*, not via this native +surface. Point `base_url` at a `Format=Anthropic` proxy (like `ddk-anthropic-inbound`) +to use the native Messages ingress. Native ingress is **single-route only** (no +multi-routing/fallback — that stays OpenAI-only) and the upstream must speak +native Anthropic (`provider: anthropic`; Bedrock is not usable here). diff --git a/python/tests/fixtures/anypoint/anthropic_inbound/reject.client-id-missing.body.json b/python/tests/fixtures/anypoint/anthropic_inbound/reject.client-id-missing.body.json new file mode 100644 index 00000000..b9d63d0e --- /dev/null +++ b/python/tests/fixtures/anypoint/anthropic_inbound/reject.client-id-missing.body.json @@ -0,0 +1 @@ +{"error":"Client ID is not present"} \ No newline at end of file diff --git a/python/tests/fixtures/anypoint/anthropic_inbound/reject.client-id-missing.headers.txt b/python/tests/fixtures/anypoint/anthropic_inbound/reject.client-id-missing.headers.txt new file mode 100644 index 00000000..d273cd27 --- /dev/null +++ b/python/tests/fixtures/anypoint/anthropic_inbound/reject.client-id-missing.headers.txt @@ -0,0 +1,10 @@ +HTTP/1.1 401 Unauthorized +www-authenticate: Client-ID-Enforcement +content-type: application/json +content-length: 36 +date: Thu, 24 Sep 2026 06:19:45 GMT +server: Anypoint Flex Gateway +x-correlation-id: a2343d2d-8082-4ab5-9fe8-920ceba27b0d +Strict-Transport-Security: max-age=31536000; includeSubdomains +Connection: Keep-Alive + diff --git a/python/tests/fixtures/anypoint/anthropic_inbound/reject.openai-shape.body.json b/python/tests/fixtures/anypoint/anthropic_inbound/reject.openai-shape.body.json new file mode 100644 index 00000000..e69de29b diff --git a/python/tests/fixtures/anypoint/anthropic_inbound/reject.openai-shape.headers.txt b/python/tests/fixtures/anypoint/anthropic_inbound/reject.openai-shape.headers.txt new file mode 100644 index 00000000..5ba3084e --- /dev/null +++ b/python/tests/fixtures/anypoint/anthropic_inbound/reject.openai-shape.headers.txt @@ -0,0 +1,19 @@ +HTTP/1.1 404 Not Found +date: Thu, 24 Sep 2026 06:19:45 GMT +vary: Accept-Encoding +server: Anypoint Flex Gateway +x-robots-tag: none +content-security-policy: default-src 'none'; frame-ancestors 'none' +cf-cache-status: DYNAMIC +cf-ray: a3ffade81afdcf3f-CMH +x-llm-proxy-routing-type: ModelBased +x-llm-proxy-routing-fallback: false +x-llm-proxy-llm-provider: anthropic +x-llm-proxy-llm-model: claude-haiku-4-5-20251001 +x-llm-proxy-model-based-routing-success: Request successfully matched. Provider: anthropic, Model: claude-haiku-4-5-20251001. +x-envoy-decorator-operation: api-instance-21194086.14d3b31e-4e3b-4d90-b77a-63c9d6b7ea6a.svc +transfer-encoding: chunked +x-correlation-id: f502f86e-1581-4692-9d9a-9fa0d2ca2110 +Strict-Transport-Security: max-age=31536000; includeSubdomains +Connection: Keep-Alive + diff --git a/python/tests/fixtures/anypoint/anthropic_inbound/request.success.http b/python/tests/fixtures/anypoint/anthropic_inbound/request.success.http new file mode 100644 index 00000000..72276378 --- /dev/null +++ b/python/tests/fixtures/anypoint/anthropic_inbound/request.success.http @@ -0,0 +1,8 @@ +POST /ddk-anthropic-inbound/v1/messages HTTP/1.1 +Host: shared-omni-gateway-qrpud8.5sc6y6-1.usa-e2.cloudhub.io +Content-Type: application/json +anthropic-version: 2023-06-01 +client_id: +client_secret: + +{"model":"claude-haiku-4-5-20251001","max_tokens":16,"messages":[{"role":"user","content":"Reply with the single word PONG."}]} diff --git a/python/tests/fixtures/anypoint/anthropic_inbound/responses.success.body.json b/python/tests/fixtures/anypoint/anthropic_inbound/responses.success.body.json new file mode 100644 index 00000000..9b262ac9 --- /dev/null +++ b/python/tests/fixtures/anypoint/anthropic_inbound/responses.success.body.json @@ -0,0 +1 @@ +{"model":"claude-haiku-4-5-20251001","id":"msg_011CfMnBFbok9WUWujYZkRvq","type":"message","role":"assistant","content":[{"type":"text","text":"PONG"}],"container":null,"stop_reason":"end_turn","stop_sequence":null,"stop_details":null,"usage":{"input_tokens":16,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":6,"service_tier":"standard","inference_geo":"not_available"},"diagnostics":null} \ No newline at end of file diff --git a/python/tests/fixtures/anypoint/anthropic_inbound/responses.success.headers.txt b/python/tests/fixtures/anypoint/anthropic_inbound/responses.success.headers.txt new file mode 100644 index 00000000..d549d794 --- /dev/null +++ b/python/tests/fixtures/anypoint/anthropic_inbound/responses.success.headers.txt @@ -0,0 +1,38 @@ +HTTP/1.1 200 OK +date: Thu, 24 Sep 2026 06:19:44 GMT +content-type: application/json +anthropic-ratelimit-input-tokens-limit: 10000000 +anthropic-ratelimit-input-tokens-remaining: 10000000 +anthropic-ratelimit-input-tokens-reset: 2026-09-24T06:19:44Z +anthropic-ratelimit-output-tokens-limit: 2000000 +anthropic-ratelimit-output-tokens-remaining: 2000000 +anthropic-ratelimit-output-tokens-reset: 2026-09-24T06:19:44Z +anthropic-ratelimit-requests-limit: 10000 +anthropic-ratelimit-requests-remaining: 9999 +anthropic-ratelimit-requests-reset: 2026-09-24T06:19:44Z +anthropic-ratelimit-tokens-limit: 12000000 +anthropic-ratelimit-tokens-remaining: 12000000 +anthropic-ratelimit-tokens-reset: 2026-09-24T06:19:44Z +server: Anypoint Flex Gateway +request-id: req_011CfMnBEhyZATzuKEENjEo6 +strict-transport-security: max-age=31536000; includeSubDomains; preload +anthropic-organization-id: 1777f1d3-b6db-42bf-80dc-b56f091c04f4 +anthropic-workspace-id: wrkspc_017xUVeur6fphn4DJU4DLEb8 +traceresponse: 00-42bc519da34324cfb5972e599654393b-1d2f66742a77b4c5-01 +vary: Accept-Encoding +x-robots-tag: none +content-security-policy: default-src 'none'; frame-ancestors 'none' +cf-cache-status: DYNAMIC +cf-ray: a3ffade0dfe5cf3f-CMH +x-llm-proxy-routing-type: ModelBased +x-llm-proxy-routing-fallback: false +x-llm-proxy-llm-provider: anthropic +x-llm-proxy-llm-model: claude-haiku-4-5-20251001 +x-llm-proxy-model-based-routing-success: Request successfully matched. Provider: anthropic, Model: claude-haiku-4-5-20251001. +x-llm-proxy-request-success: Request completed successfully. Provider: anthropic, Model: claude-haiku-4-5-20251001. +x-envoy-decorator-operation: api-instance-21194086.14d3b31e-4e3b-4d90-b77a-63c9d6b7ea6a.svc +transfer-encoding: chunked +x-correlation-id: e433ce3a-8ca1-4973-ad59-527b14e47e3b +Strict-Transport-Security: max-age=31536000; includeSubdomains +Connection: Keep-Alive + diff --git a/python/tests/unit/test_adapter_ergonomics.py b/python/tests/unit/test_adapter_ergonomics.py index c3e6e9b4..9c21e2ef 100644 --- a/python/tests/unit/test_adapter_ergonomics.py +++ b/python/tests/unit/test_adapter_ergonomics.py @@ -136,15 +136,11 @@ def test_agent_framework_connection_kwargs_are_the_openai_connection() -> None: def test_anthropic_connection_kwargs_carry_proxy_and_shared_client() -> None: - import warnings - from donkey_kit.integrations.anthropic import AnthropicAdapter - with warnings.catch_warnings(): - # The Anthropic ingress Format is documented but its exact path + live behavior - # are unverified (#304); connection_kwargs() warns once. Not what this test asserts. - warnings.simplefilter("ignore") - kw = AnthropicAdapter(_cfg(), _http()).connection_kwargs() + # The proxy's Anthropic-native route is LIVE-verified (#304), so connection_kwargs() + # no longer emits an unverified-route warning. + kw = AnthropicAdapter(_cfg(), _http()).connection_kwargs() assert kw["base_url"] == "https://proxy" assert "client_id" in kw["default_headers"] assert kw["http_client"] is not None @@ -424,7 +420,7 @@ def test_factory_and_connection_kwargs_do_not_drift( captured = _install_native_stub(monkeypatch, f.native_module, f.native_attr) with warnings.catch_warnings(): - warnings.simplefilter("ignore") # anthropic warns once about its unverified route + warnings.simplefilter("ignore") # suppress any one-time adapter construction warnings factory(*f.args) # The factory built the native object through the process-wide cached default diff --git a/website/content/concepts/verification.mdx b/website/content/concepts/verification.mdx index 48992dd4..01a07fbf 100644 --- a/website/content/concepts/verification.mdx +++ b/website/content/concepts/verification.mdx @@ -37,7 +37,7 @@ how a blocked value flips to verified — is documented for contributors in | Area | Status | What the SDK does about it | |---|---|---| -| Non-OpenAI proxy **ingress Format** (Anthropic / Gemini) | Anthropic Format documented, route path/behavior unverified (#304); Gemini-native ingress **VERIFIED (LIVE)**, no SDK adapter (#540) | The default data-plane path is **Format=OpenAI**. MuleSoft Model Proxy [documents](https://docs.mulesoft.com/general/model-proxy) three selectable ingress Formats (OpenAI / Anthropic / Gemini) fixed at proxy creation. The **Gemini**-native ingress is now **VERIFIED (LIVE)**: a `Format=Gemini` proxy serves a native `POST …/models/:generateContent` passthrough (200, native Gemini body; OpenAI-shape → 400), captured in `tests/fixtures/anypoint/gemini_inbound/` (#540). The **Anthropic**-native Messages route is documented but its exact path + live behavior are still **UNVERIFIED** (no `Format=Anthropic` proxy provisioned yet); `donkey.anthropic.client()` points `AsyncAnthropic` at it and warns on first use (#304). Either way the SDK ships **no** `donkey.gemini` adapter — a native `google-genai` adapter is demand-driven (#244); Gemini and Claude models are also reachable as **upstream providers** behind an OpenAI-format ingress via model-based routing, which works today. Ingress Format is a property of the provisioned proxy, not an SDK setting (no `llm_proxy_format` field). See [Which wire format does your proxy speak?](/frameworks#which-wire-format-does-your-proxy-speak). | +| Non-OpenAI proxy **ingress Format** (Anthropic / Gemini) | Both native ingress Formats now **VERIFIED (LIVE)**: Anthropic Messages route (#304) and Gemini `:generateContent` route (#540); no `donkey.gemini` SDK adapter | The default data-plane path is **Format=OpenAI**. MuleSoft Model Proxy [documents](https://docs.mulesoft.com/general/model-proxy) three selectable ingress Formats (OpenAI / Anthropic / Gemini) fixed at proxy creation, and both non-OpenAI Formats are now live-confirmed. The **Anthropic**-native Messages route is **VERIFIED (LIVE)**: a `Format=Anthropic` proxy serves `POST //v1/messages` natively (200, native Anthropic body; OpenAI-shape `/chat/completions` → 404), captured in `tests/fixtures/anypoint/anthropic_inbound/` (#304). The **Gemini**-native ingress is likewise **VERIFIED (LIVE)**: a `Format=Gemini` proxy serves a native `POST …/models/:generateContent` passthrough (200, native Gemini body; OpenAI-shape → 400), captured in `tests/fixtures/anypoint/gemini_inbound/` (#540). `donkey.anthropic.client()` points `AsyncAnthropic` at the native route — but it requires a `Format=Anthropic` proxy (default DDK proxies are `Format=OpenAI`). The SDK ships **no** `donkey.gemini` adapter — a native `google-genai` adapter is demand-driven (#244); Gemini and Claude models are also reachable as **upstream providers** behind an OpenAI-format ingress via model-based routing, which works today. Ingress Format is a property of the provisioned proxy, not an SDK setting (no `llm_proxy_format` field). See [Which wire format does your proxy speak?](/frameworks#which-wire-format-does-your-proxy-speak). | | Framework **constructor signatures** (docs/verified-apis.md §8) | Unverified per framework | Adapters target recorded class names; a mismatch surfaces via the verification harness / nightly matrix rather than silently misbehaving. Agent Framework's client raises `blocked on verification` today. | | MCP tool discovery + binding ([Phase 2](/tool-access)) | Unverified | `donkey.tools.discover` and the binding classes raise `blocked on verification`. | | Exchange publish API ([Phase 2](/publishing)) | Partially verified | First-class MCP/agent asset types exist, but the exact publication and discovery token vocabulary is unverified. `donkey publish` and Exchange search remain blocked rather than sending the SDK's assumed `mcp` / `a2a-agent` values. | diff --git a/website/content/frameworks/anthropic.mdx b/website/content/frameworks/anthropic.mdx index 6d95a04c..d52d96b6 100644 --- a/website/content/frameworks/anthropic.mdx +++ b/website/content/frameworks/anthropic.mdx @@ -46,17 +46,17 @@ reply = await llm.messages.create( There's no first-party Agent Fabric TypeScript SDK yet (it's on the [roadmap](/concepts/verification)). The native match here is the official **`@anthropic-ai/sdk`** client pointed at the proxy — same `client_id` / -`client_secret` header pair, model id passed per call. **Caveat (unverified -path):** MuleSoft Model Proxy offers a native Anthropic ingress Format, but every -DDK proxy is provisioned `Format=OpenAI` and the Anthropic ingress path/behavior -are not yet captured — point `baseURL` at a `Format=Anthropic` proxy before -relying on this path (#304). +`client_secret` header pair, model id passed per call. **Caveat (proxy Format):** +the native Anthropic ingress route is LIVE-verified at `POST //v1/messages`, +but it requires a proxy provisioned `Format=Anthropic`; every default DDK proxy is +`Format=OpenAI`, so point `baseURL` at a `Format=Anthropic` proxy to use the native +surface (#304). ```typescript import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ - baseURL: process.env.DONKEY_LLM_PROXY_URL, // UNVERIFIED path/behavior: use a Format=Anthropic proxy + baseURL: process.env.DONKEY_LLM_PROXY_URL, // native /v1/messages route LIVE-verified: use a Format=Anthropic proxy apiKey: "unused", // required slot; proxy uses the headers below defaultHeaders: { client_id: process.env.DONKEY_LLM_PROXY_CLIENT_ID!, @@ -130,18 +130,16 @@ you can always drop the factory and construct `AsyncAnthropic` by hand. > therefore takes no model argument at all; you supply the model id yourself > on every call. -> **UNVERIFIED path: the proxy's Anthropic-native ingress.** -> MuleSoft Model Proxy offers a **native Anthropic ingress Format** — one of -> three selectable Formats (OpenAI / Gemini / Anthropic) fixed at proxy creation -> ([MuleSoft docs](https://docs.mulesoft.com/general/model-proxy)) — so the -> Anthropic-native route is a real product capability. Two gaps keep it -> unverified: (1) it must be *provisioned* that way, and every DDK proxy is -> `Format=OpenAI`, so pointing `AsyncAnthropic` at them reaches Claude only as an -> *upstream provider*, not natively; (2) the exact Anthropic ingress path (likely -> `/v1/messages`) and its live behavior have **not** been captured against a real -> `Format=Anthropic` proxy. `client()` emits a one-time runtime warning flagging -> this rather than silently assuming the route works — point `base_url` at a -> `Format=Anthropic` proxy and confirm against your own sandbox before depending -> on it (#304). +> **The proxy's Anthropic-native ingress is LIVE-verified — but requires a +> `Format=Anthropic` proxy.** MuleSoft Model Proxy offers a **native Anthropic +> ingress Format** — one of three selectable Formats (OpenAI / Gemini / Anthropic) +> fixed at proxy creation ([MuleSoft docs](https://docs.mulesoft.com/general/model-proxy)). +> A live capture against a `Format=Anthropic` proxy confirms the native route at +> `POST //v1/messages` (200 with a native Anthropic body; an +> OpenAI-shaped `/chat/completions` request 404s), on the same `client_id` / +> `client_secret` CIE auth (#304). The one remaining caveat is provisioning: every +> default DDK proxy is `Format=OpenAI`, so pointing `AsyncAnthropic` at them +> reaches Claude only as an *upstream provider*, not natively (`/v1/messages` 404s +> there). Point `base_url` at a `Format=Anthropic` proxy to use the native surface. diff --git a/website/content/frameworks/index.mdx b/website/content/frameworks/index.mdx index f70aba30..8fe1927d 100644 --- a/website/content/frameworks/index.mdx +++ b/website/content/frameworks/index.mdx @@ -87,7 +87,7 @@ adapter to the proxy, not the other way round. | Proxy ingress **Format** | Point it at | Status | |---|---|---| | **OpenAI** | `donkey.llm.client()` or any framework adapter | **VERIFIED (LIVE)** — the SDK's verified data-plane path | -| **Anthropic** | `donkey.anthropic.client()` (native `AsyncAnthropic`) | **DOCUMENTED, path unverified** — MuleSoft docs confirm an Anthropic ingress Format exists, but the exact route path + live behavior are pending a capture against a `Format=Anthropic` proxy (every DDK proxy is `Format=OpenAI`); the factory warns on first use (#304) | +| **Anthropic** | `donkey.anthropic.client()` (native `AsyncAnthropic`) | **VERIFIED (LIVE)** — a `Format=Anthropic` proxy serves the native Anthropic Messages route at `POST //v1/messages` (OpenAI-shape `/chat/completions` → 404), captured in `tests/fixtures/anypoint/anthropic_inbound/` (#304). Requires a `Format=Anthropic` proxy — every default DDK proxy is `Format=OpenAI`, where Claude is reachable only as an upstream provider | | **Gemini** | *No SDK adapter* — point a native `google-genai` client at `…/models/:generateContent` yourself | **VERIFIED (LIVE)** — a `Format=Gemini` proxy serves a native Gemini `:generateContent` passthrough (#540). The SDK ships **no** `donkey.gemini` adapter (demand-driven, #244); reach Gemini as an **upstream provider** behind an OpenAI-format ingress instead — see below | diff --git a/website/public/concepts/verification.md b/website/public/concepts/verification.md index dac6904c..df37d96c 100644 --- a/website/public/concepts/verification.md +++ b/website/public/concepts/verification.md @@ -33,7 +33,7 @@ how a blocked value flips to verified — is documented for contributors in | Area | Status | What the SDK does about it | |---|---|---| -| Non-OpenAI proxy **ingress Format** (Anthropic / Gemini) | Anthropic Format documented, route path/behavior unverified (#304); Gemini-native ingress **VERIFIED (LIVE)**, no SDK adapter (#540) | The default data-plane path is **Format=OpenAI**. MuleSoft Model Proxy [documents](https://docs.mulesoft.com/general/model-proxy) three selectable ingress Formats (OpenAI / Anthropic / Gemini) fixed at proxy creation. The **Gemini**-native ingress is now **VERIFIED (LIVE)**: a `Format=Gemini` proxy serves a native `POST …/models/:generateContent` passthrough (200, native Gemini body; OpenAI-shape → 400), captured in `tests/fixtures/anypoint/gemini_inbound/` (#540). The **Anthropic**-native Messages route is documented but its exact path + live behavior are still **UNVERIFIED** (no `Format=Anthropic` proxy provisioned yet); `donkey.anthropic.client()` points `AsyncAnthropic` at it and warns on first use (#304). Either way the SDK ships **no** `donkey.gemini` adapter — a native `google-genai` adapter is demand-driven (#244); Gemini and Claude models are also reachable as **upstream providers** behind an OpenAI-format ingress via model-based routing, which works today. Ingress Format is a property of the provisioned proxy, not an SDK setting (no `llm_proxy_format` field). See [Which wire format does your proxy speak?](https://donkey-development-kit.github.io/donkey-development-kit/frameworks.md#which-wire-format-does-your-proxy-speak). | +| Non-OpenAI proxy **ingress Format** (Anthropic / Gemini) | Both native ingress Formats now **VERIFIED (LIVE)**: Anthropic Messages route (#304) and Gemini `:generateContent` route (#540); no `donkey.gemini` SDK adapter | The default data-plane path is **Format=OpenAI**. MuleSoft Model Proxy [documents](https://docs.mulesoft.com/general/model-proxy) three selectable ingress Formats (OpenAI / Anthropic / Gemini) fixed at proxy creation, and both non-OpenAI Formats are now live-confirmed. The **Anthropic**-native Messages route is **VERIFIED (LIVE)**: a `Format=Anthropic` proxy serves `POST //v1/messages` natively (200, native Anthropic body; OpenAI-shape `/chat/completions` → 404), captured in `tests/fixtures/anypoint/anthropic_inbound/` (#304). The **Gemini**-native ingress is likewise **VERIFIED (LIVE)**: a `Format=Gemini` proxy serves a native `POST …/models/:generateContent` passthrough (200, native Gemini body; OpenAI-shape → 400), captured in `tests/fixtures/anypoint/gemini_inbound/` (#540). `donkey.anthropic.client()` points `AsyncAnthropic` at the native route — but it requires a `Format=Anthropic` proxy (default DDK proxies are `Format=OpenAI`). The SDK ships **no** `donkey.gemini` adapter — a native `google-genai` adapter is demand-driven (#244); Gemini and Claude models are also reachable as **upstream providers** behind an OpenAI-format ingress via model-based routing, which works today. Ingress Format is a property of the provisioned proxy, not an SDK setting (no `llm_proxy_format` field). See [Which wire format does your proxy speak?](https://donkey-development-kit.github.io/donkey-development-kit/frameworks.md#which-wire-format-does-your-proxy-speak). | | Framework **constructor signatures** (docs/verified-apis.md §8) | Unverified per framework | Adapters target recorded class names; a mismatch surfaces via the verification harness / nightly matrix rather than silently misbehaving. Agent Framework's client raises `blocked on verification` today. | | MCP tool discovery + binding ([Phase 2](https://donkey-development-kit.github.io/donkey-development-kit/tool-access.md)) | Unverified | `donkey.tools.discover` and the binding classes raise `blocked on verification`. | | Exchange publish API ([Phase 2](https://donkey-development-kit.github.io/donkey-development-kit/publishing.md)) | Partially verified | First-class MCP/agent asset types exist, but the exact publication and discovery token vocabulary is unverified. `donkey publish` and Exchange search remain blocked rather than sending the SDK's assumed `mcp` / `a2a-agent` values. | diff --git a/website/public/frameworks.md b/website/public/frameworks.md index f715ffde..670953d3 100644 --- a/website/public/frameworks.md +++ b/website/public/frameworks.md @@ -74,7 +74,7 @@ adapter to the proxy, not the other way round. | Proxy ingress **Format** | Point it at | Status | |---|---|---| | **OpenAI** | `donkey.llm.client()` or any framework adapter | **VERIFIED (LIVE)** — the SDK's verified data-plane path | -| **Anthropic** | `donkey.anthropic.client()` (native `AsyncAnthropic`) | **DOCUMENTED, path unverified** — MuleSoft docs confirm an Anthropic ingress Format exists, but the exact route path + live behavior are pending a capture against a `Format=Anthropic` proxy (every DDK proxy is `Format=OpenAI`); the factory warns on first use (#304) | +| **Anthropic** | `donkey.anthropic.client()` (native `AsyncAnthropic`) | **VERIFIED (LIVE)** — a `Format=Anthropic` proxy serves the native Anthropic Messages route at `POST //v1/messages` (OpenAI-shape `/chat/completions` → 404), captured in `tests/fixtures/anypoint/anthropic_inbound/` (#304). Requires a `Format=Anthropic` proxy — every default DDK proxy is `Format=OpenAI`, where Claude is reachable only as an upstream provider | | **Gemini** | *No SDK adapter* — point a native `google-genai` client at `…/models/:generateContent` yourself | **VERIFIED (LIVE)** — a `Format=Gemini` proxy serves a native Gemini `:generateContent` passthrough (#540). The SDK ships **no** `donkey.gemini` adapter (demand-driven, #244); reach Gemini as an **upstream provider** behind an OpenAI-format ingress instead — see below | **Ingress Format is not upstream provider routing.** "Anthropic" and "Gemini" diff --git a/website/public/frameworks/anthropic.md b/website/public/frameworks/anthropic.md index 509ed975..d9072369 100644 --- a/website/public/frameworks/anthropic.md +++ b/website/public/frameworks/anthropic.md @@ -37,17 +37,17 @@ reply = await llm.messages.create( There's no first-party Agent Fabric TypeScript SDK yet (it's on the [roadmap](https://donkey-development-kit.github.io/donkey-development-kit/concepts/verification.md)). The native match here is the official **`@anthropic-ai/sdk`** client pointed at the proxy — same `client_id` / -`client_secret` header pair, model id passed per call. **Caveat (unverified -path):** MuleSoft Model Proxy offers a native Anthropic ingress Format, but every -DDK proxy is provisioned `Format=OpenAI` and the Anthropic ingress path/behavior -are not yet captured — point `baseURL` at a `Format=Anthropic` proxy before -relying on this path (#304). +`client_secret` header pair, model id passed per call. **Caveat (proxy Format):** +the native Anthropic ingress route is LIVE-verified at `POST //v1/messages`, +but it requires a proxy provisioned `Format=Anthropic`; every default DDK proxy is +`Format=OpenAI`, so point `baseURL` at a `Format=Anthropic` proxy to use the native +surface (#304). ```typescript import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ - baseURL: process.env.DONKEY_LLM_PROXY_URL, // UNVERIFIED path/behavior: use a Format=Anthropic proxy + baseURL: process.env.DONKEY_LLM_PROXY_URL, // native /v1/messages route LIVE-verified: use a Format=Anthropic proxy apiKey: "unused", // required slot; proxy uses the headers below defaultHeaders: { client_id: process.env.DONKEY_LLM_PROXY_CLIENT_ID!, @@ -118,19 +118,17 @@ you can always drop the factory and construct `AsyncAnthropic` by hand. > therefore takes no model argument at all; you supply the model id yourself > on every call. -> **UNVERIFIED path: the proxy's Anthropic-native ingress.** -> MuleSoft Model Proxy offers a **native Anthropic ingress Format** — one of -> three selectable Formats (OpenAI / Gemini / Anthropic) fixed at proxy creation -> ([MuleSoft docs](https://docs.mulesoft.com/general/model-proxy)) — so the -> Anthropic-native route is a real product capability. Two gaps keep it -> unverified: (1) it must be *provisioned* that way, and every DDK proxy is -> `Format=OpenAI`, so pointing `AsyncAnthropic` at them reaches Claude only as an -> *upstream provider*, not natively; (2) the exact Anthropic ingress path (likely -> `/v1/messages`) and its live behavior have **not** been captured against a real -> `Format=Anthropic` proxy. `client()` emits a one-time runtime warning flagging -> this rather than silently assuming the route works — point `base_url` at a -> `Format=Anthropic` proxy and confirm against your own sandbox before depending -> on it (#304). +> **The proxy's Anthropic-native ingress is LIVE-verified — but requires a +> `Format=Anthropic` proxy.** MuleSoft Model Proxy offers a **native Anthropic +> ingress Format** — one of three selectable Formats (OpenAI / Gemini / Anthropic) +> fixed at proxy creation ([MuleSoft docs](https://docs.mulesoft.com/general/model-proxy)). +> A live capture against a `Format=Anthropic` proxy confirms the native route at +> `POST //v1/messages` (200 with a native Anthropic body; an +> OpenAI-shaped `/chat/completions` request 404s), on the same `client_id` / +> `client_secret` CIE auth (#304). The one remaining caveat is provisioning: every +> default DDK proxy is `Format=OpenAI`, so pointing `AsyncAnthropic` at them +> reaches Claude only as an *upstream provider*, not natively (`/v1/messages` 404s +> there). Point `base_url` at a `Format=Anthropic` proxy to use the native surface. See the [error taxonomy](https://donkey-development-kit.github.io/donkey-development-kit/errors.md) for how proxy rejections surface as typed exceptions, and the [verification policy](https://donkey-development-kit.github.io/donkey-development-kit/concepts/verification.md) page for diff --git a/website/public/llms-full.txt b/website/public/llms-full.txt index ead77e17..51b978a3 100644 --- a/website/public/llms-full.txt +++ b/website/public/llms-full.txt @@ -805,7 +805,7 @@ adapter to the proxy, not the other way round. | Proxy ingress **Format** | Point it at | Status | |---|---|---| | **OpenAI** | `donkey.llm.client()` or any framework adapter | **VERIFIED (LIVE)** — the SDK's verified data-plane path | -| **Anthropic** | `donkey.anthropic.client()` (native `AsyncAnthropic`) | **DOCUMENTED, path unverified** — MuleSoft docs confirm an Anthropic ingress Format exists, but the exact route path + live behavior are pending a capture against a `Format=Anthropic` proxy (every DDK proxy is `Format=OpenAI`); the factory warns on first use (#304) | +| **Anthropic** | `donkey.anthropic.client()` (native `AsyncAnthropic`) | **VERIFIED (LIVE)** — a `Format=Anthropic` proxy serves the native Anthropic Messages route at `POST //v1/messages` (OpenAI-shape `/chat/completions` → 404), captured in `tests/fixtures/anypoint/anthropic_inbound/` (#304). Requires a `Format=Anthropic` proxy — every default DDK proxy is `Format=OpenAI`, where Claude is reachable only as an upstream provider | | **Gemini** | *No SDK adapter* — point a native `google-genai` client at `…/models/:generateContent` yourself | **VERIFIED (LIVE)** — a `Format=Gemini` proxy serves a native Gemini `:generateContent` passthrough (#540). The SDK ships **no** `donkey.gemini` adapter (demand-driven, #244); reach Gemini as an **upstream provider** behind an OpenAI-format ingress instead — see below | **Ingress Format is not upstream provider routing.** "Anthropic" and "Gemini" @@ -1621,17 +1621,17 @@ reply = await llm.messages.create( There's no first-party Agent Fabric TypeScript SDK yet (it's on the [roadmap](https://donkey-development-kit.github.io/donkey-development-kit/concepts/verification.md)). The native match here is the official **`@anthropic-ai/sdk`** client pointed at the proxy — same `client_id` / -`client_secret` header pair, model id passed per call. **Caveat (unverified -path):** MuleSoft Model Proxy offers a native Anthropic ingress Format, but every -DDK proxy is provisioned `Format=OpenAI` and the Anthropic ingress path/behavior -are not yet captured — point `baseURL` at a `Format=Anthropic` proxy before -relying on this path (#304). +`client_secret` header pair, model id passed per call. **Caveat (proxy Format):** +the native Anthropic ingress route is LIVE-verified at `POST //v1/messages`, +but it requires a proxy provisioned `Format=Anthropic`; every default DDK proxy is +`Format=OpenAI`, so point `baseURL` at a `Format=Anthropic` proxy to use the native +surface (#304). ```typescript import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ - baseURL: process.env.DONKEY_LLM_PROXY_URL, // UNVERIFIED path/behavior: use a Format=Anthropic proxy + baseURL: process.env.DONKEY_LLM_PROXY_URL, // native /v1/messages route LIVE-verified: use a Format=Anthropic proxy apiKey: "unused", // required slot; proxy uses the headers below defaultHeaders: { client_id: process.env.DONKEY_LLM_PROXY_CLIENT_ID!, @@ -1702,19 +1702,17 @@ you can always drop the factory and construct `AsyncAnthropic` by hand. > therefore takes no model argument at all; you supply the model id yourself > on every call. -> **UNVERIFIED path: the proxy's Anthropic-native ingress.** -> MuleSoft Model Proxy offers a **native Anthropic ingress Format** — one of -> three selectable Formats (OpenAI / Gemini / Anthropic) fixed at proxy creation -> ([MuleSoft docs](https://docs.mulesoft.com/general/model-proxy)) — so the -> Anthropic-native route is a real product capability. Two gaps keep it -> unverified: (1) it must be *provisioned* that way, and every DDK proxy is -> `Format=OpenAI`, so pointing `AsyncAnthropic` at them reaches Claude only as an -> *upstream provider*, not natively; (2) the exact Anthropic ingress path (likely -> `/v1/messages`) and its live behavior have **not** been captured against a real -> `Format=Anthropic` proxy. `client()` emits a one-time runtime warning flagging -> this rather than silently assuming the route works — point `base_url` at a -> `Format=Anthropic` proxy and confirm against your own sandbox before depending -> on it (#304). +> **The proxy's Anthropic-native ingress is LIVE-verified — but requires a +> `Format=Anthropic` proxy.** MuleSoft Model Proxy offers a **native Anthropic +> ingress Format** — one of three selectable Formats (OpenAI / Gemini / Anthropic) +> fixed at proxy creation ([MuleSoft docs](https://docs.mulesoft.com/general/model-proxy)). +> A live capture against a `Format=Anthropic` proxy confirms the native route at +> `POST //v1/messages` (200 with a native Anthropic body; an +> OpenAI-shaped `/chat/completions` request 404s), on the same `client_id` / +> `client_secret` CIE auth (#304). The one remaining caveat is provisioning: every +> default DDK proxy is `Format=OpenAI`, so pointing `AsyncAnthropic` at them +> reaches Claude only as an *upstream provider*, not natively (`/v1/messages` 404s +> there). Point `base_url` at a `Format=Anthropic` proxy to use the native surface. See the [error taxonomy](https://donkey-development-kit.github.io/donkey-development-kit/errors.md) for how proxy rejections surface as typed exceptions, and the [verification policy](https://donkey-development-kit.github.io/donkey-development-kit/concepts/verification.md) page for @@ -4483,7 +4481,7 @@ how a blocked value flips to verified — is documented for contributors in | Area | Status | What the SDK does about it | |---|---|---| -| Non-OpenAI proxy **ingress Format** (Anthropic / Gemini) | Anthropic Format documented, route path/behavior unverified (#304); Gemini-native ingress **VERIFIED (LIVE)**, no SDK adapter (#540) | The default data-plane path is **Format=OpenAI**. MuleSoft Model Proxy [documents](https://docs.mulesoft.com/general/model-proxy) three selectable ingress Formats (OpenAI / Anthropic / Gemini) fixed at proxy creation. The **Gemini**-native ingress is now **VERIFIED (LIVE)**: a `Format=Gemini` proxy serves a native `POST …/models/:generateContent` passthrough (200, native Gemini body; OpenAI-shape → 400), captured in `tests/fixtures/anypoint/gemini_inbound/` (#540). The **Anthropic**-native Messages route is documented but its exact path + live behavior are still **UNVERIFIED** (no `Format=Anthropic` proxy provisioned yet); `donkey.anthropic.client()` points `AsyncAnthropic` at it and warns on first use (#304). Either way the SDK ships **no** `donkey.gemini` adapter — a native `google-genai` adapter is demand-driven (#244); Gemini and Claude models are also reachable as **upstream providers** behind an OpenAI-format ingress via model-based routing, which works today. Ingress Format is a property of the provisioned proxy, not an SDK setting (no `llm_proxy_format` field). See [Which wire format does your proxy speak?](https://donkey-development-kit.github.io/donkey-development-kit/frameworks.md#which-wire-format-does-your-proxy-speak). | +| Non-OpenAI proxy **ingress Format** (Anthropic / Gemini) | Both native ingress Formats now **VERIFIED (LIVE)**: Anthropic Messages route (#304) and Gemini `:generateContent` route (#540); no `donkey.gemini` SDK adapter | The default data-plane path is **Format=OpenAI**. MuleSoft Model Proxy [documents](https://docs.mulesoft.com/general/model-proxy) three selectable ingress Formats (OpenAI / Anthropic / Gemini) fixed at proxy creation, and both non-OpenAI Formats are now live-confirmed. The **Anthropic**-native Messages route is **VERIFIED (LIVE)**: a `Format=Anthropic` proxy serves `POST //v1/messages` natively (200, native Anthropic body; OpenAI-shape `/chat/completions` → 404), captured in `tests/fixtures/anypoint/anthropic_inbound/` (#304). The **Gemini**-native ingress is likewise **VERIFIED (LIVE)**: a `Format=Gemini` proxy serves a native `POST …/models/:generateContent` passthrough (200, native Gemini body; OpenAI-shape → 400), captured in `tests/fixtures/anypoint/gemini_inbound/` (#540). `donkey.anthropic.client()` points `AsyncAnthropic` at the native route — but it requires a `Format=Anthropic` proxy (default DDK proxies are `Format=OpenAI`). The SDK ships **no** `donkey.gemini` adapter — a native `google-genai` adapter is demand-driven (#244); Gemini and Claude models are also reachable as **upstream providers** behind an OpenAI-format ingress via model-based routing, which works today. Ingress Format is a property of the provisioned proxy, not an SDK setting (no `llm_proxy_format` field). See [Which wire format does your proxy speak?](https://donkey-development-kit.github.io/donkey-development-kit/frameworks.md#which-wire-format-does-your-proxy-speak). | | Framework **constructor signatures** (docs/verified-apis.md §8) | Unverified per framework | Adapters target recorded class names; a mismatch surfaces via the verification harness / nightly matrix rather than silently misbehaving. Agent Framework's client raises `blocked on verification` today. | | MCP tool discovery + binding ([Phase 2](https://donkey-development-kit.github.io/donkey-development-kit/tool-access.md)) | Unverified | `donkey.tools.discover` and the binding classes raise `blocked on verification`. | | Exchange publish API ([Phase 2](https://donkey-development-kit.github.io/donkey-development-kit/publishing.md)) | Partially verified | First-class MCP/agent asset types exist, but the exact publication and discovery token vocabulary is unverified. `donkey publish` and Exchange search remain blocked rather than sending the SDK's assumed `mcp` / `a2a-agent` values. | From 9ac171618e19405fbd095f5f2b6702c84863773d Mon Sep 17 00:00:00 2001 From: Tommaso Bolis Date: Thu, 24 Sep 2026 08:42:24 +0200 Subject: [PATCH 3/4] chore(scripts): return to develop and delete bump branch after merge (#555) Co-authored-by: Cursor --- scripts/bump-version.sh | 46 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index c78ab24f..66d68523 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -10,7 +10,11 @@ # then opens a squash PR into `develop` (branch → develop is always a squash # merge, per [[ddk-merge-strategy]]) and enables GitHub auto-merge so it lands # the moment the blocking CI gates pass (base-only, typecheck-and-lint incl. -# mypy --strict + lint-imports, and the test matrix). +# mypy --strict + lint-imports, and the test matrix). It then waits for the +# merge, switches back to `develop` (fast-forwarded), and deletes the bump +# branch locally and on origin. If the PR doesn't merge within +# MERGE_TIMEOUT_SECS (default 1800), it still returns to `develop` but keeps +# the branch. # # The version scheme is PEP 440, milestone-driven, on the pre-release ladder # 0.1.0.dev0 < … < 0.1.0a1 < 0.1.0b1 < 0.1.0rc1 < 0.1.0 @@ -34,6 +38,8 @@ REPO="Donkey-Development-Kit/donkey-development-kit" BASE_BRANCH="develop" PYPROJECT="python/pyproject.toml" INIT_PY="python/src/donkey_kit/__init__.py" +MERGE_POLL_SECS="${MERGE_POLL_SECS:-15}" +MERGE_TIMEOUT_SECS="${MERGE_TIMEOUT_SECS:-1800}" DRY_RUN=0 ASSUME_YES=0 @@ -243,6 +249,9 @@ if [ "$DRY_RUN" -eq 1 ]; then "$REPO" "$BASE_BRANCH" "$BRANCH" "$PR_TITLE" printf '\033[33m[dry-run]\033[0m would run: gh pr merge --repo %s --squash --auto --delete-branch --subject %q --head %s\n' \ "$REPO" "chore: bump version to ${NEW}" "$BRANCH" + printf '\033[33m[dry-run]\033[0m would wait for the merge, then: git switch %s && git pull --ff-only origin %s\n' \ + "$BASE_BRANCH" "$BASE_BRANCH" + printf '\033[33m[dry-run]\033[0m would run: git branch -D %s && git push origin --delete %s\n' "$BRANCH" "$BRANCH" info "dry run complete — no branch, commit, or PR was created" exit 0 fi @@ -264,11 +273,44 @@ if gh pr merge "$PR_NUM" --repo "$REPO" \ --subject "chore: bump version to ${NEW} (#${PR_NUM})"; then printf '\n\033[32m✓\033[0m PR #%s opened with auto-merge enabled.\n' "$PR_NUM" printf ' It will squash-merge into %s automatically once CI is green.\n' "$BASE_BRANCH" - printf ' Watch it: gh pr checks %s --repo %s --watch\n' "$PR_NUM" "$REPO" else printf '\n\033[33m!\033[0m PR #%s is open, but enabling auto-merge failed.\n' "$PR_NUM" printf ' This usually means repository auto-merge is disabled. Once CI is green, merge with:\n' printf ' gh pr merge %s --repo %s --squash --delete-branch --subject "chore: bump version to %s (#%s)"\n' \ "$PR_NUM" "$REPO" "$NEW" "$PR_NUM" + git switch "$BASE_BRANCH" >/dev/null 2>&1 || true exit 1 fi + +# --- Wait for the merge, then return to develop and delete the bump branch ---- +# The branch can only be deleted AFTER the merge: deleting the head branch of an +# open PR closes it unmerged. +info "waiting for PR #$PR_NUM to merge (polling every ${MERGE_POLL_SECS}s, timeout ${MERGE_TIMEOUT_SECS}s; Ctrl-C to stop waiting)" +state="OPEN" +waited=0 +while [ "$waited" -lt "$MERGE_TIMEOUT_SECS" ]; do + state=$(gh pr view "$PR_NUM" --repo "$REPO" --json state --jq .state 2>/dev/null || echo "UNKNOWN") + case "$state" in MERGED|CLOSED) break ;; esac + sleep "$MERGE_POLL_SECS" + waited=$((waited + MERGE_POLL_SECS)) +done + +run git switch "$BASE_BRANCH" +run git pull --ff-only origin "$BASE_BRANCH" + +if [ "$state" != "MERGED" ]; then + printf '\n\033[33m!\033[0m PR #%s is %s — kept branch %s (local and remote).\n' "$PR_NUM" "$state" "$BRANCH" + printf ' Watch it: gh pr checks %s --repo %s --watch\n' "$PR_NUM" "$REPO" + printf ' After it merges: git branch -D %s && git push origin --delete %s\n' "$BRANCH" "$BRANCH" + exit 1 +fi + +# Squash merges leave the branch tip unreachable from develop, so -d would refuse. +run git branch -D "$BRANCH" +if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then + run git push origin --delete "$BRANCH" +fi +run git fetch --prune origin + +printf '\n\033[32m✓\033[0m PR #%s merged; on %s at %s, branch %s deleted locally and remotely.\n' \ + "$PR_NUM" "$BASE_BRANCH" "$NEW" "$BRANCH" From ac85a164f7830610c1f85617947daab44e859dea Mon Sep 17 00:00:00 2001 From: Tommaso Bolis Date: Thu, 24 Sep 2026 11:51:03 +0200 Subject: [PATCH 4/4] =?UTF-8?q?docs(verify):=20live-verify=20Amazon=20Bedr?= =?UTF-8?q?ock=20Guardrails=20refusal=20(=C2=A70.3,=20#568)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live-captured the Amazon Bedrock Guardrails content-safety refusal against ddk-bedrock-guardrails (2026-09-24), closing the Bedrock half of #253. Reconciles errors.py comments, verified-apis.md §4, unsupported-boundary, and the website surfaces; records the capture as a classify()-contract sibling fixture with a new contract-test row. No classify() change. Closes #568 Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/unsupported-boundary.md | 2 +- docs/verified-apis.md | 24 ++++++++------- python/src/donkey_kit/core/errors.py | 30 +++++++++++-------- python/tests/fixtures/rejections/README.md | 28 ++++++++++++----- .../reject.content-safety-bedrock.body.json | 1 + .../reject.content-safety-bedrock.headers.txt | 4 +++ python/tests/unit/test_rejection_contract.py | 16 ++++++++++ website/content/concepts/verification.mdx | 2 +- website/content/errors.mdx | 14 +++++---- .../reference/unsupported-boundary.mdx | 2 +- website/public/concepts/verification.md | 2 +- website/public/errors.md | 14 +++++---- website/public/llms-full.txt | 18 ++++++----- .../public/reference/unsupported-boundary.md | 2 +- 14 files changed, 104 insertions(+), 55 deletions(-) create mode 100644 python/tests/fixtures/rejections/reject.content-safety-bedrock.body.json create mode 100644 python/tests/fixtures/rejections/reject.content-safety-bedrock.headers.txt diff --git a/docs/unsupported-boundary.md b/docs/unsupported-boundary.md index dba39fb3..499b5789 100644 --- a/docs/unsupported-boundary.md +++ b/docs/unsupported-boundary.md @@ -26,7 +26,7 @@ Classification: | LLM Token Based Rate Limit `429` response: `x-token-limit`, `x-token-remaining`, and `x-token-reset` | `core.budget.Budget`; `core.errors.classify` | **Documented and public** | The [LLM Token Based Rate Limit policy](https://docs.mulesoft.com/gateway/latest/policies-included-llm-token-rate-limit) documents all three headers and the reset unit. The SDK's rejection fixture is `VERIFIED (LIVE)` in `verified-apis.md` §4. | | LLM PII Detection `403` rejection body (`error.type = "pii_detected"`) | `core.errors.classify` | **Documented and public** | The [LLM PII Detection policy](https://docs.mulesoft.com/gateway/latest/policies-included-llm-pii-detection) documents the rejection body. The SDK also captured it live; see `verified-apis.md` §4. | | Regex Prompt Guard body and Injection Protection response header | `core.errors.classify` | **Documented and public** | MuleSoft documents `matched_patterns` in [Regex Prompt Guard](https://docs.mulesoft.com/gateway/latest/policies-included-regex-prompt-guard) and `x-injection-protection: blocked` in [Injection Protection](https://docs.mulesoft.com/gateway/latest/policies-included-injection-protection). These discriminators are documented but still pending direct live capture; see `verified-apis.md` §4. | -| Azure Content Safety and Amazon Bedrock Guardrails rejection headers and bodies | `core.errors.classify` | **Documented and public** | The [Azure Content Safety](https://docs.mulesoft.com/gateway/latest/policies-included-azure-content-safety) and [Amazon Bedrock Guardrails](https://docs.mulesoft.com/gateway/latest/policies-included-bedrock-guardrails) policy pages document their action/reason headers and `403` bodies. These shapes are pending direct live capture; see `verified-apis.md` §4. | +| Azure Content Safety and Amazon Bedrock Guardrails rejection headers and bodies | `core.errors.classify` | **Documented and public** | The [Azure Content Safety](https://docs.mulesoft.com/gateway/latest/policies-included-azure-content-safety) and [Amazon Bedrock Guardrails](https://docs.mulesoft.com/gateway/latest/policies-included-bedrock-guardrails) policy pages document their action/reason headers and `403` bodies. Both are now live-captured — Azure 2026-09-22, Bedrock 2026-09-24 (#568); see `verified-apis.md` §4. | | Upstream provider error pass-through: non-`429` `4xx` nested error envelope; generic `5xx` status family | `core.errors.classify` | **Documented but no SLA for third-party use** | A real Model Proxy passed an OpenAI `400 model_not_found` envelope through verbatim, including `code`, `type`, and `param` (`VERIFIED (LIVE)`, `verified-apis.md` §4). MuleSoft's public Model Proxy page does not state a compatibility contract for provider error envelopes, whose schema ultimately belongs to the upstream provider. The SDK maps any generic `5xx` to `UpstreamModelError` by status only and assumes no body shape. Owner: DDK maintainers; retain the captured provider fixture and classify changed envelopes conservatively. | | Successful-response budget window in `x-llm-proxy-ratelimit` prose | `core.budget.Budget` | **Documented but no SLA for third-party use** | Captured from a real Model Proxy and parsed fail-open: an absent or changed sentence is ignored, never fatal. The exact prose contract is `VERIFIED (LIVE)` in `verified-apis.md` §4 but is not stated in the public policy page. Owner: DDK maintainers; re-capture on gateway-policy changes. | | Gateway identity and routing response metadata: `x-request-id`, `x-envoy-decorator-operation`, and `x-llm-proxy-{routing-type,routing-fallback,llm-provider,llm-model}` | `core.lastcall.LastCall`; `core.transport.DonkeyAsyncClient` | **Documented but no SLA for third-party use** | Captured from a real Model Proxy (`verified-apis.md` §2 and §3). Parsers are fail-open: missing or unrecognised values become `None`, and only an explicit `routing-fallback: true` changes retry behavior. Owner: DDK maintainers; contract changes require new fixtures before parser changes. | diff --git a/docs/verified-apis.md b/docs/verified-apis.md index 9bfcbf35..39eab37a 100644 --- a/docs/verified-apis.md +++ b/docs/verified-apis.md @@ -17,10 +17,12 @@ > removed. The regex-prompt-guard (`403` + > `matched_patterns`) and Azure content-safety (`403` + `…-action: reject`) > rejection shapes were also `VERIFIED (LIVE)` on 2026-09-22 against the deployed -> `ddk-injection-guard` / `ddk-azure-content-safety` proxies (#253). Still +> `ddk-injection-guard` / `ddk-azure-content-safety` proxies (#253), and the +> Bedrock Guardrails content-safety shape (same `…-action: reject` family) on +> 2026-09-24 against `ddk-bedrock-guardrails` (#568). Still > `UNVERIFIED`: the Injection Protection body (`x-injection-protection: blocked`, -> a distinct policy — no proxy deployed) and any other content-moderation / -> Bedrock-guardrail shape (§4, pending live capture #253), and the **framework +> a distinct policy — no proxy deployed) and any other unrecognised +> content-moderation shape (§4, pending live capture #253), and the **framework > constructor/binding names** (§§8–10). §11 > records A2D shapes > (`VERIFIED-SHAPE-ONLY`), used only to validate SDK value types; no blocked @@ -300,22 +302,24 @@ is these rows, not an `Unverified(...)` flip: | Shape | HTTP | Discriminator | Maps to | Source | |---|---|---|---|---| | Regex Prompt Guard | `403` | top-level `matched_patterns` list (flat-string `error`) | `PromptInjectionBlocked` (`policy="regex-prompt-guard"`) | [Regex Prompt Guard policy](https://docs.mulesoft.com/gateway/latest/policies-included-regex-prompt-guard) (v1.11.4); VERIFIED (LIVE) 2026-09-22 on `ddk-injection-guard` (instance 21179713) | -| Content safety / guardrails | `403` | `x-llm-proxy-azure-content-safety-action` / `x-llm-proxy-bedrock-guardrail-action` == `reject`; reasons in the sibling `…-reason` header | `ContentSafetyBlocked` (parses `categories`) | [Azure Content Safety policy](https://docs.mulesoft.com/gateway/latest/policies-included-azure-content-safety) (v1.13.0); [Amazon Bedrock Guardrails policy](https://docs.mulesoft.com/gateway/latest/policies-included-bedrock-guardrails) (v1.13.0); VERIFIED (LIVE) 2026-09-22 on `ddk-azure-content-safety` (instance 21180957, Azure verified; Bedrock still documented-only) | +| Content safety / guardrails | `403` | `x-llm-proxy-azure-content-safety-action` / `x-llm-proxy-bedrock-guardrail-action` == `reject`; reasons in the sibling `…-reason` header | `ContentSafetyBlocked` (parses `categories`) | [Azure Content Safety policy](https://docs.mulesoft.com/gateway/latest/policies-included-azure-content-safety) (v1.13.0); [Amazon Bedrock Guardrails policy](https://docs.mulesoft.com/gateway/latest/policies-included-bedrock-guardrails) (v1.13.0); VERIFIED (LIVE) 2026-09-22 on `ddk-azure-content-safety` (instance 21180957) + 2026-09-24 on `ddk-bedrock-guardrails` (#568) | | Unrecognised refusal (fall-through) | any non-429 `4xx` | matches **none** of the discriminators above; no nested `error` envelope; no `www-authenticate` (e.g. an unrecognised `403`) | generic `PolicyViolation` (`policy="unknown"`), message says **shape unconfirmed** and names the observed status + `x-llm-proxy-*` headers | UNVERIFIED — no known contract; capture + type via #184/#253 | -The Regex Prompt Guard (row 7) and Azure Content Safety (row 8) shapes are now -LIVE-captured (2026-09-22, #253). Still **uncaptured**: the **Injection +The Regex Prompt Guard (row 7) and Content Safety (row 8) shapes are now +LIVE-captured — Regex Prompt Guard and Azure Content Safety on 2026-09-22 +(#253), and Bedrock Guardrails on 2026-09-24 (#568), the sibling vendor of the +same row-8 `…-action: reject` family. Still **uncaptured**: the **Injection Protection** body (`x-injection-protection: blocked`, a policy *distinct* from Regex Prompt Guard — no proxy running it is deployed) and any **other -content-moderation / federated-guardrail** shape (e.g. Bedrock Guardrails). +unrecognised content-moderation / federated-guardrail** shape. Rather than guess a type for them, `classify()` surfaces any such unrecognised refusal **honestly** — a `PolicyViolation` whose message states the shape is unconfirmed and whose remediation asks the operator to file the observed status/headers/body so the shape can be typed (#184). This is the last row above; it is deliberately **not** an `AuthError`, even for a `403`, once the -verified `www-authenticate` auth shape is excluded. Closing these last shapes -needs an Injection Protection (and a Bedrock Guardrails) proxy deployed to -capture against; that residual keeps #253 open. +verified `www-authenticate` auth shape is excluded. Closing the last shape +needs an Injection Protection proxy deployed to capture against; that residual +keeps #253 open. **Budget window emission — two forms, keyed on outcome not status class (LIVE-VERIFIED).** The gateway signals its token-rate-limit window in two diff --git a/python/src/donkey_kit/core/errors.py b/python/src/donkey_kit/core/errors.py index e91a3dea..738f6235 100644 --- a/python/src/donkey_kit/core/errors.py +++ b/python/src/donkey_kit/core/errors.py @@ -477,15 +477,18 @@ def classify( (flagged reasons parsed from the sibling ``...-reason`` header). Also checked before the auth rule. - Both shapes are now **live-verified** (2026-09-22, #253): Regex Prompt Guard - against ``ddk-injection-guard`` and Azure Content Safety against - ``ddk-azure-content-safety`` (docs/verified-apis.md §4). These shapes have no - ``_verify.py`` constant — ``classify()`` reads them straight from the - response — so the record is the §4 rows, not a ``verified=True`` flip. Amazon - Bedrock Guardrails (same ``...-action: reject`` header family) and the - Injection Protection body (``x-injection-protection: blocked``, a distinct - policy) stay documented-only — no proxy running them is deployed to capture - (#253). Any other content-moderation / federated-guardrail shape still falls + All three provider-backed guardrail shapes are now **live-verified**: Regex + Prompt Guard against ``ddk-injection-guard`` and Azure Content Safety against + ``ddk-azure-content-safety`` (both 2026-09-22, #253), and Amazon Bedrock + Guardrails against ``ddk-bedrock-guardrails`` (2026-09-24, #568) — the same + ``...-action: reject`` header family, now confirmed against a real Bedrock + upstream (docs/verified-apis.md §4). These shapes have no ``_verify.py`` + constant — ``classify()`` reads them straight from the response — so the + record is the §4 rows, not a ``verified=True`` flip. The Injection Protection + body (``x-injection-protection: blocked``, a distinct policy) is the only + content-moderation shape that stays documented-only — no proxy running it is + deployed to capture (#253). Any other content-moderation / federated-guardrail + shape still falls through to a generic :class:`PolicyViolation` whose message names what was observed and says the shape is unconfirmed (#184). Because auth is the verified 401 / ``www-authenticate`` @@ -541,10 +544,10 @@ def classify( # Content-safety / guardrails policy: 403 + a vendor `...-action: reject` # header (Azure Content Safety / Amazon Bedrock Guardrails, docs/verified-apis.md §4). - # Azure verified LIVE 2026-09-22 (#253); Bedrock still documented-only. + # Azure verified LIVE 2026-09-22, Bedrock verified LIVE 2026-09-24 (#253/#568). # Checked before the 401/403 → auth rule because a moderation block is not an - # auth failure. Keyed on the header, not the body, so the body-less Bedrock - # reject is caught too. + # auth failure. Keyed on the header, not the body, so a reject with an + # unexpected or absent body is still caught. cs = _content_safety_reject(response) if cs is not None: vendor, categories = cs @@ -799,7 +802,8 @@ def _code_str(value: Any) -> str | None: # headers — an ``...-action`` (``allow``|``reject``) and a comma-separated # ``...-reason``. Both are ``x-llm-proxy--...`` (docs/verified-apis.md §4). # Azure Content Safety verified LIVE 2026-09-22 against ddk-azure-content-safety -# (#253); Amazon Bedrock Guardrails still documented-only (no proxy deployed). +# (#253); Amazon Bedrock Guardrails verified LIVE 2026-09-24 against +# ddk-bedrock-guardrails (#568). _CONTENT_SAFETY_VENDORS: tuple[tuple[str, str, str], ...] = ( ( "x-llm-proxy-azure-content-safety-action", diff --git a/python/tests/fixtures/rejections/README.md b/python/tests/fixtures/rejections/README.md index e5343a73..bb7d4eb0 100644 --- a/python/tests/fixtures/rejections/README.md +++ b/python/tests/fixtures/rejections/README.md @@ -32,6 +32,19 @@ deployed** to capture them, so their bytes remain honest placeholders (the deployed injection-lane proxy runs Regex Prompt Guard = row 7, a distinct policy). Rows 1/2/5 remain LIVE from the original 2026-08-28 captures. +**Bedrock Guardrails sibling (#568, 2026-09-24):** row 8's second vendor was +live-captured against `ddk-bedrock-guardrails` — a `403` carrying +`x-llm-proxy-bedrock-guardrail-action: reject` / +`x-llm-proxy-bedrock-guardrail-reason: content_filter` and a +`{"error":…,"categories":["content_filter"]}` body. Its bytes live beside the +Azure capture as `reject.content-safety-bedrock.{headers.txt,body.json}` and are +asserted by `test_row8_bedrock_guardrails_variant_is_content_safety_blocked`. +This sibling is a **classify()-contract fixture only** — it is *not* wired into +`simulator.fixtures.SHAPES` (row 8 stays one canonical shape, keyed on the +vendor `…-action: reject` header family), so, unlike every served fixture, it is +deliberately **absent from `fixtures.lock`**; the contract test's assertions +guard its bytes instead. + ## The eight rows | # | Shape | Fixture | classify() → | Discriminator | Provenance | @@ -43,7 +56,7 @@ policy). Rows 1/2/5 remain LIVE from the original 2026-08-28 captures. | 5 | Upstream provider 4xx | `../anypoint/llm_proxy/reject.model-not-found.body.json` | `UpstreamRequestError` | non-429 4xx, nested `error` with `code`/`type`/`param` | LIVE, 2026-08-28 (OpenAI passthrough) | | 6 | Upstream 5xx | `reject.upstream-5xx.{headers.txt,body.empty}` | `UpstreamModelError` (retryable) | 5xx status range (no competing discriminator) | **SYNTHETIC** — status-range classification only; no live capture, no invented body. | | 7 | Regex Prompt Guard | `reject.regex-prompt-guard.{headers.txt,body.json}` | `PromptInjectionBlocked` (`policy="regex-prompt-guard"`) | 403 + top-level `matched_patterns` list (flat-string `error`) | **VERIFIED (LIVE), 2026-09-22 (#253)** — body matches the live capture byte-for-byte against `ddk-injection-guard` (instance 21179713). [Regex Prompt Guard policy](https://docs.mulesoft.com/gateway/latest/policies-included-regex-prompt-guard) (v1.11.4). | -| 8 | Content safety / guardrails | `reject.content-safety.{headers.txt,body.json}` | `ContentSafetyBlocked` (parses `categories`) | 403 + `x-llm-proxy--…-action: reject` (Azure Content Safety / Bedrock Guardrails) | **VERIFIED (LIVE), 2026-09-22 (#253)** — discriminator headers + body shape confirmed against `ddk-azure-content-safety` (instance 21180957); `.body.json`/`-reason` hold a live-captured category set (`severity_hate,severity_violence`; categories are prompt-dependent). [Azure Content Safety policy](https://docs.mulesoft.com/gateway/latest/policies-included-azure-content-safety) (v1.13.0). | +| 8 | Content safety / guardrails | `reject.content-safety.{headers.txt,body.json}` (Azure); `reject.content-safety-bedrock.{headers.txt,body.json}` (Bedrock sibling) | `ContentSafetyBlocked` (parses `categories`) | 403 + `x-llm-proxy--…-action: reject` (Azure Content Safety / Bedrock Guardrails) | **VERIFIED (LIVE)** — Azure 2026-09-22 (#253): discriminator headers + body shape confirmed against `ddk-azure-content-safety` (instance 21180957), `.body.json`/`-reason` hold a live-captured set (`severity_hate,severity_violence`; categories are prompt-dependent). Bedrock Guardrails 2026-09-24 (#568): same `…-action: reject` family confirmed against `ddk-bedrock-guardrails` (`content_filter`). [Azure Content Safety policy](https://docs.mulesoft.com/gateway/latest/policies-included-azure-content-safety) (v1.13.0); [Amazon Bedrock Guardrails policy](https://docs.mulesoft.com/gateway/latest/policies-included-bedrock-guardrails) (v1.13.0). | Rows 1, 2, 5 **alias** the existing live captures in `../anypoint/llm_proxy/` (referenced, not copied — moving them would break that directory's contract-test @@ -51,12 +64,13 @@ helpers and provenance chain). Rows 3, 4, 6 live here because they are not (yet) live-captured; their bodies are zero-byte `.body.empty` placeholders — the honest "shape unknown" marker, never a guessed body. -Rows 7 and 8 (#289) also live here and are now **VERIFIED (LIVE), 2026-09-22 -(#253)**: their discriminators (the `matched_patterns` list, the vendor -`…-action: reject` header) were confirmed against the deployed provisioning -proxies, not just pinned from the policy pages. The regex-prompt-guard body -matched the committed bytes exactly; the content-safety fixture now carries a -live-captured category set. These shapes have **no `_verify.py` constant** to +Rows 7 and 8 (#289) also live here and are now **VERIFIED (LIVE)**: their +discriminators (the `matched_patterns` list, the vendor `…-action: reject` +header) were confirmed against the deployed provisioning proxies — Regex Prompt +Guard and Azure Content Safety on 2026-09-22 (#253), Bedrock Guardrails on +2026-09-24 (#568) — not just pinned from the policy pages. The regex-prompt-guard +body matched the committed bytes exactly; the content-safety fixtures carry a +live-captured category set for each vendor. These shapes have **no `_verify.py` constant** to flip — `classify()` reads them straight from the response, so the verification record is the docs/verified-apis.md §4 rows plus this table, not an `Unverified(...)` guard. Unlike rows 3/4/6, row 7 needs a non-empty body: its diff --git a/python/tests/fixtures/rejections/reject.content-safety-bedrock.body.json b/python/tests/fixtures/rejections/reject.content-safety-bedrock.body.json new file mode 100644 index 00000000..765cb2b1 --- /dev/null +++ b/python/tests/fixtures/rejections/reject.content-safety-bedrock.body.json @@ -0,0 +1 @@ +{"error":"Content blocked by Amazon Bedrock Guardrails","categories":["content_filter"]} diff --git a/python/tests/fixtures/rejections/reject.content-safety-bedrock.headers.txt b/python/tests/fixtures/rejections/reject.content-safety-bedrock.headers.txt new file mode 100644 index 00000000..e75db03e --- /dev/null +++ b/python/tests/fixtures/rejections/reject.content-safety-bedrock.headers.txt @@ -0,0 +1,4 @@ +HTTP/1.1 403 Forbidden +content-type: application/json +x-llm-proxy-bedrock-guardrail-action: reject +x-llm-proxy-bedrock-guardrail-reason: content_filter diff --git a/python/tests/unit/test_rejection_contract.py b/python/tests/unit/test_rejection_contract.py index a4ee8224..8deb7bae 100644 --- a/python/tests/unit/test_rejection_contract.py +++ b/python/tests/unit/test_rejection_contract.py @@ -119,3 +119,19 @@ def test_row8_content_safety_is_content_safety_blocked_not_auth() -> None: # on a hate-speech probe against ddk-azure-content-safety, #253). assert err.categories == ["severity_hate", "severity_violence"] assert err.remediation # required, non-empty + + +def test_row8_bedrock_guardrails_variant_is_content_safety_blocked() -> None: + # The Bedrock Guardrails sibling of row 8: a different vendor header family + # (x-llm-proxy-bedrock-guardrail-*) and a single `content_filter` category, + # live-captured against ddk-bedrock-guardrails on 2026-09-24 (#568). It must + # type identically to the Azure capture — same ContentSafetyBlocked, keyed on + # the `...-action: reject` header — which exercises the bedrock branch of + # _CONTENT_SAFETY_VENDORS the Azure fixture never reaches. This capture is a + # classify()-contract sibling of row 8, not a simulator-served shape, so it is + # deliberately absent from fixtures.lock (see rejections/README.md). + err = classify(_response(REJECTIONS, "content-safety-bedrock", 403)) + assert isinstance(err, ContentSafetyBlocked) + assert err.policy == "content-safety" + assert err.categories == ["content_filter"] + assert err.remediation # required, non-empty diff --git a/website/content/concepts/verification.mdx b/website/content/concepts/verification.mdx index 01a07fbf..4fa6cf62 100644 --- a/website/content/concepts/verification.mdx +++ b/website/content/concepts/verification.mdx @@ -46,7 +46,7 @@ how a blocked value flips to verified — is documented for contributors in | Budget query endpoint ([Phase 1](/budget)) | **Missing upstream** | Budget is reported in-band on response headers only, so `remaining` is last-known-good; `observed_at` exposes the staleness. | | Whether Anypoint Monitoring ingests OTLP GenAI spans ([Phase 1](/telemetry)) | Unverified | The SDK promises "exports OTLP" and lets the sink be your choice. | | A provisioning control plane | **Cut** | Not built. It would compete with API Manager and Terraform — see [Roadmap](/roadmap). | -| Injection-protection / Bedrock-guardrails / fall-through bodies | Not captured | The **regex prompt-guard** (`matched_patterns` list) and **Azure content-safety** (`…-action: reject` header) bodies are now **live-captured** (2026-09-22, #253). Still uncaptured: the distinct **Injection Protection** policy's body (typed by the `x-injection-protection: blocked` header; no such proxy is deployed), the **Bedrock Guardrails** sibling of content-safety, and the undiscriminated content-moderation fall-through to a generic `PolicyViolation` — all pinned from the policy pages, and no verification row flips until a live round-trip confirms them. No invented discriminator — tracked in #253. | +| Injection-protection / fall-through bodies | Not captured | The **regex prompt-guard** (`matched_patterns` list) and **content-safety** (`…-action: reject` header) bodies are now **live-captured** — Azure Content Safety 2026-09-22 (#253) and its **Bedrock Guardrails** sibling 2026-09-24 (#568). Still uncaptured: the distinct **Injection Protection** policy's body (typed by the `x-injection-protection: blocked` header; no such proxy is deployed) and the undiscriminated content-moderation fall-through to a generic `PolicyViolation` — pinned from the policy pages, and no verification row flips until a live round-trip confirms them. No invented discriminator — tracked in #253. | | First-party **TypeScript SDK** | Planned | TypeScript examples reach the proxy over its OpenAI-compatible API via the `openai` / `@anthropic-ai/sdk` clients; no first-party TS package is implied until it ships. | ## What this means for you diff --git a/website/content/errors.mdx b/website/content/errors.mdx index d4d4ca4d..545b2863 100644 --- a/website/content/errors.mdx +++ b/website/content/errors.mdx @@ -10,11 +10,12 @@ branch on the governance outcome instead of parsing bodies. `classify()` types eight rejection shapes. **Six are live-verified** against the sandbox proxies (auth, PII, token rate limit, upstream, **regex prompt guard**, -and **Azure content safety** — the last two confirmed 2026-09-22, [#253]). +and **content safety** — the last two confirmed 2026-09-22, [#253], with content +safety's **Bedrock Guardrails** vendor added 2026-09-24, [#568]). Still pending live capture: **injection protection** (typed by its header discriminator, body uncaptured — a distinct policy from regex prompt guard, with -no proxy deployed), Amazon **Bedrock guardrails**, and the **undiscriminated -content-moderation** fall-through — all tracked in [#253]. The critical lesson: +no proxy deployed) and the **undiscriminated content-moderation** fall-through — +both tracked in [#253]. The critical lesson: **neither the status code nor the shape of the `error` value alone is a sufficient discriminator** — a `403` can be PII, a regex-guard block, a content-safety block (all policy blocks) *or* auth, and the same nested-object @@ -27,7 +28,7 @@ authoritative discriminator is the error **`type`** plus specific headers. | PII detected | `403` | nested `{"error":{type:"pii_detected"}}`, **no** `www-authenticate` | `PIIDetected` (parses `entities`) | live | | Injection protection | `400` | header `x-injection-protection: blocked` (**not** the status) | `PromptInjectionBlocked` | pending (#253) | | Regex prompt guard | `403` | top-level `matched_patterns` list (flat `error`) | `PromptInjectionBlocked` (`policy="regex-prompt-guard"`) | live (#253) | -| Content safety / guardrails | `403` | header `x-llm-proxy--…-action: reject` (Azure Content Safety / Bedrock Guardrails) | `ContentSafetyBlocked` (parses `categories`) | live — Azure; Bedrock pending (#253) | +| Content safety / guardrails | `403` | header `x-llm-proxy--…-action: reject` (Azure Content Safety / Bedrock Guardrails) | `ContentSafetyBlocked` (parses `categories`) | live — Azure (#253) + Bedrock (#568) | | Token rate limit | `429` | **empty body**; `x-token-limit`/`-remaining`/`-reset` headers (ms) | `TokenBudgetExceeded` (`retry_after` derived) | live | | Content moderation (undiscriminated) | `4xx` | falls through — no nested `error`, no injection/guard/safety discriminator | generic `PolicyViolation` | pending (#253) | | Upstream provider 4xx | `4xx` | nested `error` object **with** `code`/`type`/`param` — in an OpenAI-style object envelope `{"error":{…}}` **or** a Gemini-style list envelope `[{"error":{…}}]` (`status`→`error_type`, #548) | `UpstreamRequestError` | live | @@ -43,6 +44,7 @@ Client-ID enforcement (`401`) is a **consumer-auth** case, not one of the eight policy-rejection rows. [#253]: https://github.com/Donkey-Development-Kit/donkey-development-kit/issues/253 +[#568]: https://github.com/Donkey-Development-Kit/donkey-development-kit/issues/568 ## The exception tree @@ -56,7 +58,7 @@ DonkeyError # base of the whole tree │ ├─ PIIDetected # 403, type=pii_detected; .entities │ ├─ TokenBudgetExceeded # 429; .retry_after (seconds) │ ├─ PromptInjectionBlocked # x-injection-protection: blocked, or regex matched_patterns (pending #253) -│ └─ ContentSafetyBlocked # Azure Content Safety / Bedrock Guardrails vendor reject header; .categories (pending #253) +│ └─ ContentSafetyBlocked # Azure Content Safety / Bedrock Guardrails vendor reject header; .categories ├─ GatewayUnavailable # transport failure — gateway unreachable, NO response; .base_url/.cause (ungoverned) ├─ UpstreamRequestError # upstream 4xx; .code/.error_type/.param └─ UpstreamModelError # upstream 5xx — provider error, retryable @@ -82,7 +84,7 @@ never burn an exhausted budget or replay a blocked prompt. | `PIIDetected` | `403`, nested `type: "pii_detected"`, **no** `www-authenticate` | **No** — a `PolicyViolation`, never retried. | Remove or redact the flagged values (`.entities`), or relax the policy's entity list in API Manager. | | `TokenBudgetExceeded` | `429`, empty body, `x-token-*` headers | **Not immediately** — never auto-retried; only worth retrying *after* the window resets. | Wait for `.retry_after` (seconds) / the reset, then retry — or request an increase in API Manager. | | `PromptInjectionBlocked` | header `x-injection-protection: blocked`, **or** a top-level `matched_patterns` list (regex prompt guard) | **No** — a `PolicyViolation`, never retried. | Review and sanitise the untrusted input, or adjust the policy's sensitivity / deny-list in API Manager. | -| `ContentSafetyBlocked` | `403` + `x-llm-proxy--…-action: reject` (Azure Content Safety / Bedrock Guardrails) — *body shape pending [#253]* | **No** — a `PolicyViolation`, never retried. | Revise the flagged content (`.categories`), or adjust the policy's categories / severity thresholds in API Manager. | +| `ContentSafetyBlocked` | `403` + `x-llm-proxy--…-action: reject` (Azure Content Safety / Bedrock Guardrails) — *live-verified [#253]/[#568]* | **No** — a `PolicyViolation`, never retried. | Revise the flagged content (`.categories`), or adjust the policy's categories / severity thresholds in API Manager. | | `PolicyViolation` (generic) | a `4xx` matching **no** documented rejection shape | **No** — terminal; the shape is simply not yet typed. | Inspect `.response`; file an issue with the status/headers/body so the shape can be typed ([#184]/[#253]). | | `UpstreamRequestError` | non-`429` `4xx`, nested `error` with `code`/`type`/`param` (object **or** Gemini list envelope, [#548]) | **No** — a client-side request mistake passed through the gateway, terminal. | Fix the flagged model or parameter (`.code` / `.param`); if `model_not_found`, request the model in API Manager. | | `UpstreamModelError` | `5xx` | **Yes** — the transport already retries `502` / `503` / `504`; a persistent `5xx` is safe for you to retry too. | Transient provider failure — retry, then escalate if it persists. | diff --git a/website/content/reference/unsupported-boundary.mdx b/website/content/reference/unsupported-boundary.mdx index c62ad338..f79cc988 100644 --- a/website/content/reference/unsupported-boundary.mdx +++ b/website/content/reference/unsupported-boundary.mdx @@ -31,7 +31,7 @@ needs an unverified endpoint is blocked before network I/O. | Anypoint connected-app token endpoint | **Documented and public** | Retrieves an OAuth bearer token with client credentials. | | Model Proxy OpenAI-format `/responses` endpoint | **Documented and public** | Sends buffered or streaming model requests with the documented `client_id` / `client_secret` headers and reads OpenAI-format usage. The raw client can also call documented OpenAI-native routes such as `/chat/completions`, but this repository has live-probed only `/responses`. | | Live-captured Model Proxy policy refusals | **Documented and public** | Classifies Client ID Enforcement, token-rate-limit, and PII responses observed against a deployed proxy. | -| Documentation-derived Model Proxy policy refusals | **Documented and public** | Classifies Regex Prompt Guard, Injection Protection, Azure Content Safety, and Amazon Bedrock Guardrails contracts from their official policy pages. These shapes are pending direct live capture. | +| Documentation-derived Model Proxy policy refusals | **Documented and public** | Classifies the **Injection Protection** contract (`x-injection-protection: blocked`) from its official policy page; its body is still pending direct live capture (no proxy deployed). The Regex Prompt Guard, Azure Content Safety, and Amazon Bedrock Guardrails shapes are now live-captured — see the verification page. | | Upstream provider error pass-through | **Documented, no SLA for third-party use** | Classifies the live-captured nested non-`429` `4xx` provider envelope as `UpstreamRequestError`; generic `5xx` responses become `UpstreamModelError` by status only. The envelope schema belongs to the upstream provider, and MuleSoft's public Model Proxy page states no pass-through compatibility contract. | | `x-llm-proxy-ratelimit` success-budget sentence | **Documented, no SLA for third-party use** | Updates `donkey.budget`; an absent or changed value is ignored. | | Gateway identity and routing extension headers | **Documented, no SLA for third-party use** | Populates `donkey.last_call`; missing or unrecognised values become `None`. | diff --git a/website/public/concepts/verification.md b/website/public/concepts/verification.md index df37d96c..adf51453 100644 --- a/website/public/concepts/verification.md +++ b/website/public/concepts/verification.md @@ -42,7 +42,7 @@ how a blocked value flips to verified — is documented for contributors in | Budget query endpoint ([Phase 1](https://donkey-development-kit.github.io/donkey-development-kit/budget.md)) | **Missing upstream** | Budget is reported in-band on response headers only, so `remaining` is last-known-good; `observed_at` exposes the staleness. | | Whether Anypoint Monitoring ingests OTLP GenAI spans ([Phase 1](https://donkey-development-kit.github.io/donkey-development-kit/telemetry.md)) | Unverified | The SDK promises "exports OTLP" and lets the sink be your choice. | | A provisioning control plane | **Cut** | Not built. It would compete with API Manager and Terraform — see [Roadmap](https://donkey-development-kit.github.io/donkey-development-kit/roadmap.md). | -| Injection-protection / Bedrock-guardrails / fall-through bodies | Not captured | The **regex prompt-guard** (`matched_patterns` list) and **Azure content-safety** (`…-action: reject` header) bodies are now **live-captured** (2026-09-22, #253). Still uncaptured: the distinct **Injection Protection** policy's body (typed by the `x-injection-protection: blocked` header; no such proxy is deployed), the **Bedrock Guardrails** sibling of content-safety, and the undiscriminated content-moderation fall-through to a generic `PolicyViolation` — all pinned from the policy pages, and no verification row flips until a live round-trip confirms them. No invented discriminator — tracked in #253. | +| Injection-protection / fall-through bodies | Not captured | The **regex prompt-guard** (`matched_patterns` list) and **content-safety** (`…-action: reject` header) bodies are now **live-captured** — Azure Content Safety 2026-09-22 (#253) and its **Bedrock Guardrails** sibling 2026-09-24 (#568). Still uncaptured: the distinct **Injection Protection** policy's body (typed by the `x-injection-protection: blocked` header; no such proxy is deployed) and the undiscriminated content-moderation fall-through to a generic `PolicyViolation` — pinned from the policy pages, and no verification row flips until a live round-trip confirms them. No invented discriminator — tracked in #253. | | First-party **TypeScript SDK** | Planned | TypeScript examples reach the proxy over its OpenAI-compatible API via the `openai` / `@anthropic-ai/sdk` clients; no first-party TS package is implied until it ships. | ## What this means for you diff --git a/website/public/errors.md b/website/public/errors.md index 9ce29995..7cf0ac3e 100644 --- a/website/public/errors.md +++ b/website/public/errors.md @@ -8,11 +8,12 @@ branch on the governance outcome instead of parsing bodies. `classify()` types eight rejection shapes. **Six are live-verified** against the sandbox proxies (auth, PII, token rate limit, upstream, **regex prompt guard**, -and **Azure content safety** — the last two confirmed 2026-09-22, [#253]). +and **content safety** — the last two confirmed 2026-09-22, [#253], with content +safety's **Bedrock Guardrails** vendor added 2026-09-24, [#568]). Still pending live capture: **injection protection** (typed by its header discriminator, body uncaptured — a distinct policy from regex prompt guard, with -no proxy deployed), Amazon **Bedrock guardrails**, and the **undiscriminated -content-moderation** fall-through — all tracked in [#253]. The critical lesson: +no proxy deployed) and the **undiscriminated content-moderation** fall-through — +both tracked in [#253]. The critical lesson: **neither the status code nor the shape of the `error` value alone is a sufficient discriminator** — a `403` can be PII, a regex-guard block, a content-safety block (all policy blocks) *or* auth, and the same nested-object @@ -25,7 +26,7 @@ authoritative discriminator is the error **`type`** plus specific headers. | PII detected | `403` | nested `{"error":{type:"pii_detected"}}`, **no** `www-authenticate` | `PIIDetected` (parses `entities`) | live | | Injection protection | `400` | header `x-injection-protection: blocked` (**not** the status) | `PromptInjectionBlocked` | pending (#253) | | Regex prompt guard | `403` | top-level `matched_patterns` list (flat `error`) | `PromptInjectionBlocked` (`policy="regex-prompt-guard"`) | live (#253) | -| Content safety / guardrails | `403` | header `x-llm-proxy--…-action: reject` (Azure Content Safety / Bedrock Guardrails) | `ContentSafetyBlocked` (parses `categories`) | live — Azure; Bedrock pending (#253) | +| Content safety / guardrails | `403` | header `x-llm-proxy--…-action: reject` (Azure Content Safety / Bedrock Guardrails) | `ContentSafetyBlocked` (parses `categories`) | live — Azure (#253) + Bedrock (#568) | | Token rate limit | `429` | **empty body**; `x-token-limit`/`-remaining`/`-reset` headers (ms) | `TokenBudgetExceeded` (`retry_after` derived) | live | | Content moderation (undiscriminated) | `4xx` | falls through — no nested `error`, no injection/guard/safety discriminator | generic `PolicyViolation` | pending (#253) | | Upstream provider 4xx | `4xx` | nested `error` object **with** `code`/`type`/`param` — in an OpenAI-style object envelope `{"error":{…}}` **or** a Gemini-style list envelope `[{"error":{…}}]` (`status`→`error_type`, #548) | `UpstreamRequestError` | live | @@ -41,6 +42,7 @@ Client-ID enforcement (`401`) is a **consumer-auth** case, not one of the eight policy-rejection rows. [#253]: https://github.com/Donkey-Development-Kit/donkey-development-kit/issues/253 +[#568]: https://github.com/Donkey-Development-Kit/donkey-development-kit/issues/568 ## The exception tree @@ -54,7 +56,7 @@ DonkeyError # base of the whole tree │ ├─ PIIDetected # 403, type=pii_detected; .entities │ ├─ TokenBudgetExceeded # 429; .retry_after (seconds) │ ├─ PromptInjectionBlocked # x-injection-protection: blocked, or regex matched_patterns (pending #253) -│ └─ ContentSafetyBlocked # Azure Content Safety / Bedrock Guardrails vendor reject header; .categories (pending #253) +│ └─ ContentSafetyBlocked # Azure Content Safety / Bedrock Guardrails vendor reject header; .categories ├─ GatewayUnavailable # transport failure — gateway unreachable, NO response; .base_url/.cause (ungoverned) ├─ UpstreamRequestError # upstream 4xx; .code/.error_type/.param └─ UpstreamModelError # upstream 5xx — provider error, retryable @@ -80,7 +82,7 @@ never burn an exhausted budget or replay a blocked prompt. | `PIIDetected` | `403`, nested `type: "pii_detected"`, **no** `www-authenticate` | **No** — a `PolicyViolation`, never retried. | Remove or redact the flagged values (`.entities`), or relax the policy's entity list in API Manager. | | `TokenBudgetExceeded` | `429`, empty body, `x-token-*` headers | **Not immediately** — never auto-retried; only worth retrying *after* the window resets. | Wait for `.retry_after` (seconds) / the reset, then retry — or request an increase in API Manager. | | `PromptInjectionBlocked` | header `x-injection-protection: blocked`, **or** a top-level `matched_patterns` list (regex prompt guard) | **No** — a `PolicyViolation`, never retried. | Review and sanitise the untrusted input, or adjust the policy's sensitivity / deny-list in API Manager. | -| `ContentSafetyBlocked` | `403` + `x-llm-proxy--…-action: reject` (Azure Content Safety / Bedrock Guardrails) — *body shape pending [#253]* | **No** — a `PolicyViolation`, never retried. | Revise the flagged content (`.categories`), or adjust the policy's categories / severity thresholds in API Manager. | +| `ContentSafetyBlocked` | `403` + `x-llm-proxy--…-action: reject` (Azure Content Safety / Bedrock Guardrails) — *live-verified [#253]/[#568]* | **No** — a `PolicyViolation`, never retried. | Revise the flagged content (`.categories`), or adjust the policy's categories / severity thresholds in API Manager. | | `PolicyViolation` (generic) | a `4xx` matching **no** documented rejection shape | **No** — terminal; the shape is simply not yet typed. | Inspect `.response`; file an issue with the status/headers/body so the shape can be typed ([#184]/[#253]). | | `UpstreamRequestError` | non-`429` `4xx`, nested `error` with `code`/`type`/`param` (object **or** Gemini list envelope, [#548]) | **No** — a client-side request mistake passed through the gateway, terminal. | Fix the flagged model or parameter (`.code` / `.param`); if `model_not_found`, request the model in API Manager. | | `UpstreamModelError` | `5xx` | **Yes** — the transport already retries `502` / `503` / `504`; a persistent `5xx` is safe for you to retry too. | Transient provider failure — retry, then escalate if it persists. | diff --git a/website/public/llms-full.txt b/website/public/llms-full.txt index 51b978a3..5a51d157 100644 --- a/website/public/llms-full.txt +++ b/website/public/llms-full.txt @@ -1989,11 +1989,12 @@ branch on the governance outcome instead of parsing bodies. `classify()` types eight rejection shapes. **Six are live-verified** against the sandbox proxies (auth, PII, token rate limit, upstream, **regex prompt guard**, -and **Azure content safety** — the last two confirmed 2026-09-22, [#253]). +and **content safety** — the last two confirmed 2026-09-22, [#253], with content +safety's **Bedrock Guardrails** vendor added 2026-09-24, [#568]). Still pending live capture: **injection protection** (typed by its header discriminator, body uncaptured — a distinct policy from regex prompt guard, with -no proxy deployed), Amazon **Bedrock guardrails**, and the **undiscriminated -content-moderation** fall-through — all tracked in [#253]. The critical lesson: +no proxy deployed) and the **undiscriminated content-moderation** fall-through — +both tracked in [#253]. The critical lesson: **neither the status code nor the shape of the `error` value alone is a sufficient discriminator** — a `403` can be PII, a regex-guard block, a content-safety block (all policy blocks) *or* auth, and the same nested-object @@ -2006,7 +2007,7 @@ authoritative discriminator is the error **`type`** plus specific headers. | PII detected | `403` | nested `{"error":{type:"pii_detected"}}`, **no** `www-authenticate` | `PIIDetected` (parses `entities`) | live | | Injection protection | `400` | header `x-injection-protection: blocked` (**not** the status) | `PromptInjectionBlocked` | pending (#253) | | Regex prompt guard | `403` | top-level `matched_patterns` list (flat `error`) | `PromptInjectionBlocked` (`policy="regex-prompt-guard"`) | live (#253) | -| Content safety / guardrails | `403` | header `x-llm-proxy--…-action: reject` (Azure Content Safety / Bedrock Guardrails) | `ContentSafetyBlocked` (parses `categories`) | live — Azure; Bedrock pending (#253) | +| Content safety / guardrails | `403` | header `x-llm-proxy--…-action: reject` (Azure Content Safety / Bedrock Guardrails) | `ContentSafetyBlocked` (parses `categories`) | live — Azure (#253) + Bedrock (#568) | | Token rate limit | `429` | **empty body**; `x-token-limit`/`-remaining`/`-reset` headers (ms) | `TokenBudgetExceeded` (`retry_after` derived) | live | | Content moderation (undiscriminated) | `4xx` | falls through — no nested `error`, no injection/guard/safety discriminator | generic `PolicyViolation` | pending (#253) | | Upstream provider 4xx | `4xx` | nested `error` object **with** `code`/`type`/`param` — in an OpenAI-style object envelope `{"error":{…}}` **or** a Gemini-style list envelope `[{"error":{…}}]` (`status`→`error_type`, #548) | `UpstreamRequestError` | live | @@ -2022,6 +2023,7 @@ Client-ID enforcement (`401`) is a **consumer-auth** case, not one of the eight policy-rejection rows. [#253]: https://github.com/Donkey-Development-Kit/donkey-development-kit/issues/253 +[#568]: https://github.com/Donkey-Development-Kit/donkey-development-kit/issues/568 ## The exception tree @@ -2035,7 +2037,7 @@ DonkeyError # base of the whole tree │ ├─ PIIDetected # 403, type=pii_detected; .entities │ ├─ TokenBudgetExceeded # 429; .retry_after (seconds) │ ├─ PromptInjectionBlocked # x-injection-protection: blocked, or regex matched_patterns (pending #253) -│ └─ ContentSafetyBlocked # Azure Content Safety / Bedrock Guardrails vendor reject header; .categories (pending #253) +│ └─ ContentSafetyBlocked # Azure Content Safety / Bedrock Guardrails vendor reject header; .categories ├─ GatewayUnavailable # transport failure — gateway unreachable, NO response; .base_url/.cause (ungoverned) ├─ UpstreamRequestError # upstream 4xx; .code/.error_type/.param └─ UpstreamModelError # upstream 5xx — provider error, retryable @@ -2061,7 +2063,7 @@ never burn an exhausted budget or replay a blocked prompt. | `PIIDetected` | `403`, nested `type: "pii_detected"`, **no** `www-authenticate` | **No** — a `PolicyViolation`, never retried. | Remove or redact the flagged values (`.entities`), or relax the policy's entity list in API Manager. | | `TokenBudgetExceeded` | `429`, empty body, `x-token-*` headers | **Not immediately** — never auto-retried; only worth retrying *after* the window resets. | Wait for `.retry_after` (seconds) / the reset, then retry — or request an increase in API Manager. | | `PromptInjectionBlocked` | header `x-injection-protection: blocked`, **or** a top-level `matched_patterns` list (regex prompt guard) | **No** — a `PolicyViolation`, never retried. | Review and sanitise the untrusted input, or adjust the policy's sensitivity / deny-list in API Manager. | -| `ContentSafetyBlocked` | `403` + `x-llm-proxy--…-action: reject` (Azure Content Safety / Bedrock Guardrails) — *body shape pending [#253]* | **No** — a `PolicyViolation`, never retried. | Revise the flagged content (`.categories`), or adjust the policy's categories / severity thresholds in API Manager. | +| `ContentSafetyBlocked` | `403` + `x-llm-proxy--…-action: reject` (Azure Content Safety / Bedrock Guardrails) — *live-verified [#253]/[#568]* | **No** — a `PolicyViolation`, never retried. | Revise the flagged content (`.categories`), or adjust the policy's categories / severity thresholds in API Manager. | | `PolicyViolation` (generic) | a `4xx` matching **no** documented rejection shape | **No** — terminal; the shape is simply not yet typed. | Inspect `.response`; file an issue with the status/headers/body so the shape can be typed ([#184]/[#253]). | | `UpstreamRequestError` | non-`429` `4xx`, nested `error` with `code`/`type`/`param` (object **or** Gemini list envelope, [#548]) | **No** — a client-side request mistake passed through the gateway, terminal. | Fix the flagged model or parameter (`.code` / `.param`); if `model_not_found`, request the model in API Manager. | | `UpstreamModelError` | `5xx` | **Yes** — the transport already retries `502` / `503` / `504`; a persistent `5xx` is safe for you to retry too. | Transient provider failure — retry, then escalate if it persists. | @@ -4490,7 +4492,7 @@ how a blocked value flips to verified — is documented for contributors in | Budget query endpoint ([Phase 1](https://donkey-development-kit.github.io/donkey-development-kit/budget.md)) | **Missing upstream** | Budget is reported in-band on response headers only, so `remaining` is last-known-good; `observed_at` exposes the staleness. | | Whether Anypoint Monitoring ingests OTLP GenAI spans ([Phase 1](https://donkey-development-kit.github.io/donkey-development-kit/telemetry.md)) | Unverified | The SDK promises "exports OTLP" and lets the sink be your choice. | | A provisioning control plane | **Cut** | Not built. It would compete with API Manager and Terraform — see [Roadmap](https://donkey-development-kit.github.io/donkey-development-kit/roadmap.md). | -| Injection-protection / Bedrock-guardrails / fall-through bodies | Not captured | The **regex prompt-guard** (`matched_patterns` list) and **Azure content-safety** (`…-action: reject` header) bodies are now **live-captured** (2026-09-22, #253). Still uncaptured: the distinct **Injection Protection** policy's body (typed by the `x-injection-protection: blocked` header; no such proxy is deployed), the **Bedrock Guardrails** sibling of content-safety, and the undiscriminated content-moderation fall-through to a generic `PolicyViolation` — all pinned from the policy pages, and no verification row flips until a live round-trip confirms them. No invented discriminator — tracked in #253. | +| Injection-protection / fall-through bodies | Not captured | The **regex prompt-guard** (`matched_patterns` list) and **content-safety** (`…-action: reject` header) bodies are now **live-captured** — Azure Content Safety 2026-09-22 (#253) and its **Bedrock Guardrails** sibling 2026-09-24 (#568). Still uncaptured: the distinct **Injection Protection** policy's body (typed by the `x-injection-protection: blocked` header; no such proxy is deployed) and the undiscriminated content-moderation fall-through to a generic `PolicyViolation` — pinned from the policy pages, and no verification row flips until a live round-trip confirms them. No invented discriminator — tracked in #253. | | First-party **TypeScript SDK** | Planned | TypeScript examples reach the proxy over its OpenAI-compatible API via the `openai` / `@anthropic-ai/sdk` clients; no first-party TS package is implied until it ships. | ## What this means for you @@ -5209,7 +5211,7 @@ needs an unverified endpoint is blocked before network I/O. | Anypoint connected-app token endpoint | **Documented and public** | Retrieves an OAuth bearer token with client credentials. | | Model Proxy OpenAI-format `/responses` endpoint | **Documented and public** | Sends buffered or streaming model requests with the documented `client_id` / `client_secret` headers and reads OpenAI-format usage. The raw client can also call documented OpenAI-native routes such as `/chat/completions`, but this repository has live-probed only `/responses`. | | Live-captured Model Proxy policy refusals | **Documented and public** | Classifies Client ID Enforcement, token-rate-limit, and PII responses observed against a deployed proxy. | -| Documentation-derived Model Proxy policy refusals | **Documented and public** | Classifies Regex Prompt Guard, Injection Protection, Azure Content Safety, and Amazon Bedrock Guardrails contracts from their official policy pages. These shapes are pending direct live capture. | +| Documentation-derived Model Proxy policy refusals | **Documented and public** | Classifies the **Injection Protection** contract (`x-injection-protection: blocked`) from its official policy page; its body is still pending direct live capture (no proxy deployed). The Regex Prompt Guard, Azure Content Safety, and Amazon Bedrock Guardrails shapes are now live-captured — see the verification page. | | Upstream provider error pass-through | **Documented, no SLA for third-party use** | Classifies the live-captured nested non-`429` `4xx` provider envelope as `UpstreamRequestError`; generic `5xx` responses become `UpstreamModelError` by status only. The envelope schema belongs to the upstream provider, and MuleSoft's public Model Proxy page states no pass-through compatibility contract. | | `x-llm-proxy-ratelimit` success-budget sentence | **Documented, no SLA for third-party use** | Updates `donkey.budget`; an absent or changed value is ignored. | | Gateway identity and routing extension headers | **Documented, no SLA for third-party use** | Populates `donkey.last_call`; missing or unrecognised values become `None`. | diff --git a/website/public/reference/unsupported-boundary.md b/website/public/reference/unsupported-boundary.md index d08e0d64..76f594eb 100644 --- a/website/public/reference/unsupported-boundary.md +++ b/website/public/reference/unsupported-boundary.md @@ -29,7 +29,7 @@ needs an unverified endpoint is blocked before network I/O. | Anypoint connected-app token endpoint | **Documented and public** | Retrieves an OAuth bearer token with client credentials. | | Model Proxy OpenAI-format `/responses` endpoint | **Documented and public** | Sends buffered or streaming model requests with the documented `client_id` / `client_secret` headers and reads OpenAI-format usage. The raw client can also call documented OpenAI-native routes such as `/chat/completions`, but this repository has live-probed only `/responses`. | | Live-captured Model Proxy policy refusals | **Documented and public** | Classifies Client ID Enforcement, token-rate-limit, and PII responses observed against a deployed proxy. | -| Documentation-derived Model Proxy policy refusals | **Documented and public** | Classifies Regex Prompt Guard, Injection Protection, Azure Content Safety, and Amazon Bedrock Guardrails contracts from their official policy pages. These shapes are pending direct live capture. | +| Documentation-derived Model Proxy policy refusals | **Documented and public** | Classifies the **Injection Protection** contract (`x-injection-protection: blocked`) from its official policy page; its body is still pending direct live capture (no proxy deployed). The Regex Prompt Guard, Azure Content Safety, and Amazon Bedrock Guardrails shapes are now live-captured — see the verification page. | | Upstream provider error pass-through | **Documented, no SLA for third-party use** | Classifies the live-captured nested non-`429` `4xx` provider envelope as `UpstreamRequestError`; generic `5xx` responses become `UpstreamModelError` by status only. The envelope schema belongs to the upstream provider, and MuleSoft's public Model Proxy page states no pass-through compatibility contract. | | `x-llm-proxy-ratelimit` success-budget sentence | **Documented, no SLA for third-party use** | Updates `donkey.budget`; an absent or changed value is ignored. | | Gateway identity and routing extension headers | **Documented, no SLA for third-party use** | Populates `donkey.last_call`; missing or unrecognised values become `None`. |