feat(copilot): enable OpenRouter broadcast for SDK /messages endpoint (#12277)

## Summary

OpenRouter Broadcast silently drops traces for the Anthropic-native
`/api/v1/messages` endpoint unless an `x-session-id` HTTP header is
present. This was confirmed by systematic testing against our Langfuse
integration:

| Test | Endpoint | `x-session-id` header | Broadcast to Langfuse |
|------|----------|-----------------------|----------------------|
| 1 | `/chat/completions` | N/A (body fields work) |  |
| 2 | `/messages` (body fields only) |  |  |
| 3 | `/messages` (header + body) |  |  |
| 4 | `/messages` (`metadata.user_id` only) |  |  |
| 5 | `/messages` (header only) |  |  |

**Root cause:** OpenRouter only triggers broadcast for the `/messages`
endpoint when the `x-session-id` HTTP header is present — body-level
`session_id` and `metadata.user_id` are insufficient.

### Changes
- **SDK path:** Inject `x-session-id` and `x-user-id` via
`ANTHROPIC_CUSTOM_HEADERS` env var in `_build_sdk_env()`, which the
Claude Agent SDK CLI reads and attaches to every outgoing API request
- **Non-SDK path:** Add `trace` object (`trace_name` + `environment`) to
`extra_body` for richer broadcast metadata in Langfuse

This creates complementary traces alongside the existing OTEL
integration: broadcast provides cost/usage data from OpenRouter while
OTEL provides full tool-call observability with `userId`, `sessionId`,
`environment`, and `tags`.

## Test plan
- [x] Verified via test script: `/messages` with `x-session-id` header →
trace appears in Langfuse with correct `sessionId`
- [x] Verified `/chat/completions` with `trace` object → trace appears
with custom `trace_name`
- [x] Pre-commit hooks pass (ruff, black, isort, pyright)
- [ ] Deploy to dev and verify broadcast traces appear for real copilot
SDK sessions
This commit is contained in:
Zamil Majdy
2026-03-04 16:07:48 +07:00
committed by GitHub
parent a5db9c05d0
commit 160d6eddfb
2 changed files with 33 additions and 2 deletions

View File

@@ -297,13 +297,21 @@ def _resolve_sdk_model() -> str | None:
return model
def _build_sdk_env() -> dict[str, str]:
def _build_sdk_env(
session_id: str | None = None,
user_id: str | None = None,
) -> dict[str, str]:
"""Build env vars for the SDK CLI process.
Routes API calls through OpenRouter (or a custom base_url) using
the same ``config.api_key`` / ``config.base_url`` as the non-SDK path.
This gives per-call token and cost tracking on the OpenRouter dashboard.
When *session_id* is provided, an ``x-session-id`` custom header is
injected via ``ANTHROPIC_CUSTOM_HEADERS`` so that OpenRouter Broadcast
forwards traces (including cost/usage) to Langfuse for the
``/api/v1/messages`` endpoint.
Only overrides ``ANTHROPIC_API_KEY`` when a valid proxy URL and auth
token are both present — otherwise returns an empty dict so the SDK
falls back to its default credentials.
@@ -321,6 +329,22 @@ def _build_sdk_env() -> dict[str, str]:
env["ANTHROPIC_AUTH_TOKEN"] = config.api_key
# Must be explicitly empty so the CLI uses AUTH_TOKEN instead
env["ANTHROPIC_API_KEY"] = ""
# Inject broadcast headers so OpenRouter forwards traces to Langfuse.
# The ``x-session-id`` header is *required* for the Anthropic-native
# ``/messages`` endpoint — without it broadcast silently drops the
# trace even when org-level Langfuse integration is configured.
def _safe(value: str) -> str:
"""Strip CR/LF to prevent header injection, then truncate."""
return value.replace("\r", "").replace("\n", "").strip()[:128]
headers: list[str] = []
if session_id:
headers.append(f"x-session-id: {_safe(session_id)}")
if user_id:
headers.append(f"x-user-id: {_safe(user_id)}")
if headers:
env["ANTHROPIC_CUSTOM_HEADERS"] = "\n".join(headers)
return env
@@ -706,7 +730,7 @@ async def stream_chat_completion_sdk(
set_execution_context(user_id, session, sandbox=e2b_sandbox, sdk_cwd=sdk_cwd)
try:
# Fail fast when no API credentials are available at all
sdk_env = _build_sdk_env()
sdk_env = _build_sdk_env(session_id=session_id, user_id=user_id)
if not sdk_env and not os.environ.get("ANTHROPIC_API_KEY"):
raise RuntimeError(
"No API key configured. Set OPEN_ROUTER_API_KEY "

View File

@@ -1061,6 +1061,13 @@ async def _stream_chat_chunks(
:128
] # OpenRouter limit
# Broadcast trace metadata — forwarded to Langfuse via
# OpenRouter's org-level Broadcast integration.
extra_body["trace"] = {
"trace_name": "copilot-chat",
"environment": settings.config.app_env.value,
}
# Enable adaptive thinking for Anthropic models via OpenRouter
if config.thinking_enabled and "anthropic" in model.lower():
extra_body["reasoning"] = {"enabled": True}