diff --git a/.github/renovate.json5 b/.github/renovate.json5 index ff58313e7..5d0fce2c3 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -10,8 +10,13 @@ "pip_requirements": { // Default pip_requirements fileMatch only allows [\w-]* between // "requirements" and ".txt", so per-instrumentation - // requirements.latest.txt files are skipped without this. - "managerFilePatterns": ["/(^|/)requirements\\.txt$/", "/(^|/)requirements\\.latest\\.txt$/"] + // requirements.latest.txt and requirements.-v.txt files are + // skipped without this. + "managerFilePatterns": [ + "/(^|/)requirements\\.txt$/", + "/(^|/)requirements\\.latest\\.txt$/", + "/(^|/)requirements\\.[\\w-]+-v\\d+\\.txt$/" + ] }, // Manage WEAVER_VERSION and SEMCONV_GENAI_REF in versions.env via their // `# renovate:` annotations. @@ -65,6 +70,14 @@ "matchPackageNames": ["pydantic"], "allowedVersions": "<2.13" }, + { + // requirements.-v.txt tests the newest release of an older + // major of , so itself must stay on that major while the + // rest of the file keeps updating. Add one rule per such file. + "matchFileNames": ["**/requirements.openai-v2.txt"], + "matchPackageNames": ["openai"], + "allowedVersions": "<3" + }, { "matchUpdateTypes": ["minor", "major"], "schedule": ["before 8am on Monday"] diff --git a/README.md b/README.md index 1a8d80b24..a04df5bb4 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ All instrumentations use [opentelemetry-util-genai](./util/opentelemetry-util-ge | --------------- | ----------------- | ------- | | [opentelemetry-instrumentation-genai-anthropic](./instrumentation/opentelemetry-instrumentation-genai-anthropic) | anthropic >= 0.16.0 | [1.0b0](https://pypi.org/project/opentelemetry-instrumentation-genai-anthropic/) | | [opentelemetry-instrumentation-genai-langchain](./instrumentation/opentelemetry-instrumentation-genai-langchain) | langchain >= 0.3.21 | [1.0b0](https://pypi.org/project/opentelemetry-instrumentation-genai-langchain/) | -| [opentelemetry-instrumentation-genai-openai](./instrumentation/opentelemetry-instrumentation-genai-openai) | openai >= 1.26.0 | [1.0b0](https://pypi.org/project/opentelemetry-instrumentation-genai-openai/) | +| [opentelemetry-instrumentation-genai-openai](./instrumentation/opentelemetry-instrumentation-genai-openai) | openai >= 1.26.0, < 4 | [1.0b0](https://pypi.org/project/opentelemetry-instrumentation-genai-openai/) | | [opentelemetry-instrumentation-genai-openai-agents](./instrumentation/opentelemetry-instrumentation-genai-openai-agents) | openai-agents >= 0.3.3 | [1.0b0](https://pypi.org/project/opentelemetry-instrumentation-genai-openai-agents/) | | [opentelemetry-instrumentation-google-genai](./instrumentation/opentelemetry-instrumentation-google-genai) | google-genai >= 1.32.0, <3 | [1.0b1](https://pypi.org/project/opentelemetry-instrumentation-google-genai/) | diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/393.changed b/instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/393.changed new file mode 100644 index 000000000..03ce326b0 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/393.changed @@ -0,0 +1 @@ +Extend supported version range for the openai library to include v3. diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/393.fixed b/instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/393.fixed new file mode 100644 index 000000000..293c010b9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/393.fixed @@ -0,0 +1 @@ +Fix ``server.address`` and ``server.port`` not recorded when using openai v3 (httpx2 URL type). diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/pyproject.toml b/instrumentation/opentelemetry-instrumentation-genai-openai/pyproject.toml index 117f596a6..239fd68b2 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ ] [project.optional-dependencies] -instruments = ["openai >= 1.26.0"] +instruments = ["openai >= 1.26.0, < 4"] [project.entry-points.opentelemetry_instrumentor] openai = "opentelemetry.instrumentation.genai.openai:OpenAIInstrumentor" diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/_raw_response.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/_raw_response.py index 09c34f772..b1d39a549 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/_raw_response.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/_raw_response.py @@ -16,7 +16,10 @@ runtime_checkable, ) -import httpx +try: + import httpx as _http_lib +except ImportError: + import httpx2 as _http_lib from openai import AsyncStream, Stream from wrapt import ObjectProxy @@ -51,7 +54,7 @@ class RawResponseLike(ParsableResponse, Protocol): finalize the span when the caller never calls ``parse()``. """ - http_response: httpx.Response + http_response: _http_lib.Response class RawResponseStreamProxy(ObjectProxy): diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/package.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/package.py index 4d23be8ce..76e6b35ba 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/package.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/package.py @@ -2,6 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 -_instruments = ("openai >= 1.26.0",) +_instruments = ("openai >= 1.26.0, < 4",) _supports_metrics = True diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py index 14ceec1fb..76b3ae603 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/utils.py @@ -9,7 +9,6 @@ from urllib.parse import urlparse import openai -from httpx import URL from openai import NotGiven from opentelemetry.semconv._incubating.attributes import ( @@ -68,13 +67,13 @@ def get_server_address_and_port( base_url = getattr(base_client, "base_url", None) if not base_url: return None, None - address = None - port = None - if isinstance(base_url, URL): - address = base_url.host - port = base_url.port - elif isinstance(base_url, str): - url = urlparse(base_url) + + # Use getattr rather than isinstance(base_url, httpx.URL): openai v1/v2 + # uses httpx.URL while v3 uses httpx2.URL; both expose .host and .port. + address = getattr(base_url, "host", None) + port = getattr(base_url, "port", None) + if not address: + url = urlparse(str(base_url)) address = url.hostname port = url.port diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.latest.txt b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.latest.txt index 19cde5079..55fd124b0 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.latest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.latest.txt @@ -37,17 +37,16 @@ # This variant of the requirements aims to test the system using # the newest supported version of external dependencies. -openai==2.52.0 +openai==3.1.0 # older pydantic is required for PyPy < 3.11 pydantic==2.12.5 -httpx==0.28.1 +httpx2==2.7.0 # older jiter is required for PyPy < 3.11 jiter==0.11.1 Deprecated==1.3.1 importlib-metadata==9.0.0 packaging==26.2 wrapt==2.2.2 -opentelemetry-instrumentation-httpx==0.64b0 # test with the latest version of opentelemetry-api, sdk, semantic conventions, and instrumentation -e instrumentation/opentelemetry-instrumentation-genai-openai diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.openai-v2.txt b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.openai-v2.txt new file mode 100644 index 000000000..8f522bcba --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.openai-v2.txt @@ -0,0 +1,55 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# ******************************** +# WARNING: NOT HERMETIC !!!!!!!!!! +# ******************************** +# +# This "requirements.txt" is installed in conjunction +# with multiple other dependencies in the top-level "tox.ini" +# file. In particular, please see: +# +# openai-v2: {[testenv]test_deps} +# openai-v2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.openai-v2.txt +# +# This provides additional dependencies, namely: +# +# opentelemetry-api +# opentelemetry-sdk +# opentelemetry-semantic-conventions +# opentelemetry-instrumentation +# +# ... with a "dev" version based on the latest distribution. + + +# This variant of the requirements aims to test the system using +# the latest openai v2.x release (httpx-based, as opposed to the httpx2-based v3.x). +# It also includes opentelemetry-instrumentation-httpx to exercise the httpx interop tests. + +openai==2.54.0 +# older pydantic is required for PyPy < 3.11 +pydantic==2.12.5 +httpx==0.28.1 +opentelemetry-instrumentation-httpx==0.64b0 +# older jiter is required for PyPy < 3.11 +jiter==0.11.1 +Deprecated==1.3.1 +importlib-metadata==9.0.0 +packaging==26.2 +wrapt==2.2.2 +# test with the latest version of opentelemetry-api, sdk, semantic conventions, and instrumentation + +-e instrumentation/opentelemetry-instrumentation-genai-openai +-e util/opentelemetry-util-genai diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_embedding_invocation_unit.py b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_embedding_invocation_unit.py index aeb01cada..2dd98a344 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_embedding_invocation_unit.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_embedding_invocation_unit.py @@ -15,7 +15,11 @@ from typing import Any import pytest -from httpx import URL + +try: + from httpx import URL +except ImportError: + from httpx2 import URL from opentelemetry.instrumentation.genai.openai.patch import ( _create_embedding_invocation as create_embedding_invocation, @@ -98,6 +102,18 @@ def test_server_address_and_port_from_client(handler, span_exporter): assert span.attributes[ServerAttributes.SERVER_PORT] == 8080 +def test_server_address_and_port_from_non_httpx_url(handler, span_exporter): + """Works with any URL object exposing .host/.port (e.g. httpx2.URL).""" + fake_url = SimpleNamespace(host="openrouter.ai", port=None) + client = _make_client(fake_url) + invocation = create_embedding_invocation(handler, {"model": "m"}, client) + invocation.stop() + + span = span_exporter.get_finished_spans()[0] + assert span.attributes[ServerAttributes.SERVER_ADDRESS] == "openrouter.ai" + assert ServerAttributes.SERVER_PORT not in span.attributes + + # ─── create_embedding_invocation: dimensions / encoding_format ────────────── diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_raw_response_httpx_interop.py b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_raw_response_httpx_interop.py index bfa0c61ba..b0a391a19 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_raw_response_httpx_interop.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_raw_response_httpx_interop.py @@ -18,11 +18,12 @@ import json -import httpx import pytest from openai import AsyncOpenAI, OpenAI -# Only pinned in requirements.latest.txt; oldest envs skip this module. +# Only pinned in requirements.openai-v2.txt; other envs (openai v3+ uses +# httpx2, oldest has no httpx instrumentation) skip this module. +httpx = pytest.importorskip("httpx") HTTPXClientInstrumentor = pytest.importorskip( "opentelemetry.instrumentation.httpx" ).HTTPXClientInstrumentor diff --git a/tox.ini b/tox.ini index 779dcddd8..2586c80fb 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,7 @@ envlist = ; oldest deps run only on the minimum supported Python: the lowest declared dependency versions ; predate newer interpreters and ship no wheels for them. latest covers the full interpreter range. py3{10,11,12,13,14}-test-instrumentation-genai-openai-latest + py3{10,11,12,13,14}-test-instrumentation-genai-openai-v2 py310-test-instrumentation-genai-openai-oldest pypy3-test-instrumentation-genai-openai-latest py314-test-instrumentation-genai-openai-conformance @@ -123,6 +124,9 @@ deps = openai-latest: {[testenv]test_deps} openai-latest: {[testenv]pytest_deps} openai-latest: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.latest.txt + openai-v2: {[testenv]test_deps} + openai-v2: {[testenv]pytest_deps} + openai-v2: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.openai-v2.txt openai-conformance: {[testenv]pytest_deps} openai-conformance: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-openai/tests/requirements.latest.txt @@ -245,7 +249,7 @@ commands_pre = coverage: python {toxinidir}/scripts/eachdist.py install --editable commands = - test-instrumentation-genai-openai-{oldest,latest}: pytest --ignore={toxinidir}/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_conformance.py {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-openai/tests {posargs} + test-instrumentation-genai-openai-{oldest,latest,v2}: pytest --ignore={toxinidir}/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_conformance.py {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-openai/tests {posargs} test-instrumentation-genai-openai-conformance: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_conformance.py --vcr-record=none {posargs} lint-instrumentation-genai-openai: sh -c "cd instrumentation && ruff check opentelemetry-instrumentation-genai-openai" test-instrumentation-genai-openai_agents-{oldest,latest}: pytest --ignore={toxinidir}/instrumentation/opentelemetry-instrumentation-genai-openai-agents/tests/test_conformance.py {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-openai-agents/tests {posargs} diff --git a/uv.lock b/uv.lock index 95d954521..b21248929 100644 --- a/uv.lock +++ b/uv.lock @@ -5,12 +5,12 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version < '3.11' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform == 'win32'", "python_full_version < '3.11' and sys_platform != 'win32'", ] @@ -2820,10 +2820,10 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } @@ -3013,8 +3013,8 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } @@ -3123,10 +3123,10 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform != 'win32'", ] dependencies = [ @@ -3450,7 +3450,7 @@ instruments = [ [package.metadata] requires-dist = [ - { name = "openai", marker = "extra == 'instruments'", specifier = ">=1.26.0" }, + { name = "openai", marker = "extra == 'instruments'", specifier = ">=1.26.0,<4" }, { name = "opentelemetry-api", specifier = "~=1.43" }, { name = "opentelemetry-instrumentation", specifier = ">=0.64b0,<1" }, { name = "opentelemetry-semantic-conventions", specifier = ">=0.64b0,<1" }, @@ -4802,10 +4802,10 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform != 'win32'", ] dependencies = [ @@ -5320,10 +5320,10 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } @@ -5785,10 +5785,10 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform != 'win32'", ] dependencies = [ @@ -5833,10 +5833,10 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform != 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform != 'win32'", ] dependencies = [