diff --git a/src/phoenix/experiments/functions.py b/src/phoenix/experiments/functions.py index d9615cb5b5..ab95a8cc7f 100644 --- a/src/phoenix/experiments/functions.py +++ b/src/phoenix/experiments/functions.py @@ -72,15 +72,16 @@ ) from phoenix.experiments.utils import get_dataset_experiments_url, get_experiment_url, get_func_name from phoenix.trace.attributes import flatten +from phoenix.utilities.client import VersionedAsyncClient, VersionedClient from phoenix.utilities.json import jsonify def _phoenix_clients() -> Tuple[httpx.Client, httpx.AsyncClient]: headers = get_env_client_headers() - return httpx.Client( + return VersionedClient( base_url=get_base_url(), headers=headers, - ), httpx.AsyncClient( + ), VersionedAsyncClient( base_url=get_base_url(), headers=headers, ) diff --git a/src/phoenix/server/app.py b/src/phoenix/server/app.py index 5aa1ce483f..f0c00391dd 100644 --- a/src/phoenix/server/app.py +++ b/src/phoenix/server/app.py @@ -88,6 +88,7 @@ from phoenix.server.grpc_server import GrpcServer from phoenix.server.telemetry import initialize_opentelemetry_tracer_provider from phoenix.trace.schemas import Span +from phoenix.utilities.client import PHOENIX_SERVER_VERSION_HEADER if TYPE_CHECKING: from opentelemetry.trace import TracerProvider @@ -168,8 +169,11 @@ async def dispatch( request: Request, call_next: RequestResponseEndpoint, ) -> Response: + from phoenix import __version__ as phoenix_version + response = await call_next(request) response.headers["x-colab-notebook-cache-control"] = "no-cache" + response.headers[PHOENIX_SERVER_VERSION_HEADER] = phoenix_version return response diff --git a/src/phoenix/session/client.py b/src/phoenix/session/client.py index fc16d46c84..c72b3717ce 100644 --- a/src/phoenix/session/client.py +++ b/src/phoenix/session/client.py @@ -49,6 +49,7 @@ from phoenix.trace import Evaluations, TraceDataset from phoenix.trace.dsl import SpanQuery from phoenix.trace.otel import encode_span_to_otlp +from phoenix.utilities.client import VersionedClient logger = logging.getLogger(__name__) @@ -92,7 +93,7 @@ def __init__( host = "127.0.0.1" base_url = endpoint or get_env_collector_endpoint() or f"http://{host}:{get_env_port()}" self._base_url = base_url if base_url.endswith("/") else base_url + "/" - self._client = httpx.Client(headers=headers) + self._client = VersionedClient(headers=headers) weakref.finalize(self, self._client.close) if warn_if_server_not_running: self._warn_if_phoenix_is_not_running() diff --git a/src/phoenix/utilities/client.py b/src/phoenix/utilities/client.py new file mode 100644 index 0000000000..ceb7e3211a --- /dev/null +++ b/src/phoenix/utilities/client.py @@ -0,0 +1,116 @@ +import warnings +from typing import Any + +import httpx + +PHOENIX_SERVER_VERSION_HEADER = "x-phoenix-server-version" + + +class VersionedClient(httpx.Client): + """ + A httpx.Client wrapper that warns if there is a server/client version mismatch. + """ + + def __init__(self, *args: Any, **kwargs: Any): + from phoenix import __version__ as phoenix_version + + super().__init__(*args, **kwargs) + self._client_phoenix_version = phoenix_version + self._warned_on_minor_version_mismatch = False + + def _check_version(self, response: httpx.Response) -> None: + server_version = response.headers.get(PHOENIX_SERVER_VERSION_HEADER) + + if server_version is None: + warnings.warn( + "The Phoenix server has an unknown version and may have compatibility issues." + ) + return + + try: + client_major, client_minor, client_patch = map( + int, self._client_phoenix_version.split(".") + ) + server_major, server_minor, server_patch = map(int, server_version.split(".")) + if abs(server_major - client_major) >= 1: + warnings.warn( + f"⚠️⚠️ The Phoenix server ({server_version}) and client " + f"({self._client_phoenix_version}) versions are severely mismatched. Upgrade " + " either the client or server to ensure API compatibility ⚠️⚠️" + ) + elif ( + abs(server_minor - client_minor) >= 1 and not self._warned_on_minor_version_mismatch + ): + self._warned_on_minor_version_mismatch = True + warnings.warn( + f"The Phoenix server ({server_version}) and client " + f"({self._client_phoenix_version}) versions are mismatched and may have " + "compatibility issues." + ) + except ValueError: + # if either the client or server version includes a suffix e.g. "rc1", check for an + # exact version match of the version string + if self._client_phoenix_version != server_version: + warnings.warn( + f"The Phoenix server ({server_version}) and client " + f"({self._client_phoenix_version}) versions are mismatched and may have " + "compatibility issues." + ) + + def request(self, *args: Any, **kwargs: Any) -> httpx.Response: + response = super().request(*args, **kwargs) + self._check_version(response) + return response + + +class VersionedAsyncClient(httpx.AsyncClient): + """ + A httpx.Client wrapper that warns if there is a server/client version mismatch. + """ + + def __init__(self, *args: Any, **kwargs: Any): + from phoenix import __version__ as phoenix_version + + super().__init__(*args, **kwargs) + self._client_phoenix_version = phoenix_version + self._warned_on_minor_version_mismatch = False + + def _check_version(self, response: httpx.Response) -> None: + server_version = response.headers.get(PHOENIX_SERVER_VERSION_HEADER) + + if server_version is None: + return + + try: + client_major, client_minor, client_patch = map( + int, self._client_phoenix_version.split(".") + ) + server_major, server_minor, server_patch = map(int, server_version.split(".")) + if abs(server_major - client_major) >= 1: + warnings.warn( + f"⚠️⚠️ The Phoenix server ({server_version}) and client " + f"({self._client_phoenix_version}) versions are severely mismatched. Upgrade " + " either the client or server to ensure API compatibility ⚠️⚠️" + ) + elif ( + abs(server_minor - client_minor) >= 1 and not self._warned_on_minor_version_mismatch + ): + self._warned_on_minor_version_mismatch = True + warnings.warn( + f"The Phoenix server ({server_version}) and client " + f"({self._client_phoenix_version}) versions are mismatched and may have " + "compatibility issues." + ) + except ValueError: + # if the version includes a suffix e.g. "rc1", check for an exact version match + if self._client_phoenix_version != server_version: + warnings.warn( + f"The Phoenix server ({server_version}) and client " + f"({self._client_phoenix_version}) versions are mismatched and may have " + "compatibility issues." + ) + + async def request(self, *args: Any, **kwargs: Any) -> httpx.Response: + response = await super().request(*args, **kwargs) + self._check_version(response) + return response