mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
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:
@@ -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 "
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user