Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ on:
paths:
- 'website/**'
- '.github/workflows/docs.yml'
# A rebuild every 6 hours refreshes the Roadmap snapshot (data/roadmap.json) that
# visitors without JavaScript, and search indexing, see.
schedule:
- cron: '17 */6 * * *'
workflow_dispatch:

# Least privilege + the OIDC token deploy-pages needs to publish.
Expand All @@ -34,7 +38,10 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
# Scheduled runs start from the default branch (develop); always publish main.
- uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'schedule' && 'main' || github.ref }}
- uses: actions/setup-node@v7
with:
node-version: 20
Expand Down
1 change: 1 addition & 0 deletions docs/verified-apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ request against the deployed gateway.
| 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` |
| Auth: model-wallet ingress (alt to `client_id`/`client_secret`, #372) | `core/transport.py`, `core/config.py` — contract verified; SDK wiring landed in #509 (`llm_proxy_auth="jwt"` + `llm_proxy_wallet_client_id` + `Donkey(llm_auth=…)`) | VERIFIED (LIVE) | Wallet-backed model proxies auth via an **IdP-issued JWT + a client ID**, with **no `client_secret`**: the client ID travels as the **`X-Client-Id`** request header (exact casing confirmed; value = the wallet's system-generated `clientId`, `ddk-model-wallet` here — read it from the omni API response, don't assume) which selects the wallet (echoed back as response header **`x-model-wallet-selected`**), *and* as a `client_id` **JWT claim** read by the LLM Proxy Core Policy at `#[authentication.properties.claims.client_id]` after the **JWT Validation** policy validates + publishes claims. **The JWT rides as `Authorization: Bearer <JWT>`** — this was an *assumption* from the doc read (Bearer is nowhere quoted on the source page); now **confirmed** (policy `jwtOrigin: httpBearerAuthenticationHeader`; missing header → `400 {"error":"JWT Token is required."}`, `www-authenticate: Bearer`; invalid/expired token → `401 {"error":"Invalid token."}`). No `client_id`/`client_secret` request headers are sent and the call still succeeds → the default **DataWeave Headers Transformation** + **Client ID Enforcement** policies are confirmed **disabled**. Requires an org **IdP** (JWKS URL / signing key — "orgs without an IdP can't use model wallets"). A **parallel** ingress model, NOT a replacement of the LIVE `client_id`/`client_secret` pair above. | 2026-09-21 | live probe — `tests/fixtures/anypoint/model_wallet/` (`ddk-model-wallet`, instance `21186246`, Sandbox); shape corroborates `docs.mulesoft.com/general/exp-model-wallets-manage` |
| Model routing | `llm/client.py`; consumed by `core/lastcall.py` + `core/transport.py` (#309) | VERIFIED (LIVE) | `model-based-routing` 1.0.3 reads `model` from body → provider. Response headers `x-llm-proxy-routing-type: ModelBased`, `x-llm-proxy-routing-fallback: false`, `x-llm-proxy-llm-provider: openai`, `x-llm-proxy-llm-model: gpt-5.1`, `x-llm-proxy-model-based-routing-success: Request successfully matched. …`. As of #309 the SDK surfaces these at `donkey.last_call` (`served_provider`/`served_model`/`routing_type`/`fallback`, `substituted` when served≠requested), emits them as `gen_ai.response.model` / `donkey.routing.type` / `donkey.routing.fallback` span attrs, and **does not retry** a `503` a fallback header already marked failed-over (#309, must not fight #183) | 2026-08-28 | live probe (`responses.success.headers.txt`) |
| Semantic routing (#589) | `llm/client.py`; the four shared routing headers already consumed by `core/lastcall.py` + `core/transport.py` (#309); matched-topic/score surfacing is #590 | VERIFIED (LIVE) | A **semantic-routing** proxy emits the **same four routing headers as model-based** — `x-llm-proxy-routing-type` (value **`Semantic`** vs `ModelBased`), `-routing-fallback`, `-llm-provider`, `-llm-model` — so #309's `LastCall` parsing already works unchanged for the semantic case. Additionally, a **semantic-only** header carries the match detail: **`x-llm-proxy-semantic-routing-success`**, exact format `Request successfully matched '{topic}' topic (Provider: {provider}, Model: {model}). Score: {score}.` (observed: Finance → `openai`/`gpt-5-mini` score `0.62`; Code → `gemini`/`gemini-2.5-flash` score `0.62`). Note the format is `(Provider: …, Model: …)` — **not** a `provider/model` slug — and the score is a bare `0.xx`. Also present alongside: `x-llm-proxy-request-success` (`Request completed successfully. Provider: …, Model: ….`) and, on OpenAI-served routes, cost hints `x-llm-proxy-input-tokens-cost-per-1m` / `-output-tokens-cost-per-1m` (out of scope for #590 — see its Out of scope). **Fallback (`routing-fallback: true`) was NOT reproducible on this SSC** (§0.3 — not fabricated): with a 10-vector `text-embedding-3-small` store, cosine similarity floors ~`0.51`–`0.53` (above the `0.5` threshold, comparison strictly-greater-than) even for gibberish/off-topic prompts, so every request matched a primary topic with `routing-fallback: false`. The fallback-branch header shape therefore remains uncaptured. | 2026-09-24 | live probe against `ddk-semantic-advanced-routing` (instance `21194930`, DDK/Sandbox, private-space) — captured in `python/tests/fixtures/anypoint/semantic_routing/` |
| Token accounting (cost attribution) | `llm/*`, telemetry | VERIFIED (LIVE) | response `usage: {input_tokens, output_tokens, total_tokens, input_tokens_details, output_tokens_details}` — the detail sub-objects carry `cached_tokens` / `cache_write_tokens` (under `input_tokens_details`) and `reasoning_tokens` (under `output_tokens_details`), surfaced per-call at `donkey.last_call` and on the span (#307, see §3); also upstream `x-ratelimit-*` headers passed through (Go-style **duration-string** resets, e.g. `0s`/`12ms` — distinct from the gateway's own `x-llm-proxy-ratelimit`/`x-token-*` window, see §4) | 2026-08-28 | `responses.success.body.json` |
| Request fields passed through | `llm/client.py` | VERIFIED (LIVE) | OpenAI body passed through verbatim; response returned verbatim (model `gpt-5.1`→`gpt-5.1-2025-11-13`, full Responses object) | 2026-08-28 | live probe |
| Streaming support | `llm/client.py` | VERIFIED (LIVE) | `"stream": true` → `200`, `content-type: text/event-stream`, chunked SSE (`event: response.created` / `response.in_progress` / …); same `x-llm-proxy-*` headers | 2026-08-28 | live probe (`responses.stream.*`) |
Expand Down
2 changes: 1 addition & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "donkey-kit"
version = "0.1.0.dev9"
version = "0.1.0.dev10"
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"
Expand Down
2 changes: 1 addition & 1 deletion python/src/donkey_kit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
PublicationAssetType,
)

__version__ = "0.1.0.dev9"
__version__ = "0.1.0.dev10"

__all__ = [
"STRICT",
Expand Down
62 changes: 62 additions & 0 deletions python/tests/fixtures/anypoint/semantic_routing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Semantic routing — LIVE capture (docs/verified-apis.md §3, #589)

Captured **2026-09-24** from a real deployed Agent Network LLM proxy provisioned
with **semantic-based routing** (advanced Semantic Service Configuration, an
external Azure AI Search vector store) in the DDK sandbox, env **Sandbox**
(svc id `14d3b31e-4e3b-4d90-b77a-63c9d6b7ea6a`, from the
`x-envoy-decorator-operation` header). Proxy asset `ddk-semantic-advanced-routing`,
API Manager instance **`21194930`**, deployed to the `private-space-omni-gateway`
Flex Gateway. Two topics — **Finance** → `openai`/`gpt-5-mini`, **Code** →
`gemini`/`gemini-2.5-flash` — with `openai` `text-embedding-3-small` embeddings
and a `0.5` fallback threshold.

This is the **first live confirmation of the semantic-routing header contract**.
It resolves issue **#589** and unblocks **#590** (surfacing the matched topic +
similarity score on `donkey.last_call`). It is the semantic sibling of the
model-based routing captured in `../llm_proxy/responses.success.policy-applied.headers.txt`.

## What is verified (all witnessed live)

- **The four shared routing headers are emitted identically to model-based**,
with `x-llm-proxy-routing-type: Semantic` (vs `ModelBased`) — so the #309
`LastCall` / `transport` parsing of `routing-type` / `routing-fallback` /
`llm-provider` / `llm-model` already works unchanged for the semantic case.
- **A semantic-only header carries the match detail:**
`x-llm-proxy-semantic-routing-success`, with the exact format

```
Request successfully matched '{topic}' topic (Provider: {provider}, Model: {model}). Score: {score}.
```

Note the `(Provider: …, Model: …)` shape — **not** a `provider/model` slug —
and the bare `0.xx` score. This is the string #590 must parse for
`matched_topic` + `routing_score`. See `responses.{finance,code,offtopic}.headers.txt`.
- **`x-llm-proxy-request-success`** (`Request completed successfully. Provider:
…, Model: ….`) and, on the OpenAI-served route, **cost hints**
`x-llm-proxy-input-tokens-cost-per-1m` / `-output-tokens-cost-per-1m` are also
present. Those cost headers are recorded but **out of scope for #590**.
- **Client ID Enforcement is on** — same `client_id`/`client_secret` pair as the
other CIE proxies (no `client_id` → 401 `{"error":"Client ID is not present"}`,
`www-authenticate: Client-ID-Enforcement`; verified during the probe, not
re-captured here since it is identical to `../llm_proxy/reject.client-id-missing.*`).

## What could NOT be captured — the fallback branch (§0.3, not fabricated)

`x-llm-proxy-routing-fallback: true` was **not reproducible** on this SSC. With a
10-vector `text-embedding-3-small` store the cosine similarity floors around
`0.51`–`0.53` (above the `0.5` threshold, and the comparison is
strictly-greater-than) even for gibberish and deliberately off-topic prompts —
so every request matched a *primary* topic with `routing-fallback: false`. The
`responses.offtopic.*` capture is such a low-but-above-threshold match
(`Finance`, score `0.51`), **not** an actual fallback. The fallback-branch header
shape therefore remains uncaptured; it would need a proxy whose threshold is
raised (or whose topic coverage is thinner) to observe, which is a
provisioning-side change out of scope for #589.

## How this was captured

The data-plane requests were issued directly against instance `21194930` with
the shared `ddk-test-client` consumer's already-approved contract. Consumer
`client_id`/`client_secret` are **redacted** from `request.success.http` and were
never persisted here; the Cloudflare `set-cookie` line was stripped from the
captured header dumps as noise.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Live semantic-routing request (consumer creds redacted).
# Same OpenAI /chat/completions wire shape as ../llm_proxy/ — the routing
# decision is made by prompt SEMANTICS, not the body `model` field (which is a
# placeholder here). The topic each prompt matched is echoed in the
# `x-llm-proxy-semantic-routing-success` response header (see responses.*.headers.txt).

POST /ddk-semantic-advanced-routing/chat/completions HTTP/1.1
Host: private-space-omni-gateway-qrpud8.wxdmnv.usa-e2.cloudhub.io
Content-Type: application/json
client_id: <redacted>
client_secret: <redacted>

{"model": "any-placeholder", "messages": [{"role": "user", "content": "How do I calculate compound interest?"}]}

# Prompt → matched topic (captured 2026-09-24):
# "How do I calculate compound interest?" → Finance (openai/gpt-5-mini, score 0.62) → responses.finance.*
# "Write a Python function to sort a list." → Code (gemini/gemini-2.5-flash, score 0.62) → responses.code.*
# "What is the airspeed velocity of an unladen swallow?" → Finance (openai/gpt-5-mini, score 0.51) → responses.offtopic.*
Loading
Loading