Voice
Foundry Integration
Azure AI Foundry tracing plugin for OpenClaw — emits OTel GenAI semconv spans (chat / execute_tool) parented to the OpenClaw model.call trace so Foundry's Tracing tab shows conversation id, I/O content, token usage, and tool I/O.
Install
npm install --no-save
Configuration Example
{
"plugins": {
"allow": ["openclaw-foundry-integration"],
"entries": {
"openclaw-foundry-integration": {
"enabled": true,
"hooks": {
// REQUIRED — without this OpenClaw silently denies the
// llm_input / llm_output / before_tool_call / after_tool_call
// hooks.
"allowConversationAccess": true
},
"config": {
"conversationId": {
"prefix": "oc",
"hashLength": 16,
"anonymousId": "oc-anon"
},
"captureContent": {
"inputMessages": true,
"outputMessages": true,
"systemPrompt": "hash", // "off" | "hash" | "full"
"toolInputs": true,
"toolOutputs": true,
"maxAttributeChars": 8000
}
}
}
},
"load": {
"paths": ["~/.openclaw/extensions/openclaw-foundry-integration"]
}
}
}
README
# openclaw-foundry-integration
[](LICENSE)
Single-purpose OpenClaw plugin that turns the gateway's Azure AI Foundry / OpenTelemetry tracing into something useful: every `openclaw.model.call` span carries the **conversation id**, **input messages**, **output messages**, **token usage**, and (optionally) **tool input / output** that Foundry's Tracing tab expects.
---
## Why this plugin exists
OpenClaw 2026.5.x ships two relevant plugins out of the box:
| Plugin | Owns | Limitation |
|--------|------|------------|
| `diagnostics-otel` (bundled) | `openclaw.run` + `openclaw.model.call` spans | Writes provider/model/duration metadata only; **no content, no token counts, no conversation id** |
| `openclaw-conversation-id` (community) | Conversation id | Registers via `api.registerHook(...)` which writes to the **wrong event bus** in the runtime — the hook never fires in production |
Net effect on Foundry: every model trace is missing the three attributes the Tracing tab queries (`gen_ai.conversation.id`, `gen_ai.input.messages`, `gen_ai.output.messages`), so each invocation looks like a disembodied `model.call` with no conversational context.
`openclaw-foundry-integration` fixes both by:
1. Registering on the **typed event bus** via `api.on(...)` (verified by direct probe — see `/memories/repo/openclaw-typed-hooks-api.md`).
2. Looking up the active span via a **`SpanProcessor`** keyed on `(traceId, spanId)`. `trace.getActiveSpan()` returns `null` in every OpenClaw hook because `diagnostics-otel` opens spans imperatively without `setActive`. Each hook context however carries `ctx.trace = {traceId, spanId, ...}` which directly identifies the span we want to write to.
3. Emitting **both** OTel GenAI semconv names (`gen_ai.input.messages`) **and** OpenClaw flat names (`openclaw.content.input_messages`) so existing KQL queries keep working alongside Foundry's standard ones.
---
## What it writes
On every `openclaw.model.call` span:
| Attribute | Source hook | Notes |
|-----------|-------------|-------|
| `gen_ai.conversation.id` | `model_call_started` | sha256-derived from `sessionKey`; never raw chat id |
| `openclaw.conversation.id` | `model_call_started` | same value, flat name |
| `gen_ai.system` | `model_call_started` | provider name |
| `gen_ai.request.model` | `model_call_started` | model id |
| `gen_ai.input.messages` | `llm_input` (replayed) | JSON array `[{role, content}]` of the user prompt, clamped |
| `gen_ai.input.history` | `llm_input` | JSON array of prior messages, clamped |
| `openclaw.content.input_messages` | `llm_input` | raw user prompt text |
| `openclaw.content.system_prompt_sha256` | `llm_input` | always written when systemPrompt mode = `hash` (default) |
| `openclaw.content.system_prompt` | `llm_input` | only when systemPrompt mode = `full` |
| `gen_ai.output.messages` | `llm_output` | JSON array of assistant texts |
| `openclaw.content.output_messages` | `llm_output` | newline-joined assistant texts |
| `gen_ai.usage.input_tokens` / `output_tokens` / `total_tokens` | `llm_output` | from `event.usage` |
| `gen_ai.usage.cache_read_tokens` / `cache_write_tokens` | `llm_output` | when present in usage |
| `openclaw.model.duration_ms` / `ttfb_ms` / `request_bytes` / `response_bytes` / `outcome` | `model_call_ended` | |
| `openclaw.tool.name` / `call_id` | `before_tool_call` | requires `allowConversationAccess: true` |
| `openclaw.content.tool_input` | `before_tool_call` | JSON of arguments |
| `openclaw.content.tool_output` / `tool.success` / `tool.duration_ms` | `after_tool_call` | |
---
## Configuration
```jsonc
{
"plugins": {
"allow": ["openclaw-foundry-integration"],
"entries": {
"openclaw-foundry-integration": {
"enabled": true,
"hooks": {
// REQUIRED — without this OpenClaw silently denies the
// llm_input / llm_output / before_tool_call / after_tool_call
// hooks.
"allowConversationAccess": true
},
"config": {
"conversationId": {
"prefix": "oc",
"hashLength": 16,
"anonymousId": "oc-anon"
},
"captureContent": {
"inputMessages": true,
"outputMessages": true,
"systemPrompt": "hash", // "off" | "hash" | "full"
"toolInputs": true,
"toolOutputs": true,
"maxAttributeChars": 8000
}
}
}
},
"load": {
"paths": ["~/.openclaw/extensions/openclaw-foundry-integration"]
}
}
}
```
`systemPrompt` defaults to `"hash"` because the OpenClaw default agent prompt is ~36 KB of identity text that quickly dominates trace ingestion cost. The hash still lets you correlate "did the prompt change?" without reproducing it on every span.
---
## Installation (Azure VM)
```bash
oc install-foundry-plugin family-claw.multiagentai.co
oc deploy-config family-claw.multiagentai.co
oc verify-foundry-plugin family-claw.multiagentai.co
oc smoke-test --extended family-claw.multiagentai.co
```
The installer:
1. Downloads / copies the plugin into `~/.openclaw/extensions/openclaw-foundry-integration/`
2. Runs `npm install --no-save --no-package-lock --omit=dev @opentelemetry/api@^1.9.0` inside the plugin dir (the OpenClaw plugin loader walks `node_modules` from the plugin dir, not from the gateway install).
3. Merges the entry into `plugins.allow`, `plugins.entries`, and `plugins.load.paths` with `hooks.allowConversationAccess: true`.
4. Optionally removes the legacy `openclaw-conversation-id` plugin with `--cleanup-old`.
---
## Self-test
```bash
node selftest/synthetic-turn.js
```
Returns exit 0 + JSON `{"ok": true, ...}` when every required attribute lands on a fake span. Exit 1 + JSON listing the missing attributes when the runtime regresses.
---
## Hook payload reference
OpenClaw's typed hook payloads are not officially documented. The shapes this plugin relies on are validated by direct probe (logged at `/memories/repo/openclaw-typed-hooks-api.md` in the upstream operator's repo) and exercised end-to-end by `selftest/synthetic-turn.js` and the unit tests under `test/`.
If a future OpenClaw release changes hook field names, the selftest will fail loudly *before* the rollout reaches production.
---
## License
MIT — see [LICENSE](LICENSE).
voice
Comments
Sign in to leave a comment