-
Notifications
You must be signed in to change notification settings - Fork 4.7k
fix(tracing): honour OPENAI_TRACING_INGEST_ENDPOINT and warn on OPENAI_BASE_URL mismatch #5013
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?
Changes from all commits
a73d393
ca34a12
5c33235
938fd7f
444ddff
9e71d7f
f730292
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 |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| from collections.abc import Callable | ||
| from functools import cached_property | ||
| from typing import Any, cast | ||
| from urllib.parse import urlsplit | ||
|
|
||
| import httpx2 | ||
|
|
||
|
|
@@ -23,6 +24,51 @@ | |
| from .spans import Span | ||
| from .traces import Trace | ||
|
|
||
| # Warn once per process when OPENAI_BASE_URL is set but traces still go to OpenAI. | ||
| _warned_default_trace_endpoint_with_custom_model_base = False | ||
|
|
||
|
|
||
| def _split_url(url: str) -> tuple[str, str, int | None, str] | None: | ||
| try: | ||
| parts = urlsplit(url) | ||
| hostname = parts.hostname or "" | ||
| try: | ||
| port = parts.port | ||
| except ValueError: | ||
| port = None | ||
| return parts.scheme, hostname, port, parts.path | ||
| except ValueError: | ||
| return None | ||
|
|
||
|
|
||
| def _url_origin_and_path(url: str) -> tuple[str, str] | None: | ||
| parsed = _split_url(url) | ||
| if parsed is None: | ||
| return None | ||
| scheme, hostname, port, path = parsed | ||
| if not hostname: | ||
| return None | ||
| scheme = scheme.lower() or "https" | ||
| hostname = hostname.lower() | ||
| if port is None or (scheme == "https" and port == 443) or (scheme == "http" and port == 80): | ||
| origin = f"{scheme}://{hostname}" | ||
| else: | ||
| origin = f"{scheme}://{hostname}:{port}" | ||
| return origin, path.rstrip("/") | ||
|
|
||
|
|
||
| def _url_origin(url: str) -> str | None: | ||
| parsed = _url_origin_and_path(url) | ||
| return None if parsed is None else parsed[0] | ||
|
|
||
|
|
||
| def _redact_url_for_log(url: str) -> str: | ||
| """Drop userinfo, path, query, and fragment so gateway credentials never reach logs.""" | ||
| origin = _url_origin(url) | ||
| if origin is None: | ||
| return "<invalid-url>" | ||
| return origin | ||
|
|
||
|
|
||
| class ConsoleSpanExporter(TracingExporter): | ||
| """Prints the traces and spans to the console.""" | ||
|
|
@@ -59,7 +105,7 @@ def __init__( | |
| api_key: str | None = None, | ||
| organization: str | None = None, | ||
| project: str | None = None, | ||
| endpoint: str = _OPENAI_TRACING_INGEST_ENDPOINT, | ||
| endpoint: str | None = None, | ||
| max_retries: int = 3, | ||
| base_delay: float = 1.0, | ||
| max_delay: float = 30.0, | ||
|
|
@@ -72,15 +118,17 @@ def __init__( | |
| `os.environ["OPENAI_ORG_ID"]` if not provided. | ||
| project: The OpenAI project to use. Defaults to | ||
| `os.environ["OPENAI_PROJECT_ID"]` if not provided. | ||
| endpoint: The HTTP endpoint to which traces/spans are posted. | ||
| endpoint: The HTTP endpoint to which traces/spans are posted. Defaults to | ||
| `os.environ["OPENAI_TRACING_INGEST_ENDPOINT"]` if not provided, otherwise the | ||
| OpenAI traces ingest endpoint. This is independent of `OPENAI_BASE_URL`. | ||
| max_retries: Maximum number of retries upon failures. | ||
| base_delay: Base delay (in seconds) for the first backoff. | ||
| max_delay: Maximum delay (in seconds) for backoff growth. | ||
| """ | ||
| self._api_key = api_key | ||
| self._organization = organization | ||
| self._project = project | ||
| self.endpoint = endpoint | ||
| self._endpoint = endpoint | ||
| self.max_retries = max_retries | ||
| self.base_delay = base_delay | ||
| self.max_delay = max_delay | ||
|
|
@@ -115,6 +163,54 @@ def organization(self): | |
| def project(self): | ||
| return self._project or os.environ.get("OPENAI_PROJECT_ID") | ||
|
|
||
| def _invalidate_endpoint(self) -> None: | ||
| self.__dict__.pop("_resolved_endpoint", None) | ||
|
|
||
| @property | ||
| def endpoint(self) -> str: | ||
| if "_resolved_endpoint" not in self.__dict__: | ||
| self.__dict__["_resolved_endpoint"] = ( | ||
| self._endpoint | ||
| or os.environ.get("OPENAI_TRACING_INGEST_ENDPOINT") | ||
| or self._OPENAI_TRACING_INGEST_ENDPOINT | ||
| ) | ||
| return self.__dict__["_resolved_endpoint"] | ||
|
|
||
| @endpoint.setter | ||
| def endpoint(self, value: str) -> None: | ||
| self._endpoint = value | ||
| self._invalidate_endpoint() | ||
|
|
||
| def _warn_if_trace_endpoint_ignores_model_base_url(self) -> None: | ||
| global _warned_default_trace_endpoint_with_custom_model_base | ||
| if _warned_default_trace_endpoint_with_custom_model_base: | ||
| return | ||
| model_base = (os.environ.get("OPENAI_BASE_URL") or "").strip() | ||
|
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.
When AGENTS.md reference: AGENTS.md:L115-L115 Useful? React with 👍 / 👎.
Author
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. Addressed in the follow-up commit. The exporter cannot see a later |
||
| if not model_base: | ||
| return | ||
| if not self._should_sanitize_for_openai_tracing_api(): | ||
| return | ||
| model_origin = _url_origin(model_base) | ||
| if model_origin is not None and model_origin == _url_origin( | ||
| self._OPENAI_TRACING_INGEST_ENDPOINT | ||
| ): | ||
| return | ||
| _warned_default_trace_endpoint_with_custom_model_base = True | ||
| if self._endpoint is not None: | ||
| redirect_hint = ( | ||
| "Pass a different endpoint= to BackendSpanExporter, or omit that argument so " | ||
| "OPENAI_TRACING_INGEST_ENDPOINT can redirect traces" | ||
| ) | ||
| else: | ||
| redirect_hint = "Set OPENAI_TRACING_INGEST_ENDPOINT to redirect traces" | ||
| logger.warning( | ||
| "[non-fatal] Tracing still exports to %s while OPENAI_BASE_URL is %s. " | ||
| "%s, or disable tracing with OPENAI_AGENTS_DISABLE_TRACING=1.", | ||
| _redact_url_for_log(self.endpoint), | ||
| _redact_url_for_log(model_base), | ||
| redirect_hint, | ||
| ) | ||
|
|
||
| def export(self, items: list[Trace | Span[Any]]) -> None: | ||
| self._export_with_deadline(items, deadline=None) | ||
|
|
||
|
|
@@ -133,6 +229,8 @@ def _export_with_deadline(self, items: list[Trace | Span[Any]], deadline: float | |
| logger.warning("OPENAI_API_KEY is not set, skipping trace export") | ||
| continue | ||
|
|
||
| self._warn_if_trace_endpoint_ignores_model_base_url() | ||
|
|
||
| sanitize_for_openai = self._should_sanitize_for_openai_tracing_api() | ||
| data: list[dict[str, Any]] = [] | ||
| for item in grouped: | ||
|
|
@@ -256,7 +354,9 @@ def _sleep_before_retry(self, sleep_time: float, deadline: float | None) -> bool | |
| return True | ||
|
|
||
| def _should_sanitize_for_openai_tracing_api(self) -> bool: | ||
| return self.endpoint.rstrip("/") == self._OPENAI_TRACING_INGEST_ENDPOINT.rstrip("/") | ||
| endpoint = _url_origin_and_path(self.endpoint) | ||
| default = _url_origin_and_path(self._OPENAI_TRACING_INGEST_ENDPOINT) | ||
| return endpoint is not None and endpoint == default | ||
|
|
||
| def _sanitize_for_openai_tracing_api(self, payload_item: dict[str, Any]) -> dict[str, Any]: | ||
| """Drop or truncate span fields known to be rejected by traces ingest.""" | ||
|
|
||
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.
When an application explicitly sets
OPENAI_BASE_URL=https://api.openai.com/v1(or the trailing-slash variant), this truthiness check emits the new once-only warning even thoughOpenAIProvider._get_clientsends model requests to the same OpenAI host as the default tracing exporter. The suggested redirect/disable action is therefore a false operational diagnostic; compare the normalized model endpoint’s origin with the OpenAI origin before consuming the warning.AGENTS.md reference: AGENTS.md:L115-L115
Useful? React with 👍 / 👎.
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.
Addressed. The warning now compares origins, so
OPENAI_BASE_URL=https://api.openai.com/v1(and the trailing-slash variant) no longer emits a false mismatch diagnostic.