diff --git a/dev-requirements.txt b/dev-requirements.txt
index 2668677cc5..7b0a1628ce 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -6,6 +6,7 @@ sphinx-rtd-theme==2.0.0rc4
sphinx-autodoc-typehints==1.25.2
pytest==7.4.4
pytest-cov==4.1.0
+pytest-vcr==1.0.2
readme-renderer==42.0
bleach==4.1.0 # transient dependency for readme-renderer
markupsafe>=2.0.1
diff --git a/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/CHANGELOG.md b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/CHANGELOG.md
new file mode 100644
index 0000000000..3bd68460ea
--- /dev/null
+++ b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## Unreleased
+
+- Initial Cohere instrumentation
+ ([#3081](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3081))
diff --git a/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/LICENSE b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/LICENSE
new file mode 100644
index 0000000000..261eeb9e9f
--- /dev/null
+++ b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/README.rst b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/README.rst
new file mode 100644
index 0000000000..f167d4e6c8
--- /dev/null
+++ b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/README.rst
@@ -0,0 +1,29 @@
+OpenTelemetry Cohere Instrumentation
+====================================
+
+|pypi|
+
+.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-cohere-v2.svg
+ :target: https://pypi.org/project/opentelemetry-instrumentation-cohere-v2/
+
+This library allows tracing LLM requests and logging of messages made by the
+`Cohere Python API library `_.
+
+
+Installation
+------------
+
+If your application is already instrumented with OpenTelemetry, add this
+package to your requirements.
+::
+
+ pip install opentelemetry-instrumentation-cohere-v2
+
+If you don't have an Cohere application, yet, try our `example `_
+which only needs a valid Cohere API key.
+
+References
+----------
+* `OpenTelemetry Cohere Instrumentation `_
+* `OpenTelemetry Project `_
+* `OpenTelemetry Python Examples `_
diff --git a/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/example/main.py b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/example/main.py
new file mode 100644
index 0000000000..78eed6d518
--- /dev/null
+++ b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/example/main.py
@@ -0,0 +1,25 @@
+import cohere
+
+from opentelemetry import trace
+from opentelemetry.instrumentation.cohere_v2 import CohereInstrumentor
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk.trace.export import (
+ BatchSpanProcessor,
+ ConsoleSpanExporter,
+)
+
+CohereInstrumentor().instrument()
+
+trace.set_tracer_provider(TracerProvider())
+trace.get_tracer_provider().add_span_processor(
+ BatchSpanProcessor(ConsoleSpanExporter())
+)
+tracer = trace.get_tracer(__name__)
+
+co = cohere.ClientV2()
+
+with tracer.start_as_current_span("foo"):
+ response = co.chat(
+ model="command-r-plus",
+ messages=[{"role": "user", "content": "Write a short poem on OpenTelemetry."}]
+ )
diff --git a/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/pyproject.toml b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/pyproject.toml
new file mode 100644
index 0000000000..9d0fed4428
--- /dev/null
+++ b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/pyproject.toml
@@ -0,0 +1,54 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "opentelemetry-instrumentation-cohere-v2"
+dynamic = ["version"]
+description = "OpenTelemetry Official Cohere instrumentation"
+readme = "README.rst"
+license = "Apache-2.0"
+requires-python = ">=3.8"
+authors = [
+ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" },
+]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+]
+dependencies = [
+ "opentelemetry-api ~= 1.28",
+ "opentelemetry-instrumentation ~= 0.49b0",
+ "opentelemetry-semantic-conventions ~= 0.49b0"
+]
+
+[project.optional-dependencies]
+instruments = [
+ "cohere >= 5.11.4",
+]
+
+[project.entry-points.opentelemetry_instrumentor]
+openai = "opentelemetry.instrumentation.cohere_v2:CohereInstrumentor"
+
+[project.urls]
+Homepage = "https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation-genai/opentelemetry-instrumentation-cohere-v2"
+
+[tool.hatch.version]
+path = "src/opentelemetry/instrumentation/cohere_v2/version.py"
+
+[tool.hatch.build.targets.sdist]
+include = [
+ "/src",
+ "/tests",
+]
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/opentelemetry"]
diff --git a/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/src/opentelemetry/instrumentation/cohere_v2/__init__.py b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/src/opentelemetry/instrumentation/cohere_v2/__init__.py
new file mode 100644
index 0000000000..fedcc04b89
--- /dev/null
+++ b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/src/opentelemetry/instrumentation/cohere_v2/__init__.py
@@ -0,0 +1,93 @@
+# 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.
+
+"""
+Cohere client instrumentation supporting `cohere`, it can be enabled by
+using ``CohereInstrumentor``.
+
+.. _openai: https://pypi.org/project/cohere/
+
+Usage
+-----
+
+.. code:: python
+
+ import cohere
+ from opentelemetry.instrumentation.cohere_v2 import CohereInstrumentor
+
+ CohereInstrumentor().instrument()
+
+ co = cohere.ClientV2('')
+
+ response = co.chat(
+ model="command-r-plus",
+ messages=[{"role": "user", "content": "Write a short poem on OpenTelemetry."}]
+ )
+
+API
+---
+"""
+
+from typing import Collection
+
+from wrapt import wrap_function_wrapper
+
+from opentelemetry._events import get_event_logger
+from opentelemetry.instrumentation.genai_utils import is_content_enabled
+from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.instrumentation.cohere_v2.package import _instruments
+from opentelemetry.instrumentation.cohere_v2.version import __version__
+from opentelemetry.instrumentation.utils import unwrap
+from opentelemetry.semconv.schemas import Schemas
+from opentelemetry.trace import get_tracer
+
+from .patch import client_chat
+
+
+class CohereInstrumentor(BaseInstrumentor):
+ """An instrumentor for Cohere's client library."""
+
+ def instrumentation_dependencies(self) -> Collection[str]:
+ return _instruments
+
+ def _instrument(self, **kwargs):
+ """Enable Cohere instrumentation."""
+ tracer_provider = kwargs.get("tracer_provider")
+ tracer = get_tracer(
+ __name__,
+ __version__,
+ tracer_provider,
+ schema_url=Schemas.V1_28_0.value,
+ )
+ event_logger_provider = kwargs.get("event_logger_provider")
+ event_logger = get_event_logger(
+ __name__,
+ __version__,
+ schema_url=Schemas.V1_28_0.value,
+ event_logger_provider=event_logger_provider,
+ )
+
+ wrap_function_wrapper(
+ module="cohere.client_v2",
+ name="ClientV2.chat",
+ wrapper=client_chat(
+ tracer, event_logger, is_content_enabled()
+ ),
+ )
+
+
+ def _uninstrument(self, **kwargs):
+ import cohere # pylint: disable=import-outside-toplevel
+
+ unwrap("cohere.client_v2.ClientV2", "chat")
diff --git a/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/src/opentelemetry/instrumentation/cohere_v2/package.py b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/src/opentelemetry/instrumentation/cohere_v2/package.py
new file mode 100644
index 0000000000..b7be16a26f
--- /dev/null
+++ b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/src/opentelemetry/instrumentation/cohere_v2/package.py
@@ -0,0 +1,16 @@
+# 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.
+
+
+_instruments = ("cohere >= 5.11.4",)
diff --git a/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/src/opentelemetry/instrumentation/cohere_v2/patch.py b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/src/opentelemetry/instrumentation/cohere_v2/patch.py
new file mode 100644
index 0000000000..e117899d04
--- /dev/null
+++ b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/src/opentelemetry/instrumentation/cohere_v2/patch.py
@@ -0,0 +1,66 @@
+# 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.
+
+from opentelemetry._events import EventLogger
+from opentelemetry.trace import SpanKind, Tracer
+from opentelemetry.instrumentation.genai_utils import (
+ get_span_name,
+ handle_span_exception
+)
+from opentelemetry.instrumentation.utils import is_instrumentation_enabled
+from .utils import (
+ get_genai_request_attributes,
+ message_to_event,
+ set_response_attributes,
+ set_server_address_and_port,
+)
+
+
+def client_chat(
+ tracer: Tracer, event_logger: EventLogger, capture_content: bool
+):
+ """Wrap the `chat` method of the `ClientV2` class to trace it."""
+
+ def traced_method(wrapped, instance, args, kwargs):
+ if not is_instrumentation_enabled():
+ return wrapped(*args, **kwargs)
+
+ span_attributes = {**get_genai_request_attributes(kwargs, instance)}
+ set_server_address_and_port(instance, span_attributes)
+ span_name = get_span_name(span_attributes)
+
+ with tracer.start_as_current_span(
+ name=span_name,
+ kind=SpanKind.CLIENT,
+ attributes=span_attributes,
+ ) as span:
+ if span.is_recording():
+ for message in kwargs.get("messages", []):
+ event_logger.emit(
+ message_to_event(message, capture_content)
+ )
+
+ try:
+ result = wrapped(*args, **kwargs)
+ if span.is_recording():
+ set_response_attributes(
+ span, result, event_logger, capture_content
+ )
+ return result
+
+ except Exception as error:
+ handle_span_exception(span, error)
+ raise
+
+ return traced_method
diff --git a/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/src/opentelemetry/instrumentation/cohere_v2/utils.py b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/src/opentelemetry/instrumentation/cohere_v2/utils.py
new file mode 100644
index 0000000000..26e46005d5
--- /dev/null
+++ b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/src/opentelemetry/instrumentation/cohere_v2/utils.py
@@ -0,0 +1,199 @@
+# 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.
+import cohere
+
+from typing import List, Optional, Union
+from urllib.parse import urlparse
+
+from opentelemetry._events import Event, EventLogger
+from opentelemetry.semconv._incubating.attributes import (
+ gen_ai_attributes as GenAIAttributes,
+)
+from opentelemetry.semconv._incubating.attributes import (
+ server_attributes as ServerAttributes,
+)
+from opentelemetry.trace import Span
+
+
+def extract_tool_calls(item: Union[cohere.types.ChatMessageV2, cohere.AssistantMessageResponse], capture_content: bool):
+ tool_calls: Optional[List[cohere.ToolCallV2]] = get_property_value(item, "tool_calls")
+ if tool_calls is None:
+ return None
+
+ calls = []
+ for tool_call in tool_calls:
+ tool_call_dict = {}
+ call_id = get_property_value(tool_call, "id")
+ if call_id:
+ tool_call_dict["id"] = call_id
+
+ tool_type = get_property_value(tool_call, "type")
+ if tool_type:
+ tool_call_dict["type"] = tool_type
+
+ func = get_property_value(tool_call, "function")
+ if func:
+ tool_call_dict["function"] = {}
+
+ name = get_property_value(func, "name")
+ if name:
+ tool_call_dict["function"]["name"] = name
+
+ arguments = get_property_value(func, "arguments")
+ if capture_content and arguments:
+ if isinstance(arguments, str):
+ arguments = arguments.replace("\n", "")
+ tool_call_dict["function"]["arguments"] = arguments
+
+ calls.append(tool_call_dict)
+ return calls
+
+
+def get_property_value(obj, property_name):
+ if isinstance(obj, dict):
+ return obj.get(property_name, None)
+
+ return getattr(obj, property_name, None)
+
+
+def set_server_address_and_port(client_instance: cohere.client_v2.V2Client, attributes):
+ base_client = getattr(client_instance, "_client_wrapper", None)
+ base_url = getattr(base_client, "_base_url", None)
+ if not base_url:
+ return
+
+ port = -1
+ url = urlparse(base_url)
+ attributes[ServerAttributes.SERVER_ADDRESS] = url.hostname
+ port = url.port
+
+ if port and port != 443 and port > 0:
+ attributes[ServerAttributes.SERVER_PORT] = port
+
+
+def get_genai_request_attributes(
+ kwargs,
+ client_instance: cohere.client_v2.V2Client,
+ operation_name=GenAIAttributes.GenAiOperationNameValues.CHAT.value,
+):
+ attributes = {
+ GenAIAttributes.GEN_AI_OPERATION_NAME: operation_name,
+ GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.COHERE.value,
+ GenAIAttributes.GEN_AI_REQUEST_MODEL: kwargs.get("model"),
+ GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS: kwargs.get("max_tokens"),
+ GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES: kwargs.get(
+ "stop_sequences"
+ ),
+ GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE: kwargs.get("temperature"),
+ # TODO: Add to sem conv
+ "gen_ai.cohere.request.seed": kwargs.get("seed"),
+ GenAIAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY: kwargs.get(
+ "presence_penalty"
+ ),
+ GenAIAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY: kwargs.get(
+ "frequency_penalty"
+ ),
+ GenAIAttributes.GEN_AI_REQUEST_TOP_K: kwargs.get("k"),
+ GenAIAttributes.GEN_AI_REQUEST_TOP_P: kwargs.get("p"),
+ }
+ response_format = kwargs.get("response_format")
+ if response_format:
+ # TODO: Add to sem conv
+ attributes["gen_ai.cohere.request.response_format"] = response_format.type
+
+ set_server_address_and_port(client_instance, attributes)
+
+ # filter out None values
+ return {k: v for k, v in attributes.items() if v is not None}
+
+
+def message_to_event(message: cohere.types.ChatMessageV2, capture_content: bool) -> Event:
+ attributes = {
+ GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.COHERE.value
+ }
+ role = get_property_value(message, "role")
+ content = get_property_value(message, "content")
+
+ body = {}
+ if capture_content and content:
+ body["content"] = content
+ if role == "assistant":
+ tool_calls = extract_tool_calls(message, capture_content)
+ if tool_calls:
+ body = {"tool_calls": tool_calls}
+ elif role == "tool":
+ tool_call_id = get_property_value(message, "tool_call_id")
+ if tool_call_id:
+ body["id"] = tool_call_id
+
+ return Event(
+ name=f"gen_ai.{role}.message",
+ attributes=attributes,
+ body=body if body else None,
+ )
+
+
+def set_response_attributes(
+ span: Span, result: cohere.ChatResponse, event_logger: EventLogger, capture_content: bool
+):
+ event_logger.emit(_response_to_event(result, capture_content))
+
+ span.set_attribute(
+ GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS,
+ [result.finish_reason],
+ )
+
+ if getattr(result, "id", None):
+ span.set_attribute(GenAIAttributes.GEN_AI_RESPONSE_ID, result.id)
+
+ # Get the usage
+ if getattr(result, "usage", None):
+ if getattr(result.usage, "tokens"):
+ span.set_attribute(
+ GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS,
+ result.usage.tokens.input_tokens,
+ )
+ span.set_attribute(
+ GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS,
+ result.usage.tokens.output_tokens,
+ )
+
+
+def _response_to_event(response: cohere.ChatResponse, capture_content):
+ attributes = {
+ GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.COHERE.value
+ }
+
+ body = {
+ "id": response.id,
+ "finish_reason": response.finish_reason or "error",
+ }
+
+ if response.message:
+ message = {}
+ if response.message.role and response.message.role != "assistant":
+ message["role"] = response.message.role
+ tool_calls = extract_tool_calls(response.message, capture_content)
+ if tool_calls:
+ message["tool_calls"] = tool_calls
+ content: List[cohere.AssistantMessageResponseContentItem] = get_property_value(response.message, "content")
+ if capture_content and content:
+ message["content"] = content
+ body["message"] = message
+
+ return Event(
+ name="gen_ai.choice",
+ attributes=attributes,
+ body=body,
+ )
\ No newline at end of file
diff --git a/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/src/opentelemetry/instrumentation/cohere_v2/version.py b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/src/opentelemetry/instrumentation/cohere_v2/version.py
new file mode 100644
index 0000000000..61ae9b7c25
--- /dev/null
+++ b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/src/opentelemetry/instrumentation/cohere_v2/version.py
@@ -0,0 +1,15 @@
+# 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.
+
+__version__ = "2.0b0.dev"
diff --git a/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/tests/__init__.py b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/tests/cassettes/test_chat_with_content.yaml b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/tests/cassettes/test_chat_with_content.yaml
new file mode 100644
index 0000000000..95650ec93f
--- /dev/null
+++ b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/tests/cassettes/test_chat_with_content.yaml
@@ -0,0 +1,108 @@
+interactions:
+- request:
+ body: |-
+ {
+ "model": "command-r-plus",
+ "messages": [
+ {
+ "role": "user",
+ "content": "Say this is a test"
+ }
+ ],
+ "stream": false
+ }
+ headers:
+ accept:
+ - '*/*'
+ accept-encoding:
+ - gzip, deflate
+ authorization:
+ - Bearer test_cohere_api_key
+ connection:
+ - keep-alive
+ content-length:
+ - '109'
+ content-type:
+ - application/json
+ host:
+ - api.cohere.com
+ user-agent:
+ - python-httpx/0.27.2
+ x-fern-language:
+ - Python
+ x-fern-sdk-name:
+ - cohere
+ x-fern-sdk-version:
+ - 5.11.4
+ method: POST
+ uri: https://api.cohere.com/v2/chat
+ response:
+ body:
+ string: |-
+ {
+ "id": "68d63b3a-360d-4460-9cb9-8413ef2adc52",
+ "message": {
+ "role": "assistant",
+ "content": [
+ {
+ "type": "text",
+ "text": "This is a test."
+ }
+ ]
+ },
+ "finish_reason": "COMPLETE",
+ "usage": {
+ "billed_units": {
+ "input_tokens": 5,
+ "output_tokens": 5
+ },
+ "tokens": {
+ "input_tokens": 198,
+ "output_tokens": 5
+ }
+ }
+ }
+ headers:
+ Alt-Svc:
+ - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
+ Content-Length:
+ - '266'
+ Set-Cookie: test_set_cookie
+ Via:
+ - 1.1 google
+ access-control-expose-headers:
+ - X-Debug-Trace-ID
+ cache-control:
+ - no-cache, no-store, no-transform, must-revalidate, private, max-age=0
+ content-type:
+ - application/json
+ date:
+ - Tue, 10 Dec 2024 16:04:21 GMT
+ expires:
+ - Thu, 01 Jan 1970 00:00:00 UTC
+ num_chars:
+ - '1188'
+ num_tokens:
+ - '10'
+ pragma:
+ - no-cache
+ server:
+ - envoy
+ vary:
+ - Origin
+ x-accel-expires:
+ - '0'
+ x-debug-trace-id:
+ - 90f329169476c8dd3d7bf0071236f7f8
+ x-endpoint-monthly-call-limit:
+ - '1000'
+ x-envoy-upstream-service-time:
+ - '188'
+ x-trial-endpoint-call-limit:
+ - '40'
+ x-trial-endpoint-call-remaining:
+ - '39'
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/tests/conftest.py b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/tests/conftest.py
new file mode 100644
index 0000000000..318d62548d
--- /dev/null
+++ b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/tests/conftest.py
@@ -0,0 +1,178 @@
+"""Unit tests configuration module."""
+
+import json
+import os
+
+import pytest
+import yaml
+from cohere import ClientV2
+
+from opentelemetry.instrumentation.cohere_v2 import CohereInstrumentor
+from opentelemetry.instrumentation.genai_utils import (
+ OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT,
+)
+from opentelemetry.sdk._events import EventLoggerProvider
+from opentelemetry.sdk._logs import LoggerProvider
+from opentelemetry.sdk._logs.export import (
+ InMemoryLogExporter,
+ SimpleLogRecordProcessor,
+)
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk.trace.export import SimpleSpanProcessor
+from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
+ InMemorySpanExporter,
+)
+
+
+@pytest.fixture(scope="function", name="span_exporter")
+def fixture_span_exporter():
+ exporter = InMemorySpanExporter()
+ yield exporter
+
+
+@pytest.fixture(scope="function", name="log_exporter")
+def fixture_log_exporter():
+ exporter = InMemoryLogExporter()
+ yield exporter
+
+
+@pytest.fixture(scope="function", name="tracer_provider")
+def fixture_tracer_provider(span_exporter):
+ provider = TracerProvider()
+ provider.add_span_processor(SimpleSpanProcessor(span_exporter))
+ return provider
+
+
+@pytest.fixture(scope="function", name="event_logger_provider")
+def fixture_event_logger_provider(log_exporter):
+ provider = LoggerProvider()
+ provider.add_log_record_processor(SimpleLogRecordProcessor(log_exporter))
+ event_logger_provider = EventLoggerProvider(provider)
+
+ return event_logger_provider
+
+
+@pytest.fixture(autouse=True)
+def environment():
+ if not os.getenv("CO_API_KEY"):
+ os.environ["CO_API_KEY"] = "test_cohere_api_key"
+
+
+@pytest.fixture
+def cohere_client():
+ return ClientV2()
+
+
+@pytest.fixture(scope="module")
+def vcr_config():
+ return {
+ "filter_headers": [
+ ("cookie", "test_cookie"),
+ ("authorization", "Bearer test_cohere_api_key"),
+ ],
+ "decode_compressed_response": True,
+ "before_record_response": scrub_response_headers,
+ }
+
+
+@pytest.fixture(scope="function")
+def instrument_no_content(tracer_provider, event_logger_provider):
+ instrumentor = CohereInstrumentor()
+ instrumentor.instrument(
+ tracer_provider=tracer_provider,
+ event_logger_provider=event_logger_provider,
+ )
+
+ yield instrumentor
+ instrumentor.uninstrument()
+
+
+@pytest.fixture(scope="function")
+def instrument_with_content(tracer_provider, event_logger_provider):
+ os.environ.update(
+ {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "True"}
+ )
+ instrumentor = CohereInstrumentor()
+ instrumentor.instrument(
+ tracer_provider=tracer_provider,
+ event_logger_provider=event_logger_provider,
+ )
+
+ yield instrumentor
+ os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None)
+ instrumentor.uninstrument()
+
+
+class LiteralBlockScalar(str):
+ """Formats the string as a literal block scalar, preserving whitespace and
+ without interpreting escape characters"""
+
+
+def literal_block_scalar_presenter(dumper, data):
+ """Represents a scalar string as a literal block, via '|' syntax"""
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
+
+
+yaml.add_representer(LiteralBlockScalar, literal_block_scalar_presenter)
+
+
+def process_string_value(string_value):
+ """Pretty-prints JSON or returns long strings as a LiteralBlockScalar"""
+ try:
+ json_data = json.loads(string_value)
+ return LiteralBlockScalar(json.dumps(json_data, indent=2))
+ except (ValueError, TypeError):
+ if len(string_value) > 80:
+ return LiteralBlockScalar(string_value)
+ return string_value
+
+
+def convert_body_to_literal(data):
+ """Searches the data for body strings, attempting to pretty-print JSON"""
+ if isinstance(data, dict):
+ for key, value in data.items():
+ # Handle response body case (e.g., response.body.string)
+ if key == "body" and isinstance(value, dict) and "string" in value:
+ value["string"] = process_string_value(value["string"])
+
+ # Handle request body case (e.g., request.body)
+ elif key == "body" and isinstance(value, str):
+ data[key] = process_string_value(value)
+
+ else:
+ convert_body_to_literal(value)
+
+ elif isinstance(data, list):
+ for idx, choice in enumerate(data):
+ data[idx] = convert_body_to_literal(choice)
+
+ return data
+
+
+class PrettyPrintJSONBody:
+ """This makes request and response body recordings more readable."""
+
+ @staticmethod
+ def serialize(cassette_dict):
+ cassette_dict = convert_body_to_literal(cassette_dict)
+ return yaml.dump(
+ cassette_dict, default_flow_style=False, allow_unicode=True
+ )
+
+ @staticmethod
+ def deserialize(cassette_string):
+ return yaml.load(cassette_string, Loader=yaml.Loader)
+
+
+@pytest.fixture(scope="module", autouse=True)
+def fixture_vcr(vcr):
+ vcr.register_serializer("yaml", PrettyPrintJSONBody)
+ return vcr
+
+
+def scrub_response_headers(response):
+ """
+ This scrubs sensitive response headers. Note they are case-sensitive!
+ """
+ response["headers"]["Set-Cookie"] = "test_set_cookie"
+ return response
diff --git a/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/tests/test_client_chat.py b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/tests/test_client_chat.py
new file mode 100644
index 0000000000..4d2d719e75
--- /dev/null
+++ b/instrumentation-genai/opentelemetry-instrumentation-cohere-v2/tests/test_client_chat.py
@@ -0,0 +1,154 @@
+# 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.
+# pylint: disable=too-many-locals
+
+from typing import Optional
+
+import pytest
+from cohere import ChatResponse
+
+from opentelemetry.sdk.trace import ReadableSpan
+from opentelemetry.semconv._incubating.attributes import (
+ error_attributes as ErrorAttributes,
+)
+from opentelemetry.semconv._incubating.attributes import (
+ event_attributes as EventAttributes,
+)
+from opentelemetry.semconv._incubating.attributes import (
+ gen_ai_attributes as GenAIAttributes,
+)
+from opentelemetry.semconv._incubating.attributes import (
+ server_attributes as ServerAttributes,
+)
+
+
+@pytest.mark.vcr()
+def test_chat_with_content(
+ span_exporter, log_exporter, instrument_with_content, cohere_client
+):
+ llm_model_value = "command-r-plus"
+ messages_value = [{"role": "user", "content": "Say this is a test"}]
+
+ response = cohere_client.chat(
+ messages=messages_value, model=llm_model_value,
+ )
+
+ spans = span_exporter.get_finished_spans()
+ assert_chat_attributes(spans[0], llm_model_value, response)
+
+ logs = log_exporter.get_finished_logs()
+ assert len(logs) == 2
+
+ user_message = {"content": messages_value[0]["content"]}
+ assert_message_in_logs(
+ logs[0], "gen_ai.user.message", user_message, spans[0]
+ )
+
+ response_event = {
+ "id": response.id,
+ "finish_reason": "COMPLETE",
+ "message": {
+ "content": response.message.content,
+ },
+ }
+ assert_message_in_logs(logs[1], "gen_ai.choice", response_event, spans[0])
+
+
+def assert_message_in_logs(log, event_name, expected_content, parent_span):
+ assert log.log_record.attributes[EventAttributes.EVENT_NAME] == event_name
+ assert (
+ log.log_record.attributes[GenAIAttributes.GEN_AI_SYSTEM]
+ == GenAIAttributes.GenAiSystemValues.COHERE.value
+ )
+
+ if not expected_content:
+ assert not log.log_record.body
+ else:
+ assert log.log_record.body
+ assert dict(log.log_record.body) == expected_content
+ assert_log_parent(log, parent_span)
+
+
+def assert_chat_attributes(
+ span: ReadableSpan,
+ request_model: str,
+ response: ChatResponse,
+ operation_name: str = "chat",
+ server_address: str = "api.cohere.com",
+):
+ return assert_all_attributes(
+ span,
+ request_model,
+ response.id,
+ response.usage.tokens.input_tokens,
+ response.usage.tokens.output_tokens,
+ operation_name,
+ server_address,
+ )
+
+
+def assert_all_attributes(
+ span: ReadableSpan,
+ request_model: str,
+ response_id: str = None,
+ input_tokens: Optional[int] = None,
+ output_tokens: Optional[int] = None,
+ operation_name: str = "chat",
+ server_address: str = "api.cohere.com",
+):
+ assert span.name == f"{operation_name} {request_model}"
+ assert (
+ operation_name
+ == span.attributes[GenAIAttributes.GEN_AI_OPERATION_NAME]
+ )
+ assert (
+ GenAIAttributes.GenAiSystemValues.COHERE.value
+ == span.attributes[GenAIAttributes.GEN_AI_SYSTEM]
+ )
+ assert (
+ request_model == span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL]
+ )
+
+ if response_id:
+ assert (
+ response_id == span.attributes[GenAIAttributes.GEN_AI_RESPONSE_ID]
+ )
+ else:
+ assert GenAIAttributes.GEN_AI_RESPONSE_ID not in span.attributes
+
+ if input_tokens:
+ assert (
+ input_tokens
+ == span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS]
+ )
+ else:
+ assert GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS not in span.attributes
+
+ if output_tokens:
+ assert (
+ output_tokens
+ == span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS]
+ )
+ else:
+ assert (
+ GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS not in span.attributes
+ )
+
+ assert server_address == span.attributes[ServerAttributes.SERVER_ADDRESS]
+
+
+def assert_log_parent(log, span):
+ assert log.log_record.trace_id == span.get_span_context().trace_id
+ assert log.log_record.span_id == span.get_span_context().span_id
+ assert log.log_record.trace_flags == span.get_span_context().trace_flags
diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/README.rst b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/README.rst
index d2cb0b5724..989ebf37e8 100644
--- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/README.rst
+++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/README.rst
@@ -76,7 +76,7 @@ To uninstrument clients, call the uninstrument method:
References
----------
-* `OpenTelemetry OpenAI Instrumentation `_
+* `OpenTelemetry OpenAI Instrumentation `_
* `OpenTelemetry Project `_
-* `OpenTelemetry Python Examples `_
+* `OpenTelemetry Python Examples `_
diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/__init__.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/__init__.py
index ee3bbfdb73..b5673d36d6 100644
--- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/__init__.py
+++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/__init__.py
@@ -47,7 +47,7 @@
from opentelemetry._events import get_event_logger
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.openai_v2.package import _instruments
-from opentelemetry.instrumentation.openai_v2.utils import is_content_enabled
+from opentelemetry.instrumentation.genai_utils import is_content_enabled
from opentelemetry.instrumentation.utils import unwrap
from opentelemetry.semconv.schemas import Schemas
from opentelemetry.trace import get_tracer
diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py
index cd284473ce..4f56bc6c8f 100644
--- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py
+++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py
@@ -18,6 +18,10 @@
from openai import Stream
from opentelemetry._events import Event, EventLogger
+from opentelemetry.instrumentation.genai_utils import (
+ get_span_name,
+ handle_span_exception,
+)
from opentelemetry.semconv._incubating.attributes import (
gen_ai_attributes as GenAIAttributes,
)
@@ -25,8 +29,7 @@
from .utils import (
choice_to_event,
- get_llm_request_attributes,
- handle_span_exception,
+ get_genai_request_attributes,
is_streaming,
message_to_event,
set_span_attribute,
@@ -39,9 +42,9 @@ def chat_completions_create(
"""Wrap the `create` method of the `ChatCompletion` class to trace it."""
def traced_method(wrapped, instance, args, kwargs):
- span_attributes = {**get_llm_request_attributes(kwargs, instance)}
+ span_attributes = {**get_genai_request_attributes(kwargs, instance)}
- span_name = f"{span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME]} {span_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL]}"
+ span_name = get_span_name(span_attributes)
with tracer.start_as_current_span(
name=span_name,
kind=SpanKind.CLIENT,
@@ -81,7 +84,7 @@ def async_chat_completions_create(
"""Wrap the `create` method of the `AsyncChatCompletion` class to trace it."""
async def traced_method(wrapped, instance, args, kwargs):
- span_attributes = {**get_llm_request_attributes(kwargs, instance)}
+ span_attributes = {**get_genai_request_attributes(kwargs, instance)}
span_name = f"{span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME]} {span_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL]}"
with tracer.start_as_current_span(
diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py
index f8a837259e..809e1fad6d 100644
--- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py
+++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py
@@ -26,22 +26,6 @@
from opentelemetry.semconv._incubating.attributes import (
server_attributes as ServerAttributes,
)
-from opentelemetry.semconv.attributes import (
- error_attributes as ErrorAttributes,
-)
-from opentelemetry.trace.status import Status, StatusCode
-
-OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = (
- "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
-)
-
-
-def is_content_enabled() -> bool:
- capture_content = environ.get(
- OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false"
- )
-
- return capture_content.lower() == "true"
def extract_tool_calls(item, capture_content):
@@ -183,7 +167,7 @@ def non_numerical_value_is_set(value: Optional[Union[bool, str]]):
return bool(value) and value != NOT_GIVEN
-def get_llm_request_attributes(
+def get_genai_request_attributes(
kwargs,
client_instance,
operation_name=GenAIAttributes.GenAiOperationNameValues.CHAT.value,
@@ -227,12 +211,3 @@ def get_llm_request_attributes(
# filter out None values
return {k: v for k, v in attributes.items() if v is not None}
-
-
-def handle_span_exception(span, error):
- span.set_status(Status(StatusCode.ERROR, str(error)))
- if span.is_recording():
- span.set_attribute(
- ErrorAttributes.ERROR_TYPE, type(error).__qualname__
- )
- span.end()
diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/conftest.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/conftest.py
index 18e6582dff..76e3db6d7c 100644
--- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/conftest.py
+++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/conftest.py
@@ -7,10 +7,10 @@
import yaml
from openai import AsyncOpenAI, OpenAI
-from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
-from opentelemetry.instrumentation.openai_v2.utils import (
+from opentelemetry.instrumentation.genai_utils import (
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT,
)
+from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
from opentelemetry.sdk._events import EventLoggerProvider
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import (
diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/genai_utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/genai_utils.py
new file mode 100644
index 0000000000..f9db12c4a7
--- /dev/null
+++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/genai_utils.py
@@ -0,0 +1,51 @@
+# 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.
+
+from os import environ
+
+from opentelemetry.semconv._incubating.attributes import (
+ gen_ai_attributes as GenAIAttributes,
+)
+from opentelemetry.semconv.attributes import (
+ error_attributes as ErrorAttributes,
+)
+from opentelemetry.trace.status import Status, StatusCode
+
+
+OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = (
+ "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
+)
+
+
+def is_content_enabled() -> bool:
+ capture_content = environ.get(
+ OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false"
+ )
+
+ return capture_content.lower() == "true"
+
+
+def get_span_name(span_attributes):
+ name = span_attributes.get(GenAIAttributes.GEN_AI_OPERATION_NAME, "")
+ model = span_attributes.get(GenAIAttributes.GEN_AI_REQUEST_MODEL, "")
+ return f"{name} {model}"
+
+
+def handle_span_exception(span, error):
+ span.set_status(Status(StatusCode.ERROR, str(error)))
+ if span.is_recording():
+ span.set_attribute(
+ ErrorAttributes.ERROR_TYPE, type(error).__qualname__
+ )
+ span.end()