-
Notifications
You must be signed in to change notification settings - Fork 263
docs: add AG2, AgentGateway, and Genkit integration pages (community PRs, review fixes applied) #3244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
docs: add AG2, AgentGateway, and Genkit integration pages (community PRs, review fixes applied) #3244
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,277 @@ | ||
| --- | ||
| title: Observability for AG2 with Langfuse Integration | ||
| sidebarTitle: AG2 | ||
| logo: /images/integrations/ag2_icon.svg | ||
| description: Integrate Langfuse with AG2 via native OpenTelemetry tracing for full observability into multi-agent conversations, LLM calls, tool executions, and costs. | ||
| category: Integrations | ||
| --- | ||
|
|
||
| # Integrate Langfuse with AG2 | ||
|
|
||
| This guide shows how to integrate **Langfuse** with **AG2** using AG2's built-in OpenTelemetry tracing for full observability into your multi-agent workflows. | ||
|
|
||
| > **What is AG2?** [AG2](https://ag2.ai/) ([GitHub](https://github.com/ag2ai/ag2)) is an open-source Python framework for building multi-agent AI systems. AG2 provides tools for orchestrating collaborative agents, tool use, group chats, and distributed agent-to-agent (A2A) deployments. AG2 v0.11+ includes native [OpenTelemetry tracing](https://docs.ag2.ai/docs/user-guide/tracing/opentelemetry) that captures every conversation, agent turn, LLM call, tool execution, and speaker selection as structured spans. | ||
|
|
||
| > **What is Langfuse?** [Langfuse](https://langfuse.com) is an open-source LLM engineering platform. It offers tracing and monitoring capabilities for AI applications. Langfuse helps developers debug, analyze, and optimize their AI systems by providing detailed insights and integrating with a wide array of tools and frameworks through native integrations, OpenTelemetry, and dedicated SDKs. | ||
|
|
||
| <Callout type="info"> | ||
| AG2 is the community-driven continuation of AutoGen. If you are using | ||
| Microsoft AutoGen instead, see the [AutoGen | ||
| integration](/integrations/frameworks/autogen), which traces via OpenLIT | ||
| rather than AG2's native OpenTelemetry support. | ||
| </Callout> | ||
|
|
||
| ## Get started | ||
|
|
||
| AG2's native OpenTelemetry tracing exports rich, hierarchical spans that Langfuse ingests directly — no intermediary libraries needed. | ||
|
|
||
| <Steps> | ||
| ### Step 1: Install dependencies | ||
|
|
||
| ```bash | ||
| pip install "ag2[openai,tracing]" opentelemetry-exporter-otlp langfuse -U | ||
| ``` | ||
|
|
||
| ### Step 2: Configure Langfuse SDK | ||
|
|
||
| Set up your Langfuse API keys. You can get these keys by signing up for a free [Langfuse Cloud](https://cloud.langfuse.com/) account or by [self-hosting Langfuse](https://langfuse.com/self-hosting). | ||
|
|
||
| ```python | ||
| import os | ||
|
|
||
| # Get keys for your project from the project settings page: https://cloud.langfuse.com | ||
| os.environ["LANGFUSE_PUBLIC_KEY"] = "pk-lf-..." | ||
| os.environ["LANGFUSE_SECRET_KEY"] = "sk-lf-..." | ||
| os.environ["LANGFUSE_BASE_URL"] = "https://cloud.langfuse.com" # 🇪🇺 EU region | ||
| # os.environ["LANGFUSE_BASE_URL"] = "https://us.cloud.langfuse.com" # 🇺🇸 US region | ||
|
|
||
| # Your OpenAI key | ||
| os.environ["OPENAI_API_KEY"] = "sk-proj-..." | ||
| ``` | ||
|
|
||
| Initialize the Langfuse client and verify the connection: | ||
|
|
||
| ```python | ||
| from langfuse import get_client | ||
|
|
||
| langfuse = get_client() | ||
|
|
||
| if langfuse.auth_check(): | ||
| print("Langfuse client is authenticated and ready!") | ||
| ``` | ||
|
|
||
| ### Step 3: Configure OpenTelemetry to export to Langfuse | ||
|
|
||
| Set up an OpenTelemetry `TracerProvider` that exports spans directly to Langfuse's OTel endpoint: | ||
|
|
||
| ```python | ||
| import os | ||
| import base64 | ||
| from opentelemetry import trace | ||
| from opentelemetry.sdk.resources import Resource | ||
| from opentelemetry.sdk.trace import TracerProvider | ||
| from opentelemetry.sdk.trace.export import BatchSpanProcessor | ||
| from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter | ||
|
|
||
| LANGFUSE_PUBLIC_KEY = os.environ["LANGFUSE_PUBLIC_KEY"] | ||
| LANGFUSE_SECRET_KEY = os.environ["LANGFUSE_SECRET_KEY"] | ||
| LANGFUSE_BASE_URL = os.environ["LANGFUSE_BASE_URL"] | ||
|
|
||
| auth = base64.b64encode( | ||
| f"{LANGFUSE_PUBLIC_KEY}:{LANGFUSE_SECRET_KEY}".encode() | ||
| ).decode() | ||
|
|
||
| resource = Resource.create({"service.name": "ag2-langfuse-demo"}) | ||
| tracer_provider = TracerProvider(resource=resource) | ||
|
|
||
| exporter = OTLPSpanExporter( | ||
| endpoint=f"{LANGFUSE_BASE_URL}/api/public/otel/v1/traces", | ||
| headers={"Authorization": f"Basic {auth}"}, | ||
| ) | ||
| tracer_provider.add_span_processor(BatchSpanProcessor(exporter)) | ||
| trace.set_tracer_provider(tracer_provider) | ||
| ``` | ||
|
|
||
| ### Step 4: Instrument agents and run | ||
|
|
||
| Create AG2 agents and instrument them with AG2's built-in tracing: | ||
|
|
||
| ```python | ||
| from autogen import ConversableAgent, LLMConfig | ||
| from autogen.opentelemetry import instrument_agent, instrument_llm_wrapper | ||
|
|
||
| llm_config = LLMConfig({"model": "gpt-4o-mini"}) | ||
|
|
||
| assistant = ConversableAgent( | ||
| name="assistant", | ||
| system_message="You are a helpful assistant.", | ||
| llm_config=llm_config, | ||
| human_input_mode="NEVER", | ||
| ) | ||
|
|
||
| user_proxy = ConversableAgent( | ||
| name="user_proxy", | ||
| human_input_mode="NEVER", | ||
| max_consecutive_auto_reply=1, | ||
| ) | ||
|
|
||
| # Instrument agents and LLM calls | ||
| instrument_llm_wrapper(tracer_provider=tracer_provider) | ||
| instrument_agent(assistant, tracer_provider=tracer_provider) | ||
| instrument_agent(user_proxy, tracer_provider=tracer_provider) | ||
|
|
||
| # Run a chat — traces are sent to Langfuse automatically | ||
| result = user_proxy.run( | ||
| assistant, | ||
| message="What is the capital of France?", | ||
| max_turns=2, | ||
| ) | ||
| result.process() | ||
|
|
||
| # Flush spans before exit | ||
| tracer_provider.shutdown() | ||
| ``` | ||
|
|
||
| ### Step 5: View traces in Langfuse | ||
|
|
||
| After executing the application, navigate to your Langfuse Trace Table. You will see hierarchical traces showing the full conversation flow — each agent turn, LLM call (with model, tokens, and cost), and tool execution nested in a tree that mirrors your agent workflow. | ||
|
|
||
| AG2's tracing emits 7 span types that map to Langfuse observations: | ||
|
|
||
| | AG2 span type | What it captures | | ||
| | ------------------- | ------------------------------------------------- | | ||
| | `conversation` | Overall chat with total token usage and cost | | ||
| | `agent` | Individual agent turn with input/output messages | | ||
| | `llm` | LLM API call with model, tokens, cost, parameters | | ||
| | `tool` | Tool execution with arguments and return value | | ||
| | `code_execution` | Code execution with output | | ||
| | `human_input` | Human input prompt and response | | ||
| | `speaker_selection` | Group chat speaker selection with candidates | | ||
|
|
||
| </Steps> | ||
|
|
||
| ## Tool use example | ||
|
|
||
| AG2 traces tool executions with full argument and return value capture. This example reuses the `tracer_provider` configured in Step 3 above: | ||
|
|
||
| ```python | ||
| from typing import Annotated | ||
| from autogen import ConversableAgent, LLMConfig | ||
| from autogen.opentelemetry import instrument_agent, instrument_llm_wrapper | ||
| from autogen.tools import tool | ||
|
|
||
| @tool(description="Get weather information for a city") | ||
| def get_weather(city: Annotated[str, "The city name"]) -> str: | ||
| """Get weather information for a city.""" | ||
| weather_data = { | ||
| "new york": "Sunny, 72F", | ||
| "london": "Cloudy, 15C", | ||
| "tokyo": "Rainy, 18C", | ||
| } | ||
| return weather_data.get(city.lower(), f"Weather data not available for {city}") | ||
|
|
||
| llm_config = LLMConfig({"model": "gpt-4o-mini"}) | ||
|
|
||
| weather_agent = ConversableAgent( | ||
| name="weather", | ||
| system_message="Use the get_weather tool to answer weather questions.", | ||
| functions=[get_weather], | ||
| llm_config=llm_config, | ||
| human_input_mode="NEVER", | ||
| ) | ||
|
|
||
| instrument_llm_wrapper(tracer_provider=tracer_provider) | ||
| instrument_agent(weather_agent, tracer_provider=tracer_provider) | ||
|
|
||
| result = weather_agent.run(message="What is the weather in Tokyo?", max_turns=2) | ||
| result.process() | ||
| ``` | ||
|
|
||
| In Langfuse, `execute_tool get_weather` spans appear nested under the agent span, with tool arguments and return values visible in the observation input/output. | ||
|
|
||
| ## Group chat example | ||
|
|
||
| For group chats, use `instrument_pattern` to instrument all agents in a single call (again reusing the `tracer_provider` from Step 3): | ||
|
|
||
| ```python | ||
| from autogen import ConversableAgent, LLMConfig | ||
| from autogen.agentchat import run_group_chat | ||
| from autogen.agentchat.group.patterns import AutoPattern | ||
| from autogen.opentelemetry import instrument_llm_wrapper, instrument_pattern | ||
|
|
||
| llm_config = LLMConfig({"model": "gpt-4o-mini"}) | ||
|
|
||
| researcher = ConversableAgent( | ||
| name="researcher", | ||
| system_message="You research topics and provide factual information.", | ||
| llm_config=llm_config, | ||
| human_input_mode="NEVER", | ||
| ) | ||
|
|
||
| writer = ConversableAgent( | ||
| name="writer", | ||
| system_message="You write clear summaries. Say TERMINATE when done.", | ||
| llm_config=llm_config, | ||
| human_input_mode="NEVER", | ||
| ) | ||
|
|
||
| user = ConversableAgent(name="user", human_input_mode="NEVER", llm_config=False) | ||
|
|
||
| pattern = AutoPattern( | ||
| initial_agent=researcher, | ||
| agents=[researcher, writer], | ||
| user_agent=user, | ||
| group_manager_args={"llm_config": llm_config}, | ||
| ) | ||
|
|
||
| instrument_llm_wrapper(tracer_provider=tracer_provider) | ||
| instrument_pattern(pattern, tracer_provider=tracer_provider) | ||
|
|
||
| result = run_group_chat( | ||
| pattern=pattern, | ||
| messages="Explain quantum computing in simple terms.", | ||
| max_rounds=5, | ||
| ) | ||
| result.process() | ||
| ``` | ||
|
|
||
| This produces hierarchical traces in Langfuse including `speaker_selection` spans that show which agent was chosen and why. | ||
|
|
||
| ## Distributed tracing with A2A | ||
|
|
||
| AG2 supports the [A2A (Agent-to-Agent) protocol](https://a2a-protocol.org/) for distributed multi-service deployments. When agents run as separate services, AG2 propagates W3C Trace Context headers across HTTP calls, and Langfuse stitches the spans into a unified trace. | ||
|
|
||
| ```python | ||
| from autogen import ConversableAgent, LLMConfig | ||
| from autogen.a2a import A2aAgentServer | ||
| from autogen.opentelemetry import instrument_a2a_server | ||
|
|
||
| agent = ConversableAgent( | ||
| name="assistant", | ||
| llm_config=LLMConfig({"model": "gpt-4o-mini"}), | ||
| ) | ||
|
|
||
| server = A2aAgentServer(agent, url="http://localhost:18123/") | ||
| instrument_a2a_server(server, tracer_provider=tracer_provider) | ||
| ``` | ||
|
|
||
| ## Environment variables (alternative setup) | ||
|
|
||
| Instead of configuring the exporter in code, you can use environment variables: | ||
|
|
||
| ```bash | ||
| export OTEL_EXPORTER_OTLP_ENDPOINT="https://cloud.langfuse.com/api/public/otel" | ||
| export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic $(echo -n 'pk-lf-...:sk-lf-...' | base64)" | ||
| export OTEL_SERVICE_NAME="ag2-app" | ||
| ``` | ||
|
Check warning on line 266 in content/integrations/frameworks/ag2.mdx
|
||
|
|
||
| ## Learn more | ||
|
|
||
| - [AG2 OpenTelemetry documentation](https://docs.ag2.ai/docs/user-guide/tracing/opentelemetry) | ||
| - [AG2 GitHub](https://github.com/ag2ai/ag2) | ||
| - [Langfuse OpenTelemetry integration](/integrations/native/opentelemetry) | ||
| - [OpenTelemetry GenAI Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/) | ||
|
|
||
| import LearnMore from "@/components-mdx/integration-learn-more.mdx"; | ||
|
|
||
| <LearnMore /> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| --- | ||
| title: Trace Genkit with Langfuse via OpenTelemetry | ||
| sidebarTitle: Genkit | ||
| logo: /images/integrations/genkit_icon.svg | ||
| description: Learn how to send Genkit traces to Langfuse from Go applications using OpenTelemetry. | ||
| category: Integrations | ||
| --- | ||
|
|
||
| # Trace Genkit with Langfuse | ||
|
|
||
| Langfuse can ingest Genkit traces via OpenTelemetry. Once your Genkit application exports OTLP spans, Langfuse maps Genkit spans into traces and observations with model input, output, and usage data when available. | ||
|
|
||
| > **What is Genkit?** [Genkit](https://genkit.dev/) is an open-source framework for building AI features and agentic workflows with structured flows, tool calling, and model integrations. | ||
|
|
||
| > **What is Langfuse?** [Langfuse](https://langfuse.com/) is an open-source LLM engineering platform for observability, prompt management, and evals. | ||
|
|
||
| <Callout type="info" emoji="ℹ️"> | ||
| This Langfuse integration is OpenTelemetry-based, not Go-specific. If your Genkit setup already exports OTLP traces, you can point that exporter at Langfuse. For Genkit Go, the common setup is the [Genkit OpenTelemetry plugin](https://github.com/genkit-ai/opentelemetry-go-plugin?tab=readme-ov-file#langfuse). | ||
| </Callout> | ||
|
|
||
| <Steps> | ||
| ## Step 1: Install the Genkit OpenTelemetry plugin | ||
|
|
||
| ```bash | ||
| go get github.com/xavidop/genkit-opentelemetry-go | ||
| ``` | ||
|
|
||
| The package is documented in the [`genkit-ai/opentelemetry-go-plugin` repository](https://github.com/genkit-ai/opentelemetry-go-plugin). | ||
|
Check failure on line 28 in content/integrations/frameworks/genkit.mdx
|
||
|
Comment on lines
+22
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 The Callout (line 18) and prose (line 28) both link to Extended reasoning...The bug.
Why this is a real problem. In Go, the module path IS the package identity — Impact on the reader. This is the very first step of the integration (Step 1: Install the Genkit OpenTelemetry plugin). A reader who follows the prose link lands on Step-by-step proof.
Why this wasn't caught. The PR description itself flags Genkit content as "as written by the original authors — worth a quick spot-check," indicating the maintainer already suspected this area needed verification. Fix. Pick one authoritative source and align both sides. If |
||
|
|
||
| ## Step 2: Configure Langfuse credentials | ||
|
|
||
| ```bash | ||
| export LANGFUSE_BASE_URL="https://cloud.langfuse.com" # 🇪🇺 EU region | ||
| # export LANGFUSE_BASE_URL="https://us.cloud.langfuse.com" # 🇺🇸 US region | ||
|
|
||
| export LANGFUSE_PUBLIC_KEY="pk-lf-..." | ||
| export LANGFUSE_SECRET_KEY="sk-lf-..." | ||
| ``` | ||
|
|
||
| ## Step 3: Initialize the OpenTelemetry plugin | ||
|
|
||
| Configure the plugin to send OTLP/HTTP traces to your Langfuse instance. Langfuse currently accepts OTLP over HTTP, so set `OTLPUseHTTP: true`. The example below disables metrics export because this setup is only sending traces to Langfuse. | ||
|
|
||
| ```go | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/base64" | ||
| "log" | ||
| "os" | ||
|
|
||
| "github.com/firebase/genkit/go/genkit" | ||
| opentelemetry "github.com/xavidop/genkit-opentelemetry-go" | ||
| ) | ||
|
|
||
| func main() { | ||
| baseURL := os.Getenv("LANGFUSE_BASE_URL") | ||
| if baseURL == "" { | ||
| baseURL = "https://cloud.langfuse.com" | ||
| } | ||
|
|
||
| publicKey := os.Getenv("LANGFUSE_PUBLIC_KEY") | ||
| secretKey := os.Getenv("LANGFUSE_SECRET_KEY") | ||
| if publicKey == "" || secretKey == "" { | ||
| log.Fatal("LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY must be set") | ||
| } | ||
|
|
||
| auth := base64.StdEncoding.EncodeToString([]byte(publicKey + ":" + secretKey)) | ||
|
|
||
| plugin := opentelemetry.New(opentelemetry.Config{ | ||
| ServiceName: "my-genkit-app", | ||
| ForceExport: true, | ||
| DisableMetricsExporter: true, | ||
| OTLPEndpoint: baseURL + "/api/public/otel/v1/traces", | ||
| OTLPUseHTTP: true, | ||
| OTLPHeaders: map[string]string{ | ||
| "Authorization": "Basic " + auth, | ||
| "X-Langfuse-Ingestion-Version": "4", | ||
| }, | ||
| }) | ||
|
|
||
| genkit.Init( | ||
| context.Background(), | ||
| genkit.WithPlugins(plugin), | ||
| ) | ||
| } | ||
| ``` | ||
|
|
||
| ## Step 4: Run your Genkit flows | ||
|
|
||
| After initializing the plugin, Genkit flows and model calls emit OpenTelemetry spans. Langfuse ingests those spans and displays them as traces with nested observations, including inputs, outputs, tool calls, and token usage when Genkit provides that metadata. | ||
|
|
||
| If you already use an OpenTelemetry Collector, you can route Genkit spans through the Collector and export them to Langfuse from there. For endpoint and authentication details, see the [Langfuse OpenTelemetry guide](/integrations/native/opentelemetry). | ||
|
|
||
| ## Step 5: View traces in Langfuse | ||
|
|
||
| Open **Traces** in Langfuse to inspect flow executions, model calls, latency, cost, and errors. If traces do not show up in local development, double-check `ForceExport: true`, the OTLP endpoint, and the Basic Auth credentials. | ||
|
|
||
|  | ||
|
|
||
| </Steps> | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| - **No traces appear in Langfuse** | ||
| - The Genkit OpenTelemetry plugin is initialized before your flows run. | ||
| - `OTLPUseHTTP` is set to `true`. | ||
| - The OTLP endpoint points to `https://<your-langfuse-host>/api/public/otel/v1/traces`. | ||
| - The `Authorization` header contains the Base64-encoded `public_key:secret_key` pair. | ||
| - If the `X-Langfuse-Ingestion-Version: 4` header is not set, traces may still ingest successfully but can take up to 10 minutes to appear in Langfuse. | ||
| - `ForceExport` is enabled in short-lived local processes so spans are flushed before the program exits. | ||
|
|
||
| ## Next steps | ||
|
|
||
| Once traces are flowing, you can use Langfuse to debug prompt inputs and outputs, add scores, and build dashboards on top of your Genkit workflows. | ||
|
|
||
| import NextSteps from "@/components-mdx/get-started/next-steps.mdx"; | ||
|
|
||
| <NextSteps /> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Two new bash snippets pipe the
pk:skpair throughbase64without-w0:ag2.mdx:264(OTEL_EXPORTER_OTLP_HEADERS) andagentgateway.mdx:63(LANGFUSE_AUTH). GNU coreutilsbase64wraps at 76 chars, and real Langfuse keys (pk-lf-<UUID>:sk-lf-<UUID>, ~85 chars → ~116 chars encoded) cross that threshold — the embedded newline survives$(...)(which only strips trailing newlines) and corrupts the Authorization header, causing silent 401s on Linux. Suggest adopting the pattern already documented incontent/integrations/frameworks/spring-ai.mdx:213-214(base64 -w0on Linux with a macOS-BSD fallback comment).Extended reasoning...
What is wrong
This PR adds two new bash snippets that base64-encode a Langfuse
public_key:secret_keypair without disabling GNUbase64's default line-wrapping:content/integrations/frameworks/ag2.mdx:264— setsOTEL_EXPORTER_OTLP_HEADERSinlinecontent/integrations/gateways/agentgateway.mdx:63— setsLANGFUSE_AUTHfor later use in a YAML manifestBoth use the form
$(echo -n "…" | base64)with no-w0.Step-by-step proof (Linux, real Langfuse cloud keys)
pk-lf-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX(42 chars) andsk-lf-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX(42 chars).:→ 85 characters of input.ceil(85/3)*4= 116 characters of output.base64defaults to--wrap=76, so it inserts a literal LF after column 76.$(...)command substitution strips only trailing newlines from the captured stdout — an LF at position 77 stays inside the string.agentgateway.mdx:LANGFUSE_AUTHnow contains<76 chars>\n<40 chars>, which is pasted into the Collector YAML asAuthorization: "Basic <broken>"→ OTLP server sees a malformed credential → 401.ag2.mdx:OTEL_EXPORTER_OTLP_HEADERSgets an embedded LF. HTTP header values cannot contain raw LF; the OTLP HTTP client either rejects the header or transmits a truncated one → 401.pk-lf-...:sk-lf-...shown in the docs is only 19 chars (28 encoded), so testing with the literal placeholder does NOT trigger the wrap — the bug is invisible until a reader plugs in real keys.Why existing code does not prevent it
There is no runtime guard — the docs are copy-pasted verbatim by readers. The failure mode is silent (auth error, no hint that the credential is malformed) and specific to Linux (BSD
base64on macOS does not wrap), which is the majority CI/dev environment.The repo already documents the fix
content/integrations/frameworks/spring-ai.mdx:213-214spells out the correct cross-platform pattern:The explicit
# Linux/# macOS (BSD base64 has no -w0)comments are a direct acknowledgment that the maintainers have already been bitten by this.github-copilot.mdxandother/openclaw.mdxuse the same pattern.Suggested fix
Adopt the spring-ai pattern in both new files: append
-w0to thebase64call and add a one-line macOS fallback comment. This is a one-character diff per file.Severity note
Marking as nit because several pre-existing docs (
amazon-agentcore.mdx,pipecat.mdx,vscode.mdx,existing-otel-setup.mdx, andagentgateway.mdx's own troubleshooting row at line 425) use the same non-wrapping-safe pattern, so this PR extends a pre-existing inconsistency rather than introducing a new one. It is still worth fixing on the way in — the correct pattern is one directory over inspring-ai.mdx, and following the wrong pattern here causes a real silent-auth failure for readers who paste the snippet with real UUID keys.