diff --git a/.codespellrc b/.codespellrc
index e20054be5..f85d0dafd 100644
--- a/.codespellrc
+++ b/.codespellrc
@@ -3,5 +3,5 @@
[codespell]
check-hidden = true
# skipping auto generated folders
-skip = ./.git,./.tox,./.venv,./.mypy_cache,./docs/_build,./target,*/LICENSE,./venv,*/cassettes
+skip = ./.git,./.tox,./.venv,./test_env,./.mypy_cache,./docs/_build,./target,*/LICENSE,./venv,*/cassettes
ignore-words-list = ot
diff --git a/instrumentation/README.md b/instrumentation/README.md
index 228b4672a..dc6e53668 100644
--- a/instrumentation/README.md
+++ b/instrumentation/README.md
@@ -4,6 +4,7 @@
| [opentelemetry-instrumentation-genai-agno](./opentelemetry-instrumentation-genai-agno) | agno >= 2.0.0 | No | development
| [opentelemetry-instrumentation-genai-anthropic](./opentelemetry-instrumentation-genai-anthropic) | anthropic >= 0.16.0 | No | development
| [opentelemetry-instrumentation-genai-claude-agent-sdk](./opentelemetry-instrumentation-genai-claude-agent-sdk) | claude-agent-sdk >= 0.1.14 | No | development
+| [opentelemetry-instrumentation-genai-groq](./opentelemetry-instrumentation-genai-groq) | groq >= 0.6.0 | No | development
| [opentelemetry-instrumentation-genai-langchain](./opentelemetry-instrumentation-genai-langchain) | langchain >= 0.3.21 | No | development
| [opentelemetry-instrumentation-genai-llama-index](./opentelemetry-instrumentation-genai-llama-index) | llama-index-core >= 0.14.19 | No | development
| [opentelemetry-instrumentation-genai-openai](./opentelemetry-instrumentation-genai-openai) | openai >= 1.26.0 | Yes | development
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/.changelog/313.added b/instrumentation/opentelemetry-instrumentation-genai-groq/.changelog/313.added
new file mode 100644
index 000000000..8d669868e
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/.changelog/313.added
@@ -0,0 +1 @@
+Add `opentelemetry-instrumentation-genai-groq` package for instrumenting the official Groq Python client.
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/README.rst b/instrumentation/opentelemetry-instrumentation-genai-groq/README.rst
new file mode 100644
index 000000000..58b3d91e7
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/README.rst
@@ -0,0 +1,114 @@
+OpenTelemetry Groq Instrumentation
+==================================
+
+|pypi|
+
+.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-genai-groq.svg
+ :target: https://pypi.org/project/opentelemetry-instrumentation-genai-groq/
+
+This library allows tracing LLM requests and logging of messages made by the
+`Groq Python API library `_. It also captures
+the duration of the operations and the number of tokens used as metrics.
+
+Installation
+------------
+
+If your application is already instrumented with OpenTelemetry, add this
+package to your requirements.
+::
+
+ pip install opentelemetry-instrumentation-genai-groq
+
+If you don't have a Groq application, yet, try our `examples `_
+which only need a valid Groq API key.
+
+Check out `zero-code example `_ for a quick start.
+
+Usage
+-----
+
+This section describes how to set up Groq instrumentation if you're setting OpenTelemetry up manually.
+Check out the `manual example `_ for more details.
+
+Instrumenting all clients
+*************************
+
+When using the instrumentor, all clients will automatically trace Groq operations including chat completions.
+You can also optionally capture prompts and completions as log events.
+
+Make sure to configure OpenTelemetry tracing, logging, and events to capture all telemetry emitted by the instrumentation.
+
+.. code-block:: python
+
+ from opentelemetry.instrumentation.genai.groq import GroqInstrumentor
+ from groq import Groq
+
+ GroqInstrumentor().instrument()
+
+ client = Groq()
+ # Chat completion example
+ response = client.chat.completions.create(
+ model="llama3-8b-8192",
+ messages=[
+ {"role": "user", "content": "Write a short poem on open telemetry."},
+ ],
+ )
+
+Enabling message content
+*************************
+
+Message content such as the contents of the prompt, completion, function arguments and return values
+are not captured by default. To capture message content, set the environment variable
+``OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`` to one of the following values:
+
+- ``span_only`` - capture content on *span* attributes.
+- ``event_only`` - capture content on *event* attributes.
+- ``span_and_event`` - capture content on both *span* and *event* attributes.
+- ``no_content`` - do not capture content (the default).
+
+Uploading prompts and completions
+*********************************
+
+To enable the built-in upload hook, set:
+
+- ``OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload``
+- ``OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH`` to an ``fsspec``-compatible URI/path
+ (e.g. ``/path/to/prompts`` or ``gs://my_bucket``).
+
+Install the ``upload`` extra to pull in ``fsspec``::
+
+ pip install opentelemetry-util-genai[upload]
+
+See the `opentelemetry-util-genai
+`_
+for additional options.
+
+Enabling the latest experimental features
+***********************************************
+
+The latest experimental GenAI semantic conventions are used unconditionally; there is
+no environment variable to opt in or out.
+
+.. note:: Generative AI semantic conventions are still evolving. The latest experimental features may introduce breaking changes in future releases.
+
+Uninstrument
+************
+
+To uninstrument clients, call the uninstrument method:
+
+.. code-block:: python
+
+ from opentelemetry.instrumentation.genai.groq import GroqInstrumentor
+
+ GroqInstrumentor().instrument()
+ # ...
+
+ # Uninstrument all clients
+ GroqInstrumentor().uninstrument()
+
+References
+----------
+
+* `OpenTelemetry Project `_
+* `OpenTelemetry GenAI semantic conventions `_
+* `Groq SDK (Python) `_
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/examples/manual/.env b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/manual/.env
new file mode 100644
index 000000000..28cc4d645
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/manual/.env
@@ -0,0 +1,26 @@
+# TODO: Set your Groq API key here
+GROQ_API_KEY=sk-YOUR_API_KEY
+
+# Uncomment to use Ollama instead of Groq
+# GROQ_BASE_URL=http://localhost:11434/v1
+# GROQ_API_KEY=unused
+# CHAT_MODEL=qwen2.5:0.5b
+
+# Uncomment and change to your OTLP endpoint
+# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
+# OTEL_EXPORTER_OTLP_PROTOCOL=grpc
+
+OTEL_SERVICE_NAME=opentelemetry-python-groq
+
+# Remove to hide prompt and completion content
+# Possible values (case insensitive):
+# - `span_only` - record content on span attributes
+# - `event_only` - record content on event attributes
+# - `span_and_event` - record content on both span and event attributes
+# - everything else - don't record content on any signal
+OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=span_only
+
+# Uncomment to upload prompts and responses to an fsspec-compatible
+# destination instead of or in addition to recording them inline on spans/events.
+# OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload
+# OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH=/path/to/prompts
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/examples/manual/README.rst b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/manual/README.rst
new file mode 100644
index 000000000..60eb05c3a
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/manual/README.rst
@@ -0,0 +1,58 @@
+OpenTelemetry Groq Instrumentation Example
+============================================
+
+This is an example of how to instrument Groq calls when configuring OpenTelemetry SDK and Instrumentations manually.
+
+When `main.py `_ is run, it exports traces and logs to an OTLP
+compatible endpoint. Traces include details such as the model used and the
+duration of the chat request. Logs capture the chat request and the generated
+response, providing a comprehensive view of the performance and behavior of
+your Groq requests.
+
+Note: `.env <.env>`_ file configures additional environment variables:
+
+- ``OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=span_only`` configures Groq instrumentation to capture prompt and completion contents on *span* attributes.
+- ``OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK`` (commented out) - uncomment along with ``OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH`` to upload prompts and completions to an ``fsspec``-compatible destination instead of recording them inline. Also uncomment the ``opentelemetry-util-genai[upload]`` line in `requirements.txt `_ and reinstall.
+
+Setup
+-----
+
+Minimally, update the `.env <.env>`_ file with your ``GROQ_API_KEY``. An
+OTLP compatible endpoint should be listening for traces and logs on
+http://localhost:4317. If not, update ``OTEL_EXPORTER_OTLP_ENDPOINT`` as well.
+
+Next, set up a virtual environment like this:
+
+::
+
+ python3 -m venv .venv
+ source .venv/bin/activate
+ pip install "python-dotenv[cli]"
+ pip install -r requirements.txt
+
+Run
+---
+
+Run the example like this:
+
+::
+
+ dotenv run -- python main.py
+
+You should see a poem generated by Groq while traces and logs export to your
+configured observability tool.
+
+Custom completion hook
+----------------------
+
+`custom_hook.py `_ is a variant of ``main.py`` that passes a
+custom ``CompletionHook`` implementation programmatically via
+``GroqInstrumentor().instrument(completion_hook=...)``. The example hook
+prints prompts and completions to stdout; real hooks typically forward content
+to external storage and record reference URIs on the span/log_record.
+
+Run it the same way:
+
+::
+
+ dotenv run -- python custom_hook.py
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/examples/manual/custom_hook.py b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/manual/custom_hook.py
new file mode 100644
index 000000000..15d8c880f
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/manual/custom_hook.py
@@ -0,0 +1,98 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+# pylint: skip-file
+"""Same as main.py, but instruments Groq with a custom CompletionHook
+that prints prompts and completions to stdout.
+
+Run with: dotenv run -- python custom_hook.py
+"""
+
+import os
+
+from groq import Groq
+
+from opentelemetry import _logs, metrics, trace
+from opentelemetry.exporter.otlp.proto.grpc._log_exporter import (
+ OTLPLogExporter,
+)
+from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
+ OTLPMetricExporter,
+)
+from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
+ OTLPSpanExporter,
+)
+from opentelemetry.instrumentation.genai.groq import GroqInstrumentor
+from opentelemetry.sdk._logs import LoggerProvider
+from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
+from opentelemetry.sdk.metrics import MeterProvider
+from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk.trace.export import BatchSpanProcessor
+from opentelemetry.util.genai.completion_hook import CompletionHook
+from opentelemetry.util.genai.types import (
+ InputMessage,
+ MessagePart,
+ OutputMessage,
+ ToolDefinition,
+)
+
+
+class PrintCompletionHook(CompletionHook):
+ """Minimal CompletionHook that prints inputs/outputs to stdout.
+
+ Real hooks typically forward content to external storage (object store,
+ database, etc.) and record reference URIs on the span/log_record.
+ """
+
+ def on_completion(
+ self,
+ *,
+ inputs: list[InputMessage],
+ outputs: list[OutputMessage],
+ system_instruction: list[MessagePart],
+ tool_definitions: list[ToolDefinition] | None = None,
+ span=None,
+ log_record=None,
+ ) -> None:
+ print(f"[hook] inputs: {inputs}")
+ print(f"[hook] outputs: {outputs}")
+
+
+trace.set_tracer_provider(TracerProvider())
+trace.get_tracer_provider().add_span_processor(
+ BatchSpanProcessor(OTLPSpanExporter())
+)
+
+_logs.set_logger_provider(LoggerProvider())
+_logs.get_logger_provider().add_log_record_processor(
+ BatchLogRecordProcessor(OTLPLogExporter())
+)
+
+metrics.set_meter_provider(
+ MeterProvider(
+ metric_readers=[
+ PeriodicExportingMetricReader(OTLPMetricExporter()),
+ ]
+ )
+)
+
+GroqInstrumentor().instrument(completion_hook=PrintCompletionHook())
+
+
+def main():
+ client = Groq()
+ chat_completion = client.chat.completions.create(
+ model=os.getenv("CHAT_MODEL", "llama3-8b-8192"),
+ messages=[
+ {
+ "role": "user",
+ "content": "Write a short poem on OpenTelemetry.",
+ },
+ ],
+ )
+ print(chat_completion.choices[0].message.content)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/examples/manual/main.py b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/manual/main.py
new file mode 100644
index 000000000..69f5485c0
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/manual/main.py
@@ -0,0 +1,70 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+# pylint: skip-file
+import os
+
+from groq import Groq
+
+# NOTE: OpenTelemetry Python Logs API is in beta
+from opentelemetry import _logs, metrics, trace
+from opentelemetry.exporter.otlp.proto.grpc._log_exporter import (
+ OTLPLogExporter,
+)
+from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
+ OTLPMetricExporter,
+)
+from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
+ OTLPSpanExporter,
+)
+from opentelemetry.instrumentation.genai.groq import GroqInstrumentor
+from opentelemetry.sdk._logs import LoggerProvider
+from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
+from opentelemetry.sdk.metrics import MeterProvider
+from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk.trace.export import BatchSpanProcessor
+
+# configure tracing
+trace.set_tracer_provider(TracerProvider())
+trace.get_tracer_provider().add_span_processor(
+ BatchSpanProcessor(OTLPSpanExporter())
+)
+
+# configure logging
+_logs.set_logger_provider(LoggerProvider())
+_logs.get_logger_provider().add_log_record_processor(
+ BatchLogRecordProcessor(OTLPLogExporter())
+)
+
+# configure metrics
+metrics.set_meter_provider(
+ MeterProvider(
+ metric_readers=[
+ PeriodicExportingMetricReader(
+ OTLPMetricExporter(),
+ ),
+ ]
+ )
+)
+
+# instrument Groq
+GroqInstrumentor().instrument()
+
+
+def main():
+ client = Groq()
+ chat_completion = client.chat.completions.create(
+ model=os.getenv("CHAT_MODEL", "llama3-8b-8192"),
+ messages=[
+ {
+ "role": "user",
+ "content": "Write a short poem on OpenTelemetry.",
+ },
+ ],
+ )
+ print(chat_completion.choices[0].message.content)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/examples/manual/requirements.txt b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/manual/requirements.txt
new file mode 100644
index 000000000..7ab0859aa
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/manual/requirements.txt
@@ -0,0 +1,9 @@
+groq~=0.13.0
+
+opentelemetry-sdk~=1.43.0
+opentelemetry-exporter-otlp-proto-grpc~=1.43.0
+opentelemetry-instrumentation-genai-groq~=1.0b0
+
+# Uncomment to enable the upload completion hook (pulls in fsspec).
+# Required when OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload.
+# opentelemetry-util-genai[upload]
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/examples/zero-code/.env b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/zero-code/.env
new file mode 100644
index 000000000..ff5e01960
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/zero-code/.env
@@ -0,0 +1,29 @@
+# TODO: Set your Groq API key here
+GROQ_API_KEY=sk-YOUR_API_KEY
+
+# Uncomment to use Ollama instead of Groq
+# GROQ_BASE_URL=http://localhost:11434/v1
+# GROQ_API_KEY=unused
+# CHAT_MODEL=qwen2.5:0.5b
+
+# Uncomment and change to your OTLP endpoint
+# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
+# OTEL_EXPORTER_OTLP_PROTOCOL=grpc
+
+OTEL_SERVICE_NAME=opentelemetry-python-groq
+
+# Uncomment if your OTLP endpoint doesn't support logs
+# OTEL_LOGS_EXPORTER=console
+
+# Remove to hide prompt and completion content
+# Possible values (case insensitive):
+# - `span_only` - record content on span attributes
+# - `event_only` - record content on event attributes
+# - `span_and_event` - record content on both span and event attributes
+# - everything else - don't record content on any signal
+OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=span_only
+
+# Uncomment to upload prompts and responses to an fsspec-compatible
+# destination instead of or in addition to recording them inline on spans/events.
+# OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload
+# OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH=/path/to/prompts
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/examples/zero-code/README.rst b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/zero-code/README.rst
new file mode 100644
index 000000000..4098baa65
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/zero-code/README.rst
@@ -0,0 +1,45 @@
+OpenTelemetry Groq Zero-Code Instrumentation Example
+======================================================
+
+This is an example of how to instrument Groq calls with zero code changes,
+using `opentelemetry-instrument`.
+
+When `main.py `_ is run, it exports traces and logs to an OTLP
+compatible endpoint. Traces include details such as the model used and the
+duration of the chat request. Logs capture the chat request and the generated
+response, providing a comprehensive view of the performance and behavior of
+your Groq requests.
+
+Note: `.env <.env>`_ file configures additional environment variables:
+
+- ``OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=span_only`` configures Groq instrumentation to capture prompt and completion contents on *span* attributes.
+- ``OTEL_LOGS_EXPORTER=otlp`` to specify exporter type.
+- ``OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK`` (commented out) - uncomment along with ``OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH`` to upload prompts and completions to an ``fsspec``-compatible destination instead of recording them inline. Also uncomment the ``opentelemetry-util-genai[upload]`` line in `requirements.txt `_ and reinstall.
+
+Setup
+-----
+
+Minimally, update the `.env <.env>`_ file with your ``GROQ_API_KEY``. An
+OTLP compatible endpoint should be listening for traces and logs on
+http://localhost:4317. If not, update ``OTEL_EXPORTER_OTLP_ENDPOINT`` as well.
+
+Next, set up a virtual environment like this:
+
+::
+
+ python3 -m venv .venv
+ source .venv/bin/activate
+ pip install "python-dotenv[cli]"
+ pip install -r requirements.txt
+
+Run
+---
+
+Run the example like this:
+
+::
+
+ dotenv run -- opentelemetry-instrument python main.py
+
+You should see a poem generated by Groq while traces and logs export to your
+configured observability tool.
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/examples/zero-code/main.py b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/zero-code/main.py
new file mode 100644
index 000000000..c8830e7be
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/zero-code/main.py
@@ -0,0 +1,24 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+import os
+
+from groq import Groq
+
+
+def main():
+ client = Groq()
+ chat_completion = client.chat.completions.create(
+ model=os.getenv("CHAT_MODEL", "llama-3.1-8b-instant"),
+ messages=[
+ {
+ "role": "user",
+ "content": "Write a short poem on OpenTelemetry.",
+ },
+ ],
+ )
+ print(chat_completion.choices[0].message.content)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/examples/zero-code/requirements.txt b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/zero-code/requirements.txt
new file mode 100644
index 000000000..6fd5824d6
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/examples/zero-code/requirements.txt
@@ -0,0 +1,10 @@
+groq~=0.13.0
+
+opentelemetry-sdk~=1.43.0
+opentelemetry-exporter-otlp-proto-grpc~=1.43.0
+opentelemetry-distro~=0.64b0
+opentelemetry-instrumentation-genai-groq~=1.0b0
+
+# Uncomment to enable the upload completion hook (pulls in fsspec).
+# Required when OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload.
+# opentelemetry-util-genai[upload]
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/pyproject.toml b/instrumentation/opentelemetry-instrumentation-genai-groq/pyproject.toml
new file mode 100644
index 000000000..4e05e2de6
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/pyproject.toml
@@ -0,0 +1,85 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "opentelemetry-instrumentation-genai-groq"
+dynamic = ["version"]
+description = "OpenTelemetry Official Groq instrumentation"
+readme = "README.rst"
+license = "Apache-2.0"
+requires-python = ">=3.10"
+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.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
+]
+dependencies = [
+ "opentelemetry-api ~= 1.43",
+ "opentelemetry-instrumentation >= 0.64b0, <1",
+ "opentelemetry-semantic-conventions >= 0.64b0, <1",
+ "opentelemetry-util-genai >= 1.0b0, <2",
+]
+
+[project.optional-dependencies]
+instruments = ["groq >= 0.6.0"]
+
+[project.entry-points.opentelemetry_instrumentor]
+groq = "opentelemetry.instrumentation.genai.groq:GroqInstrumentor"
+
+[project.urls]
+Homepage = "https://github.com/open-telemetry/opentelemetry-python-genai/tree/main/instrumentation/opentelemetry-instrumentation-genai-groq"
+Repository = "https://github.com/open-telemetry/opentelemetry-python-genai"
+
+[tool.hatch.version]
+path = "src/opentelemetry/instrumentation/genai/groq/version.py"
+
+[tool.hatch.build.targets.sdist]
+include = ["/src", "/tests"]
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/opentelemetry"]
+
+[tool.towncrier]
+directory = ".changelog"
+filename = "CHANGELOG.md"
+start_string = "\n"
+template = "../../scripts/changelog_template.j2"
+issue_format = "[#{issue}](https://github.com/open-telemetry/opentelemetry-python-genai/pull/{issue})"
+wrap = true
+issue_pattern = "^(\\d+)"
+
+[[tool.towncrier.type]]
+directory = "added"
+name = "Added"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "changed"
+name = "Changed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "deprecated"
+name = "Deprecated"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "removed"
+name = "Removed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "fixed"
+name = "Fixed"
+showcontent = true
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/__init__.py b/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/__init__.py
new file mode 100644
index 000000000..999da7183
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/__init__.py
@@ -0,0 +1,146 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Groq client instrumentation supporting `groq`_, it can be enabled by
+using ``GroqInstrumentor``.
+
+.. _groq: https://pypi.org/project/groq/
+
+Usage
+-----
+
+.. code:: python
+
+ from groq import Groq
+ from opentelemetry.instrumentation.genai.groq import GroqInstrumentor
+
+ GroqInstrumentor().instrument()
+
+ client = Groq()
+ response = client.chat.completions.create(
+ model="llama3-8b-8192",
+ messages=[
+ {"role": "user", "content": "Write a short poem on open telemetry."},
+ ],
+ )
+
+Configuration
+-------------
+
+This instrumentation emits telemetry using the latest GenAI semantic
+conventions and does not capture prompt or completion content by default.
+Behavior is controlled via environment variables:
+
+- ``OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`` - enable capture of
+ prompts, completions, tool arguments, and return values. Supported values
+ are ``span_only``, ``event_only``, and ``span_and_event``.
+- ``OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK=upload`` together with
+ ``OTEL_INSTRUMENTATION_GENAI_UPLOAD_BASE_PATH=`` - upload
+ prompts and completions to an ``fsspec``-compatible destination
+ (local filesystem, ``gs://``, ``s3://``, etc.) and record reference URIs as
+ ``gen_ai.input.messages.ref`` / ``gen_ai.output.messages.ref`` attributes.
+ Inline content is not captured unless
+ ``OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`` is also set.
+
+See the `opentelemetry-util-genai README
+`_
+for the full list of GenAI configuration variables.
+
+A custom ``CompletionHook`` implementation can also be passed programmatically::
+
+ GroqInstrumentor().instrument(completion_hook=my_hook)
+
+When provided, this takes precedence over the hook resolved from
+``OTEL_INSTRUMENTATION_GENAI_COMPLETION_HOOK``.
+
+API
+---
+"""
+
+from typing import Collection
+
+from wrapt import wrap_function_wrapper
+
+from opentelemetry.instrumentation.genai.groq.package import _instruments
+from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.instrumentation.utils import unwrap
+from opentelemetry.util.genai.completion_hook import load_completion_hook
+from opentelemetry.util.genai.handler import (
+ TelemetryHandler,
+)
+
+from .patch import (
+ async_chat_completions_create_v_new,
+ chat_completions_create_v_new,
+)
+
+
+def _is_parse_supported():
+ """Check if the parse() method is available on the Completions class."""
+ try:
+ from groq.resources.chat.completions import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
+ Completions,
+ )
+
+ return hasattr(Completions, "parse")
+ except ImportError:
+ return False
+
+
+class GroqInstrumentor(BaseInstrumentor):
+ def __init__(self):
+ self._parse_supported = False
+
+ def instrumentation_dependencies(self) -> Collection[str]:
+ return _instruments
+
+ def _instrument(self, **kwargs):
+ """Enable Groq instrumentation."""
+
+ tracer_provider = kwargs.get("tracer_provider")
+ logger_provider = kwargs.get("logger_provider")
+ meter_provider = kwargs.get("meter_provider")
+
+ handler = TelemetryHandler(
+ tracer_provider=tracer_provider,
+ meter_provider=meter_provider,
+ logger_provider=logger_provider,
+ completion_hook=kwargs.get("completion_hook")
+ or load_completion_hook(),
+ )
+
+ wrap_function_wrapper(
+ "groq.resources.chat.completions",
+ "Completions.create",
+ chat_completions_create_v_new(handler),
+ )
+
+ wrap_function_wrapper(
+ "groq.resources.chat.completions",
+ "AsyncCompletions.create",
+ async_chat_completions_create_v_new(handler),
+ )
+
+ self._parse_supported = _is_parse_supported()
+ if self._parse_supported:
+ wrap_function_wrapper(
+ "groq.resources.chat.completions",
+ "Completions.parse",
+ chat_completions_create_v_new(handler),
+ )
+
+ wrap_function_wrapper(
+ "groq.resources.chat.completions",
+ "AsyncCompletions.parse",
+ async_chat_completions_create_v_new(handler),
+ )
+
+ def _uninstrument(self, **kwargs):
+ import groq # pylint: disable=import-outside-toplevel # noqa: PLC0415
+
+ unwrap(groq.resources.chat.completions.Completions, "create")
+ unwrap(groq.resources.chat.completions.AsyncCompletions, "create")
+ if self._parse_supported:
+ unwrap(groq.resources.chat.completions.Completions, "parse")
+ unwrap(groq.resources.chat.completions.AsyncCompletions, "parse")
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/chat_buffers.py b/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/chat_buffers.py
new file mode 100644
index 000000000..c89b4d3bb
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/chat_buffers.py
@@ -0,0 +1,52 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+from typing import Any
+
+
+class ToolCallBuffer:
+ def __init__(
+ self,
+ index: int,
+ tool_call_id: str | None,
+ function_name: str | None,
+ ) -> None:
+ self.index: int = index
+ self.function_name: str | None = function_name
+ self.tool_call_id: str | None = tool_call_id
+ self.arguments: list[str] = []
+
+ def append_arguments(self, arguments: str | None) -> None:
+ if arguments is not None:
+ self.arguments.append(arguments)
+
+
+class ChoiceBuffer:
+ def __init__(self, index: int) -> None:
+ self.index: int = index
+ self.finish_reason: str | None = None
+ self.text_content: list[str] = []
+ self.tool_calls_buffers: list[ToolCallBuffer | None] = []
+
+ def append_text_content(self, content: str) -> None:
+ self.text_content.append(content)
+
+ def append_tool_call(self, tool_call: Any) -> None:
+ idx = tool_call.index
+ for _ in range(len(self.tool_calls_buffers), idx + 1):
+ self.tool_calls_buffers.append(None)
+
+ function = tool_call.function
+ buffer = self.tool_calls_buffers[idx]
+ if buffer is None:
+ buffer = ToolCallBuffer(
+ idx,
+ tool_call.id,
+ function.name if function else None,
+ )
+ self.tool_calls_buffers[idx] = buffer
+
+ if function:
+ buffer.append_arguments(function.arguments)
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/chat_wrappers.py b/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/chat_wrappers.py
new file mode 100644
index 000000000..bd0e193d7
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/chat_wrappers.py
@@ -0,0 +1,221 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import json
+from typing import Any, Optional
+
+from opentelemetry.semconv._incubating.attributes import (
+ openai_attributes as OpenAIAttributes,
+)
+from opentelemetry.util.genai.invocation import InferenceInvocation
+from opentelemetry.util.genai.stream import (
+ AsyncStreamWrapper,
+ SyncStreamWrapper,
+)
+from opentelemetry.util.genai.types import (
+ OutputMessage,
+ Text,
+ ToolCallRequest,
+)
+
+from .chat_buffers import ChoiceBuffer
+
+
+class _ChatStreamMixin:
+ """Chat-specific hooks shared by sync and async stream wrappers."""
+
+ _self_invocation: InferenceInvocation
+ _self_capture_content: bool
+ _self_choice_buffers: list[ChoiceBuffer]
+ _self_response_id: Optional[str]
+ _self_response_model: Optional[str]
+ _self_service_tier: Optional[str]
+ _self_prompt_tokens: Optional[int]
+ _self_completion_tokens: Optional[int]
+
+ def _set_response_model(self, chunk: Any) -> None:
+ if self._self_response_model:
+ return
+
+ if getattr(chunk, "model", None):
+ self._self_response_model = chunk.model
+
+ def _set_response_id(self, chunk: Any) -> None:
+ if self._self_response_id:
+ return
+
+ if getattr(chunk, "id", None):
+ self._self_response_id = chunk.id
+
+ def _set_response_service_tier(self, chunk: Any) -> None:
+ if self._self_service_tier:
+ return
+
+ service_tier = getattr(chunk, "service_tier", None)
+ if service_tier:
+ self._self_service_tier = service_tier
+
+ def _build_streaming_response(self, chunk: Any) -> None:
+ if getattr(chunk, "choices", None) is None:
+ return
+
+ for choice in chunk.choices:
+ if not getattr(choice, "delta", None):
+ continue
+
+ for idx in range(len(self._self_choice_buffers), choice.index + 1):
+ self._self_choice_buffers.append(ChoiceBuffer(idx))
+
+ if getattr(choice, "finish_reason", None):
+ self._self_choice_buffers[
+ choice.index
+ ].finish_reason = choice.finish_reason
+
+ if getattr(choice.delta, "content", None) is not None:
+ self._self_choice_buffers[choice.index].append_text_content(
+ choice.delta.content
+ )
+
+ if getattr(choice.delta, "tool_calls", None) is not None:
+ for tool_call in choice.delta.tool_calls:
+ self._self_choice_buffers[choice.index].append_tool_call(
+ tool_call
+ )
+
+ def _set_usage(self, chunk: Any) -> None:
+ usage = getattr(chunk, "x_groq", None)
+ if usage and getattr(usage, "usage", None):
+ self._self_completion_tokens = getattr(
+ usage.usage, "completion_tokens", None
+ )
+ self._self_prompt_tokens = getattr(
+ usage.usage, "prompt_tokens", None
+ )
+
+ def _process_chunk(self, chunk: Any) -> None:
+ self._set_response_id(chunk)
+ self._set_response_model(chunk)
+ self._set_response_service_tier(chunk)
+ self._build_streaming_response(chunk)
+ self._set_usage(chunk)
+
+ def _set_output_messages(self) -> None:
+ if not self._self_capture_content: # optimization
+ return
+ output_messages = []
+ for choice in self._self_choice_buffers:
+ message = OutputMessage(
+ role="assistant",
+ finish_reason=choice.finish_reason or "error",
+ parts=[],
+ )
+ if choice.text_content:
+ message.parts.append(
+ Text(content="".join(choice.text_content))
+ )
+ if choice.tool_calls_buffers:
+ tool_calls = []
+ for tool_call in filter(None, choice.tool_calls_buffers):
+ arguments = None
+ arguments_str = "".join(tool_call.arguments)
+ if arguments_str:
+ try:
+ arguments = json.loads(arguments_str)
+ except json.JSONDecodeError:
+ arguments = arguments_str
+ tool_call_part = ToolCallRequest(
+ name=tool_call.function_name,
+ id=tool_call.tool_call_id,
+ arguments=arguments,
+ )
+ tool_calls.append(tool_call_part)
+ message.parts.extend(tool_calls)
+ output_messages.append(message)
+
+ self._self_invocation.output_messages = output_messages
+
+ def _on_stream_end(self) -> None:
+ self._cleanup()
+
+ def _on_stream_error(self, error: BaseException) -> None:
+ self._cleanup(error)
+
+ def parse(self) -> _ChatStreamMixin:
+ """Called when using with_raw_response with stream=True."""
+ return self
+
+ def _cleanup(self, error: Optional[BaseException] = None) -> None:
+ self._self_invocation.response_model_name = self._self_response_model
+ self._self_invocation.response_id = self._self_response_id
+ self._self_invocation.input_tokens = self._self_prompt_tokens
+ self._self_invocation.output_tokens = self._self_completion_tokens
+ finish_reasons = [
+ choice.finish_reason
+ for choice in self._self_choice_buffers
+ if choice.finish_reason
+ ]
+ if finish_reasons:
+ self._self_invocation.finish_reasons = finish_reasons
+ if self._self_service_tier:
+ self._self_invocation.attributes.update(
+ {
+ OpenAIAttributes.OPENAI_RESPONSE_SERVICE_TIER: self._self_service_tier
+ },
+ )
+
+ self._set_output_messages()
+
+ if error:
+ self._self_invocation.fail(error)
+ else:
+ self._self_invocation.stop()
+
+
+class ChatStreamWrapper(
+ _ChatStreamMixin,
+ SyncStreamWrapper,
+):
+ def __init__(
+ self,
+ stream: Any,
+ invocation: InferenceInvocation,
+ capture_content: bool,
+ ) -> None:
+ super().__init__(stream)
+ self._self_invocation = invocation
+ self._self_choice_buffers = []
+ self._self_capture_content = capture_content
+ self._self_response_id = None
+ self._self_response_model = None
+ self._self_service_tier = None
+ self._self_prompt_tokens = None
+ self._self_completion_tokens = None
+
+
+class AsyncChatStreamWrapper(
+ _ChatStreamMixin,
+ AsyncStreamWrapper,
+):
+ def __init__(
+ self,
+ stream: Any,
+ invocation: InferenceInvocation,
+ capture_content: bool,
+ ) -> None:
+ super().__init__(stream)
+ self._self_invocation = invocation
+ self._self_choice_buffers = []
+ self._self_capture_content = capture_content
+ self._self_response_id = None
+ self._self_response_model = None
+ self._self_service_tier = None
+ self._self_prompt_tokens = None
+ self._self_completion_tokens = None
+
+
+__all__ = [
+ "AsyncChatStreamWrapper",
+ "ChatStreamWrapper",
+]
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/package.py b/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/package.py
new file mode 100644
index 000000000..150e9e2a9
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/package.py
@@ -0,0 +1,4 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+_instruments = ("groq >= 0.6.0",)
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/patch.py b/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/patch.py
new file mode 100644
index 000000000..980586119
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/patch.py
@@ -0,0 +1,130 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+
+import logging
+
+from opentelemetry.util.genai.handler import TelemetryHandler
+from opentelemetry.util.genai.invocation import (
+ InferenceInvocation,
+)
+from opentelemetry.util.genai.types import (
+ Error,
+)
+
+from .chat_wrappers import AsyncChatStreamWrapper, ChatStreamWrapper
+from .utils import (
+ _prepare_output_messages,
+ create_chat_invocation,
+ is_streaming,
+)
+
+_logger = logging.getLogger(__name__)
+
+
+def chat_completions_create_v_new(
+ handler: TelemetryHandler,
+):
+ """Wrap the `create` method of the `ChatCompletion` class to trace it."""
+ capture_content = handler.should_capture_content()
+
+ def traced_method(wrapped, instance, args, kwargs):
+ chat_invocation = create_chat_invocation(
+ handler, kwargs, instance, capture_content=capture_content
+ )
+
+ try:
+ result = wrapped(*args, **kwargs)
+ if hasattr(result, "parse"):
+ # result is of type LegacyAPIResponse, call parse to get the actual response
+ parsed_result = result.parse()
+ else:
+ parsed_result = result
+ if is_streaming(kwargs):
+ return ChatStreamWrapper(
+ parsed_result, chat_invocation, capture_content
+ )
+
+ _set_response_properties(
+ chat_invocation, parsed_result, capture_content
+ )
+ chat_invocation.stop()
+ return result
+ except Exception as error:
+ chat_invocation.fail(Error(type=type(error), message=str(error)))
+ raise
+
+ return traced_method
+
+
+def async_chat_completions_create_v_new(
+ handler: TelemetryHandler,
+):
+ """Wrap the `create` method of the `AsyncChatCompletion` class to trace it."""
+ capture_content = handler.should_capture_content()
+
+ async def traced_method(wrapped, instance, args, kwargs):
+ chat_invocation = create_chat_invocation(
+ handler, kwargs, instance, capture_content=capture_content
+ )
+
+ try:
+ result = await wrapped(*args, **kwargs)
+ if hasattr(result, "parse"):
+ # result is of type LegacyAPIResponse, calling parse to get the actual response
+ parsed_result = result.parse()
+ else:
+ parsed_result = result
+ if is_streaming(kwargs):
+ return AsyncChatStreamWrapper(
+ parsed_result, chat_invocation, capture_content
+ )
+
+ _set_response_properties(
+ chat_invocation, parsed_result, capture_content
+ )
+ chat_invocation.stop()
+ return result
+
+ except Exception as error:
+ chat_invocation.fail(Error(type=type(error), message=str(error)))
+ raise
+
+ return traced_method
+
+
+def _set_response_properties(
+ chat_invocation: InferenceInvocation, result, capture_content: bool
+) -> InferenceInvocation:
+ if getattr(result, "model", None):
+ chat_invocation.response_model_name = result.model
+
+ if getattr(result, "choices", None):
+ finish_reasons = []
+ for choice in result.choices:
+ finish_reasons.append(choice.finish_reason or "error")
+
+ chat_invocation.finish_reasons = finish_reasons
+
+ if capture_content: # optimization
+ chat_invocation.output_messages = _prepare_output_messages(
+ result.choices
+ )
+
+ if getattr(result, "id", None):
+ chat_invocation.response_id = result.id
+
+ if getattr(result, "usage", None):
+ chat_invocation.input_tokens = result.usage.prompt_tokens
+ chat_invocation.output_tokens = result.usage.completion_tokens
+ elif getattr(result, "x_groq", None):
+ usage = getattr(result.x_groq, "usage", None)
+ if usage:
+ chat_invocation.input_tokens = getattr(
+ usage, "prompt_tokens", None
+ )
+ chat_invocation.output_tokens = getattr(
+ usage, "completion_tokens", None
+ )
+
+ return chat_invocation
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/utils.py b/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/utils.py
new file mode 100644
index 000000000..fea7281aa
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/utils.py
@@ -0,0 +1,254 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+from __future__ import annotations
+
+import json
+from typing import Any, Iterable, List, Mapping
+from urllib.parse import urlparse
+
+import groq
+from groq import NotGiven
+from httpx import URL
+
+from opentelemetry.semconv._incubating.attributes import (
+ gen_ai_attributes as GenAIAttributes,
+)
+from opentelemetry.util.genai.handler import TelemetryHandler
+from opentelemetry.util.genai.invocation import (
+ InferenceInvocation,
+)
+from opentelemetry.util.genai.types import (
+ FunctionToolDefinition,
+ InputMessage,
+ OutputMessage,
+ Text,
+ ToolCallRequest,
+ ToolCallResponse,
+ ToolDefinition,
+)
+
+_GroqOmit = getattr(groq, "Omit", None)
+
+
+def get_property_value(obj, property_name):
+ if isinstance(obj, dict):
+ return obj.get(property_name, None)
+
+ return getattr(obj, property_name, None)
+
+
+def get_server_address_and_port(
+ client_instance,
+) -> tuple[str | None, int | None]:
+ base_client = getattr(client_instance, "_client", None)
+ 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)
+ address = url.hostname
+ port = url.port
+
+ if port == 443:
+ port = None
+
+ return address, port
+
+
+def is_streaming(kwargs):
+ return non_numerical_value_is_set(kwargs.get("stream"))
+
+
+def non_numerical_value_is_set(value: bool | str | NotGiven | None):
+ return bool(value) and value_is_set(value)
+
+
+def value_is_set(value):
+ if _GroqOmit is not None and isinstance(value, _GroqOmit):
+ return False
+ return value is not None and not isinstance(value, NotGiven)
+
+
+def _openai_response_format_to_output_type(response_format_type: str) -> str:
+ if response_format_type in ("json_object", "json_schema"):
+ return GenAIAttributes.GenAiOutputTypeValues.JSON.value
+ return response_format_type
+
+
+def create_chat_invocation(
+ handler: TelemetryHandler,
+ kwargs,
+ client_instance,
+ capture_content: bool,
+) -> InferenceInvocation:
+ # pylint: disable=too-many-branches
+
+ address, port = get_server_address_and_port(client_instance)
+ invocation = handler.inference(
+ "groq",
+ request_model=kwargs.get("model", ""),
+ server_address=address if address else None,
+ server_port=port if port else None,
+ )
+ invocation.temperature = get_value(kwargs.get("temperature"))
+ invocation.top_p = get_value(kwargs.get("p") or kwargs.get("top_p"))
+ invocation.max_tokens = get_value(kwargs.get("max_tokens"))
+ invocation.presence_penalty = get_value(kwargs.get("presence_penalty"))
+ invocation.frequency_penalty = get_value(kwargs.get("frequency_penalty"))
+ invocation.seed = get_value(kwargs.get("seed"))
+ if (stop_sequences := get_value(kwargs.get("stop"))) is not None:
+ if isinstance(stop_sequences, str):
+ stop_sequences = [stop_sequences]
+ invocation.stop_sequences = stop_sequences
+
+ if (choice_count := get_value(kwargs.get("n"))) is not None:
+ # Only add non default, meaningful values
+ if isinstance(choice_count, int) and choice_count != 1:
+ invocation.request_choice_count = choice_count
+
+ if (
+ response_format := get_value(kwargs.get("response_format"))
+ ) is not None:
+ if isinstance(response_format, type):
+ invocation.attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = (
+ GenAIAttributes.GenAiOutputTypeValues.JSON.value
+ )
+ elif isinstance(response_format, Mapping):
+ if (
+ response_format_type := get_value(response_format.get("type"))
+ ) is not None:
+ invocation.attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = (
+ _openai_response_format_to_output_type(
+ response_format_type
+ )
+ )
+ elif isinstance(response_format, str):
+ invocation.attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = (
+ _openai_response_format_to_output_type(response_format)
+ )
+
+ if capture_content: # optimization
+ invocation.input_messages = _prepare_input_messages(
+ kwargs.get("messages", [])
+ )
+ invocation.tool_definitions = _prepare_tool_definitions(
+ kwargs.get("tools")
+ )
+ return invocation
+
+
+def get_value(v: Any):
+ if value_is_set(v):
+ return v
+ return None
+
+
+def _is_text_part(content: Any) -> bool:
+ return isinstance(content, str) or (
+ isinstance(content, Iterable)
+ and all(isinstance(part, str) for part in content)
+ )
+
+
+def _prepare_input_messages(messages) -> List[InputMessage]:
+ chat_messages = []
+ for message in messages:
+ role = get_property_value(message, "role")
+ chat_message = InputMessage(role=str(role), parts=[])
+ chat_messages.append(chat_message)
+
+ content = get_property_value(message, "content")
+
+ if role == "assistant":
+ tool_calls = get_property_value(message, "tool_calls")
+ if tool_calls:
+ chat_message.parts += extract_tool_calls_new(tool_calls)
+ if _is_text_part(content):
+ chat_message.parts.append(Text(content=str(content)))
+
+ elif role == "tool":
+ tool_call_id = get_property_value(message, "tool_call_id")
+ chat_message.parts.append(
+ ToolCallResponse(id=tool_call_id, response=content)
+ )
+
+ else:
+ # system, developer, user, fallback
+ if _is_text_part(content):
+ chat_message.parts.append(Text(content=str(content)))
+ return chat_messages
+
+
+def extract_tool_calls_new(tool_calls) -> list[ToolCallRequest]:
+ parts = []
+ for tool_call in tool_calls:
+ call_id = get_property_value(tool_call, "id")
+
+ func_name = ""
+ arguments = None
+ func = get_property_value(tool_call, "function")
+ if func:
+ func_name = get_property_value(func, "name") or ""
+ arguments_str = get_property_value(func, "arguments")
+ if arguments_str:
+ try:
+ arguments = json.loads(arguments_str)
+ except json.JSONDecodeError:
+ arguments = arguments_str
+
+ parts.append(
+ ToolCallRequest(id=call_id, name=func_name, arguments=arguments)
+ )
+ return parts
+
+
+def _prepare_tool_definitions(tools) -> list[ToolDefinition] | None:
+ if not tools:
+ return None
+
+ definitions: list[ToolDefinition] = []
+ for tool in tools:
+ tool_type = get_property_value(tool, "type")
+ if tool_type == "function":
+ func = get_property_value(tool, "function")
+ if func:
+ definitions.append(
+ FunctionToolDefinition(
+ name=get_property_value(func, "name") or "",
+ description=get_property_value(func, "description"),
+ parameters=get_property_value(func, "parameters"),
+ )
+ )
+ return definitions
+
+
+def _prepare_output_messages(choices) -> List[OutputMessage]:
+ output_messages = []
+ for choice in choices:
+ if choice.message:
+ parts = []
+ tool_calls = get_property_value(choice.message, "tool_calls")
+ if tool_calls:
+ parts += extract_tool_calls_new(tool_calls)
+ content = get_property_value(choice.message, "content")
+ if _is_text_part(content):
+ parts.append(Text(content=str(content)))
+
+ message = OutputMessage(
+ finish_reason=choice.finish_reason or "error",
+ role=(
+ choice.message.role
+ if choice.message and choice.message.role
+ else ""
+ ),
+ parts=parts,
+ )
+ output_messages.append(message)
+
+ return output_messages
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/version.py b/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/version.py
new file mode 100644
index 000000000..d302c33ff
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/src/opentelemetry/instrumentation/genai/groq/version.py
@@ -0,0 +1,3 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+__version__ = "1.0b0.dev"
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/__init__.py
new file mode 100644
index 000000000..e57cf4aba
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/__init__.py
@@ -0,0 +1,2 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/inference_conformance.yaml b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/inference_conformance.yaml
new file mode 100644
index 000000000..8b49345b1
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/inference_conformance.yaml
@@ -0,0 +1,48 @@
+interactions:
+- request:
+ body: '{"messages":[{"role":"user","content":"Say this is a test"}],"model":"llama3-8b-8192","stream":false}'
+ headers:
+ authorization:
+ - Bearer test_groq_api_key
+ content-type:
+ - application/json
+ method: POST
+ uri: https://api.groq.com/openai/v1/chat/completions
+ response:
+ body:
+ string: |-
+ {
+ "id": "chatcmpl-12345",
+ "object": "chat.completion",
+ "created": 1731368630,
+ "model": "llama3-8b-8192",
+ "choices": [
+ {
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": "This is a test."
+ },
+ "logprobs": null,
+ "finish_reason": "stop"
+ }
+ ],
+ "usage": {
+ "prompt_tokens": 12,
+ "completion_tokens": 14,
+ "total_tokens": 26
+ },
+ "x_groq": {
+ "usage": {
+ "prompt_tokens": 12,
+ "completion_tokens": 14
+ }
+ }
+ }
+ headers:
+ Content-Type:
+ - application/json
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_async_chat_completions_basic.yaml b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_async_chat_completions_basic.yaml
new file mode 100644
index 000000000..d2ce4302b
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_async_chat_completions_basic.yaml
@@ -0,0 +1,21 @@
+# TODO: this is generated by AI, re-record
+interactions:
+- request:
+ body: '{"messages":[{"role":"user","content":"Tell me a joke"}],"model":"llama3-8b-8192"}'
+ headers:
+ accept:
+ - application/json
+ content-type:
+ - application/json
+ method: POST
+ uri: https://api.groq.com/openai/v1/chat/completions
+ response:
+ body:
+ string: '{"id":"chatcmpl-test","choices":[{"finish_reason":"stop","index":0,"message":{"content":"A joke!","role":"assistant"}}],"created":123,"model":"llama3-8b-8192","object":"chat.completion"}'
+ headers:
+ content-type:
+ - application/json
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_async_chat_completions_streaming.yaml b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_async_chat_completions_streaming.yaml
new file mode 100644
index 000000000..fef748b35
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_async_chat_completions_streaming.yaml
@@ -0,0 +1,27 @@
+# TODO: this is generated by AI, re-record
+interactions:
+- request:
+ body: '{"messages":[{"role":"user","content":"Tell me a joke"}],"model":"llama3-8b-8192","stream":true}'
+ headers:
+ accept:
+ - application/json
+ content-type:
+ - application/json
+ method: POST
+ uri: https://api.groq.com/openai/v1/chat/completions
+ response:
+ body:
+ string: 'data: {"id":"chatcmpl-test","choices":[{"delta":{"content":"A"},"index":0}],"created":123,"model":"llama3-8b-8192","object":"chat.completion.chunk"}
+
+data: {"id":"chatcmpl-test","choices":[{"delta":{},"finish_reason":"stop","index":0}],"created":123,"model":"llama3-8b-8192","object":"chat.completion.chunk"}
+
+data: [DONE]
+
+'
+ headers:
+ content-type:
+ - text/event-stream
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_chat_completions_basic.yaml b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_chat_completions_basic.yaml
new file mode 100644
index 000000000..f03abb0ae
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_chat_completions_basic.yaml
@@ -0,0 +1,48 @@
+interactions:
+- request:
+ body: '{"messages":[{"role":"user","content":"Tell me a joke"}],"model":"llama3-8b-8192"}'
+ headers:
+ authorization:
+ - Bearer test_groq_api_key
+ content-type:
+ - application/json
+ method: POST
+ uri: https://api.groq.com/openai/v1/chat/completions
+ response:
+ body:
+ string: |-
+ {
+ "id": "chatcmpl-12345",
+ "object": "chat.completion",
+ "created": 1731368630,
+ "model": "llama3-8b-8192",
+ "choices": [
+ {
+ "index": 0,
+ "message": {
+ "role": "assistant",
+ "content": "Why did the chicken cross the road? To get to the other side."
+ },
+ "logprobs": null,
+ "finish_reason": "stop"
+ }
+ ],
+ "usage": {
+ "prompt_tokens": 12,
+ "completion_tokens": 14,
+ "total_tokens": 26
+ },
+ "x_groq": {
+ "usage": {
+ "prompt_tokens": 12,
+ "completion_tokens": 14
+ }
+ }
+ }
+ headers:
+ Content-Type:
+ - application/json
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_chat_completions_caller_side_error.yaml b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_chat_completions_caller_side_error.yaml
new file mode 100644
index 000000000..1f9bba514
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_chat_completions_caller_side_error.yaml
@@ -0,0 +1,27 @@
+# TODO: this is generated by AI, re-record
+interactions:
+- request:
+ body: '{"messages":[{"role":"user","content":"Tell me a long joke"}],"model":"llama3-8b-8192","stream":true}'
+ headers:
+ accept:
+ - application/json
+ content-type:
+ - application/json
+ method: POST
+ uri: https://api.groq.com/openai/v1/chat/completions
+ response:
+ body:
+ string: 'data: {"id":"chatcmpl-test","choices":[{"delta":{"content":"A"},"index":0}],"created":123,"model":"llama3-8b-8192","object":"chat.completion.chunk"}
+
+data: {"id":"chatcmpl-test","choices":[{"delta":{"content":" long"},"index":0}],"created":123,"model":"llama3-8b-8192","object":"chat.completion.chunk"}
+
+data: [DONE]
+
+'
+ headers:
+ content-type:
+ - text/event-stream
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_chat_completions_provider_error.yaml b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_chat_completions_provider_error.yaml
new file mode 100644
index 000000000..b0b582c1c
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_chat_completions_provider_error.yaml
@@ -0,0 +1,93 @@
+interactions:
+- request:
+ body: |-
+ {
+ "messages": [
+ {
+ "role": "user",
+ "content": "Tell me a joke"
+ }
+ ],
+ "model": "non-existent-model"
+ }
+ headers:
+ accept:
+ - application/json
+ accept-encoding:
+ - gzip, deflate
+ authorization:
+ - Bearer test_groq_api_key
+ connection:
+ - keep-alive
+ content-length:
+ - '86'
+ content-type:
+ - application/json
+ host:
+ - api.groq.com
+ user-agent:
+ - Groq/Python 1.6.0
+ x-stainless-arch:
+ - other:amd64
+ x-stainless-async:
+ - 'false'
+ x-stainless-lang:
+ - python
+ x-stainless-os:
+ - Windows
+ x-stainless-package-version:
+ - 1.6.0
+ x-stainless-read-timeout:
+ - '60'
+ x-stainless-retry-count:
+ - '0'
+ x-stainless-runtime:
+ - CPython
+ x-stainless-runtime-version:
+ - 3.12.13
+ method: POST
+ uri: https://api.groq.com/openai/v1/chat/completions
+ response:
+ body:
+ string: |-
+ {
+ "error": {
+ "message": "Invalid API Key",
+ "type": "invalid_request_error",
+ "code": "invalid_api_key"
+ }
+ }
+ headers:
+ alt-svc:
+ - h3=":443"; ma=86400
+ cache-control:
+ - private, max-age=0, no-store, no-cache, must-revalidate
+ cf-cache-status:
+ - DYNAMIC
+ cf-ray:
+ - a2471c7b281cb1c5-BOM
+ connection:
+ - keep-alive
+ content-length:
+ - '96'
+ content-type:
+ - application/json
+ date:
+ - Sat, 01 Aug 2026 19:04:54 GMT
+ server:
+ - cloudflare
+ set-cookie: test_set_cookie
+ strict-transport-security:
+ - max-age=15552000
+ vary:
+ - Origin
+ via:
+ - 1.1 google
+ x-groq-region:
+ - bom
+ x-request-id:
+ - req_01kyzbe28dfpt959zxf99vxag3
+ status:
+ code: 401
+ message: Unauthorized
+version: 1
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_chat_completions_stream_side_error.yaml b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_chat_completions_stream_side_error.yaml
new file mode 100644
index 000000000..1f9bba514
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_chat_completions_stream_side_error.yaml
@@ -0,0 +1,27 @@
+# TODO: this is generated by AI, re-record
+interactions:
+- request:
+ body: '{"messages":[{"role":"user","content":"Tell me a long joke"}],"model":"llama3-8b-8192","stream":true}'
+ headers:
+ accept:
+ - application/json
+ content-type:
+ - application/json
+ method: POST
+ uri: https://api.groq.com/openai/v1/chat/completions
+ response:
+ body:
+ string: 'data: {"id":"chatcmpl-test","choices":[{"delta":{"content":"A"},"index":0}],"created":123,"model":"llama3-8b-8192","object":"chat.completion.chunk"}
+
+data: {"id":"chatcmpl-test","choices":[{"delta":{"content":" long"},"index":0}],"created":123,"model":"llama3-8b-8192","object":"chat.completion.chunk"}
+
+data: [DONE]
+
+'
+ headers:
+ content-type:
+ - text/event-stream
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_chat_completions_streaming.yaml b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_chat_completions_streaming.yaml
new file mode 100644
index 000000000..fef748b35
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/cassettes/test_chat_completions_streaming.yaml
@@ -0,0 +1,27 @@
+# TODO: this is generated by AI, re-record
+interactions:
+- request:
+ body: '{"messages":[{"role":"user","content":"Tell me a joke"}],"model":"llama3-8b-8192","stream":true}'
+ headers:
+ accept:
+ - application/json
+ content-type:
+ - application/json
+ method: POST
+ uri: https://api.groq.com/openai/v1/chat/completions
+ response:
+ body:
+ string: 'data: {"id":"chatcmpl-test","choices":[{"delta":{"content":"A"},"index":0}],"created":123,"model":"llama3-8b-8192","object":"chat.completion.chunk"}
+
+data: {"id":"chatcmpl-test","choices":[{"delta":{},"finish_reason":"stop","index":0}],"created":123,"model":"llama3-8b-8192","object":"chat.completion.chunk"}
+
+data: [DONE]
+
+'
+ headers:
+ content-type:
+ - text/event-stream
+ status:
+ code: 200
+ message: OK
+version: 1
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/tests/conformance/__init__.py b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/conformance/__init__.py
new file mode 100644
index 000000000..e57cf4aba
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/conformance/__init__.py
@@ -0,0 +1,2 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/tests/conformance/inference.py b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/conformance/inference.py
new file mode 100644
index 000000000..7cf846962
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/conformance/inference.py
@@ -0,0 +1,49 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+"""Conformance scenario: groq chat completion (inference)."""
+
+from __future__ import annotations
+
+from typing import Any
+
+from groq import Groq
+
+from opentelemetry.instrumentation.genai.groq import GroqInstrumentor
+from opentelemetry.sdk._logs import LoggerProvider
+from opentelemetry.sdk.metrics import MeterProvider
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.test_util_genai.conformance import Scenario
+from opentelemetry.test_util_genai.instrumentor import instrument
+
+
+class InferenceScenario(Scenario):
+ expected_spans = {"chat": 1}
+ expected_metrics = (
+ "gen_ai.client.operation.duration",
+ "gen_ai.client.token.usage",
+ )
+
+ def run(
+ self,
+ *,
+ tracer_provider: TracerProvider,
+ meter_provider: MeterProvider,
+ logger_provider: LoggerProvider,
+ vcr: Any,
+ ) -> None:
+ with instrument(
+ GroqInstrumentor(),
+ tracer_provider=tracer_provider,
+ logger_provider=logger_provider,
+ meter_provider=meter_provider,
+ content_capture="SPAN_ONLY",
+ ):
+ with vcr.use_cassette("inference_conformance.yaml"):
+ Groq().chat.completions.create(
+ messages=[
+ {"role": "user", "content": "Say this is a test"}
+ ],
+ model="llama3-8b-8192",
+ stream=False,
+ )
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/tests/conftest.py b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/conftest.py
new file mode 100644
index 000000000..1b2dcd627
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/conftest.py
@@ -0,0 +1,128 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+"""Unit tests configuration module."""
+
+import os
+
+import pytest
+from groq import AsyncGroq, Groq
+
+from opentelemetry.instrumentation.genai.groq import GroqInstrumentor
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk.trace.export import SimpleSpanProcessor
+from opentelemetry.sdk.trace.sampling import ALWAYS_OFF
+from opentelemetry.test_util_genai.instrumentor import instrument
+
+pytest_plugins = [
+ "opentelemetry.test_util_genai.fixtures",
+ "opentelemetry.test_util_genai.vcr",
+]
+
+
+@pytest.fixture(autouse=True)
+def environment():
+ if not os.getenv("GROQ_API_KEY"):
+ os.environ["GROQ_API_KEY"] = "test_groq_api_key"
+
+
+@pytest.fixture
+def groq_client():
+ return Groq()
+
+
+@pytest.fixture
+def async_groq_client():
+ return AsyncGroq()
+
+
+@pytest.fixture(scope="module")
+def vcr_config():
+ from opentelemetry.test_util_genai.vcr import ( # noqa: PLC0415
+ scrub_response_headers_overwrite,
+ )
+
+ return {
+ "filter_headers": [
+ ("cookie", "test_cookie"),
+ ("authorization", "Bearer test_groq_api_key"),
+ ],
+ "decode_compressed_response": True,
+ "before_record_response": scrub_response_headers_overwrite(
+ {
+ "Set-Cookie": "test_set_cookie",
+ }
+ ),
+ }
+
+
+@pytest.fixture(
+ scope="function",
+ params=[(True, "span_only")],
+ name="content_mode",
+)
+def fixture_content_mode(request):
+ # returns tuple: (latest_experimental_enabled: bool, content_mode: str)
+ # we don't test (True, "event_only"), (True, "span_and_event") because it's util's
+ # responsibility.
+ return request.param
+
+
+@pytest.fixture(scope="function")
+def instrument_no_content(
+ tracer_provider,
+ logger_provider,
+ meter_provider,
+ content_mode,
+):
+ with instrument(
+ GroqInstrumentor(),
+ tracer_provider=tracer_provider,
+ logger_provider=logger_provider,
+ meter_provider=meter_provider,
+ ) as instrumentor:
+ yield instrumentor
+
+
+@pytest.fixture(scope="function")
+def instrument_with_content(
+ tracer_provider, logger_provider, meter_provider, content_mode
+):
+ _, content_mode_value = content_mode
+ with instrument(
+ GroqInstrumentor(),
+ tracer_provider=tracer_provider,
+ logger_provider=logger_provider,
+ meter_provider=meter_provider,
+ content_capture=content_mode_value,
+ ) as instrumentor:
+ yield instrumentor
+
+
+@pytest.fixture(scope="function")
+def instrument_with_content_unsampled(
+ span_exporter, logger_provider, meter_provider, content_mode
+):
+ _, content_mode_value = content_mode
+ tracer_provider = TracerProvider(sampler=ALWAYS_OFF)
+ tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter))
+ with instrument(
+ GroqInstrumentor(),
+ tracer_provider=tracer_provider,
+ logger_provider=logger_provider,
+ meter_provider=meter_provider,
+ content_capture=content_mode_value,
+ ) as instrumentor:
+ yield instrumentor
+
+
+@pytest.fixture(scope="function")
+def instrument_event_only(tracer_provider, logger_provider, meter_provider):
+ with instrument(
+ GroqInstrumentor(),
+ tracer_provider=tracer_provider,
+ logger_provider=logger_provider,
+ meter_provider=meter_provider,
+ content_capture="event_only",
+ ) as instrumentor:
+ yield instrumentor
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/tests/requirements.latest.txt b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/requirements.latest.txt
new file mode 100644
index 000000000..6151caf32
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/requirements.latest.txt
@@ -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.
+
+groq
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/requirements.oldest.txt
new file mode 100644
index 000000000..2c12cb100
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/requirements.oldest.txt
@@ -0,0 +1,24 @@
+# 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.
+
+# Oldest test-only dependency pins.
+#
+# The package's own declared deps (groq via the instruments extra, opentelemetry-api,
+# opentelemetry-instrumentation, opentelemetry-semantic-conventions, opentelemetry-util-genai)
+# are resolved to their pyproject.toml floors by UV_RESOLUTION=lowest-direct on the oldest tox
+# factor, so they are NOT pinned here — pyproject.toml is the single source of truth. The
+# OpenTelemetry SDK and test utilities come transitively from opentelemetry-test-util-genai, which
+# every oldest env installs. Pin here only test-only deps that nothing else already provides.
+
+httpx==0.27.2
\ No newline at end of file
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/tests/test_chat_completions.py b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/test_chat_completions.py
new file mode 100644
index 000000000..4afbdc890
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/test_chat_completions.py
@@ -0,0 +1,197 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+from unittest import mock
+
+import pytest
+from groq import AsyncGroq, Groq
+
+from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import (
+ GEN_AI_OPERATION_NAME,
+ GEN_AI_REQUEST_MODEL,
+ GEN_AI_RESPONSE_MODEL,
+ GEN_AI_SYSTEM,
+ GenAiOperationNameValues,
+)
+from opentelemetry.trace import StatusCode
+
+
+@pytest.mark.vcr
+def test_chat_completions_basic(
+ instrument_no_content,
+ span_exporter,
+ vcr,
+ groq_client: Groq,
+):
+ with vcr.use_cassette("test_chat_completions_basic.yaml"):
+ response = groq_client.chat.completions.create(
+ model="llama3-8b-8192",
+ messages=[
+ {"role": "user", "content": "Tell me a joke"},
+ ],
+ )
+
+ assert response is not None
+ spans = span_exporter.get_finished_spans()
+ assert len(spans) == 1
+ span = spans[0]
+
+ assert (
+ span.attributes.get(GEN_AI_SYSTEM) == "groq"
+ or span.attributes.get("gen_ai.provider.name") == "groq"
+ )
+ assert (
+ span.attributes.get(GEN_AI_OPERATION_NAME)
+ == GenAiOperationNameValues.CHAT.value
+ )
+ assert span.attributes[GEN_AI_REQUEST_MODEL] == "llama3-8b-8192"
+ assert GEN_AI_RESPONSE_MODEL in span.attributes
+
+
+@pytest.mark.vcr
+@pytest.mark.asyncio
+async def test_async_chat_completions_basic(
+ instrument_no_content,
+ span_exporter,
+ vcr,
+ async_groq_client: AsyncGroq,
+):
+ with vcr.use_cassette("test_async_chat_completions_basic.yaml"):
+ response = await async_groq_client.chat.completions.create(
+ model="llama3-8b-8192",
+ messages=[{"role": "user", "content": "Tell me a joke"}],
+ )
+
+ assert response is not None
+ spans = span_exporter.get_finished_spans()
+ assert len(spans) == 1
+ span = spans[0]
+ assert (
+ span.attributes.get(GEN_AI_SYSTEM) == "groq"
+ or span.attributes.get("gen_ai.provider.name") == "groq"
+ )
+ assert (
+ span.attributes.get(GEN_AI_OPERATION_NAME)
+ == GenAiOperationNameValues.CHAT.value
+ )
+ assert span.attributes[GEN_AI_REQUEST_MODEL] == "llama3-8b-8192"
+
+
+@pytest.mark.vcr
+def test_chat_completions_streaming(
+ instrument_no_content,
+ span_exporter,
+ vcr,
+ groq_client: Groq,
+):
+ with vcr.use_cassette("test_chat_completions_streaming.yaml"):
+ response = groq_client.chat.completions.create(
+ model="llama3-8b-8192",
+ messages=[{"role": "user", "content": "Tell me a joke"}],
+ stream=True,
+ )
+ for _ in response:
+ pass
+
+ spans = span_exporter.get_finished_spans()
+ assert len(spans) == 1
+ span = spans[0]
+ assert (
+ span.attributes.get(GEN_AI_SYSTEM) == "groq"
+ or span.attributes.get("gen_ai.provider.name") == "groq"
+ )
+
+
+@pytest.mark.vcr
+@pytest.mark.asyncio
+async def test_async_chat_completions_streaming(
+ instrument_no_content,
+ span_exporter,
+ vcr,
+ async_groq_client: AsyncGroq,
+):
+ with vcr.use_cassette("test_async_chat_completions_streaming.yaml"):
+ response = await async_groq_client.chat.completions.create(
+ model="llama3-8b-8192",
+ messages=[{"role": "user", "content": "Tell me a joke"}],
+ stream=True,
+ )
+ async for _ in response:
+ pass
+
+ spans = span_exporter.get_finished_spans()
+ assert len(spans) == 1
+
+
+@pytest.mark.vcr
+def test_chat_completions_provider_error(
+ instrument_no_content,
+ span_exporter,
+ vcr,
+ groq_client: Groq,
+):
+ with vcr.use_cassette("test_chat_completions_provider_error.yaml"):
+ with pytest.raises(Exception):
+ groq_client.chat.completions.create(
+ model="non-existent-model",
+ messages=[{"role": "user", "content": "Tell me a joke"}],
+ )
+
+ spans = span_exporter.get_finished_spans()
+ assert len(spans) == 1
+ span = spans[0]
+ assert span.status.status_code == StatusCode.ERROR
+ assert "error.type" in span.attributes
+
+
+@pytest.mark.vcr
+def test_chat_completions_caller_side_error_inside_stream(
+ instrument_no_content,
+ span_exporter,
+ vcr,
+ groq_client: Groq,
+):
+ with vcr.use_cassette("test_chat_completions_caller_side_error.yaml"):
+ with pytest.raises(ValueError, match="Caller error"):
+ response = groq_client.chat.completions.create(
+ model="llama3-8b-8192",
+ messages=[{"role": "user", "content": "Tell me a long joke"}],
+ stream=True,
+ )
+ with response:
+ raise ValueError("Caller error")
+
+ spans = span_exporter.get_finished_spans()
+ assert len(spans) == 1
+ span = spans[0]
+ assert span.status.status_code == StatusCode.ERROR
+ assert span.attributes.get("error.type") == "ValueError"
+
+
+@pytest.mark.vcr
+def test_chat_completions_stream_side_error_mid_iteration(
+ instrument_no_content,
+ span_exporter,
+ vcr,
+ groq_client: Groq,
+):
+ with vcr.use_cassette("test_chat_completions_stream_side_error.yaml"):
+ response = groq_client.chat.completions.create(
+ model="llama3-8b-8192",
+ messages=[{"role": "user", "content": "Tell me a long joke"}],
+ stream=True,
+ )
+
+ iterator_mock = mock.MagicMock()
+ iterator_mock.__next__.side_effect = ConnectionError("Stream dropped")
+ response._self_iterator = iterator_mock
+
+ with pytest.raises(ConnectionError, match="Stream dropped"):
+ for _ in response:
+ pass
+
+ spans = span_exporter.get_finished_spans()
+ assert len(spans) == 1
+ span = spans[0]
+ assert span.status.status_code == StatusCode.ERROR
+ assert span.attributes.get("error.type") == "ConnectionError"
diff --git a/instrumentation/opentelemetry-instrumentation-genai-groq/tests/test_conformance.py b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/test_conformance.py
new file mode 100644
index 000000000..c84b975b9
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-genai-groq/tests/test_conformance.py
@@ -0,0 +1,34 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+"""Per-scenario conformance tests for groq."""
+
+from __future__ import annotations
+
+from typing import Any
+
+import pytest
+
+# Skip collection when weaver_live_check isn't installed (non-conformance envs).
+pytest.importorskip("opentelemetry.test.weaver_live_check")
+
+from opentelemetry.test.weaver_live_check import WeaverLiveCheck # noqa: E402
+from opentelemetry.test_util_genai.conformance import ( # noqa: E402
+ Scenario,
+ run_conformance,
+)
+
+from .conformance.inference import InferenceScenario
+
+
+@pytest.mark.parametrize(
+ "scenario",
+ [
+ InferenceScenario(),
+ ],
+ ids=lambda s: type(s).__name__,
+)
+def test_conformance(
+ scenario: Scenario, vcr: Any, weaver_live_check: WeaverLiveCheck
+) -> None:
+ run_conformance(scenario, vcr=vcr, weaver=weaver_live_check)
diff --git a/pyproject.toml b/pyproject.toml
index cbb921195..76666be4c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -101,6 +101,7 @@ include = [
"instrumentation/opentelemetry-instrumentation-genai-agno",
"instrumentation/opentelemetry-instrumentation-genai-anthropic",
"instrumentation/opentelemetry-instrumentation-genai-claude-agent-sdk",
+
"instrumentation/opentelemetry-instrumentation-genai-langchain",
"instrumentation/opentelemetry-instrumentation-genai-llama-index",
"instrumentation/opentelemetry-instrumentation-genai-openai-agents",
@@ -118,6 +119,7 @@ exclude = [
"instrumentation/opentelemetry-instrumentation-genai-anthropic/examples/**/*.py",
"instrumentation/opentelemetry-instrumentation-genai-claude-agent-sdk/tests/**/*.py",
"instrumentation/opentelemetry-instrumentation-genai-claude-agent-sdk/examples/**/*.py",
+
"instrumentation/opentelemetry-instrumentation-genai-langchain/tests/**/*.py",
"instrumentation/opentelemetry-instrumentation-genai-langchain/examples/**/*.py",
"instrumentation/opentelemetry-instrumentation-genai-llama-index/tests/**/*.py",
diff --git a/pytest.ini b/pytest.ini
index 886af1c76..bf1a6c984 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -3,3 +3,4 @@
addopts = -rfE -v
log_cli = true
log_cli_level = warning
+asyncio_default_fixture_loop_scope = function
diff --git a/screenshots/photo_2026-07-27_03-17-43.jpg b/screenshots/photo_2026-07-27_03-17-43.jpg
new file mode 100644
index 000000000..d0548e5ee
Binary files /dev/null and b/screenshots/photo_2026-07-27_03-17-43.jpg differ
diff --git a/screenshots/photo_2026-07-27_03-17-48.jpg b/screenshots/photo_2026-07-27_03-17-48.jpg
new file mode 100644
index 000000000..3db4636cc
Binary files /dev/null and b/screenshots/photo_2026-07-27_03-17-48.jpg differ
diff --git a/screenshots/photo_2026-07-27_03-17-55.jpg b/screenshots/photo_2026-07-27_03-17-55.jpg
new file mode 100644
index 000000000..75cf32270
Binary files /dev/null and b/screenshots/photo_2026-07-27_03-17-55.jpg differ
diff --git a/scripts/check_license_header.py b/scripts/check_license_header.py
index 500476344..286802c68 100644
--- a/scripts/check_license_header.py
+++ b/scripts/check_license_header.py
@@ -16,6 +16,7 @@
"target",
".tox",
".venv",
+ "test_env",
"__pycache__",
"node_modules",
)
diff --git a/tox.ini b/tox.ini
index b614d3b57..86f8c17ed 100644
--- a/tox.ini
+++ b/tox.ini
@@ -43,6 +43,12 @@ envlist =
py314-test-instrumentation-genai-anthropic-conformance
lint-instrumentation-genai-anthropic
+ ; instrumentation-genai-groq
+ py3{10,11,12,13,14}-test-instrumentation-genai-groq-latest
+ py310-test-instrumentation-genai-groq-oldest
+ py314-test-instrumentation-genai-groq-conformance
+ lint-instrumentation-genai-groq
+
; instrumentation-genai-claude-agent-sdk
py3{10,11,12,13}-test-instrumentation-genai-claude-agent-sdk-latest
py310-test-instrumentation-genai-claude-agent-sdk-oldest
@@ -145,6 +151,17 @@ deps =
anthropic-conformance: {[testenv]pytest_deps}
anthropic-conformance: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/requirements.latest.txt
+ groq-oldest: {[testenv]pytest_deps}
+ groq-oldest: -e {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-groq[instruments]
+ groq-oldest: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-groq/tests/requirements.oldest.txt
+ groq-latest: {[testenv]test_deps}
+ groq-latest: {[testenv]pytest_deps}
+ groq-latest: -e {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-groq[instruments]
+ groq-latest: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-groq/tests/requirements.latest.txt
+ groq-conformance: {[testenv]pytest_deps}
+ groq-conformance: -e {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-groq[instruments]
+ groq-conformance: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-groq/tests/requirements.latest.txt
+
claude-agent-sdk-oldest: {[testenv]pytest_deps}
claude-agent-sdk-oldest: -e {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-claude-agent-sdk[instruments]
claude-agent-sdk-oldest: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-claude-agent-sdk/tests/requirements.oldest.txt
@@ -221,6 +238,10 @@ commands =
test-instrumentation-genai-anthropic-conformance: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/test_conformance.py --vcr-record=none {posargs}
lint-instrumentation-genai-anthropic: sh -c "cd instrumentation && ruff check opentelemetry-instrumentation-genai-anthropic"
+ test-instrumentation-genai-groq-{oldest,latest}: pytest --ignore={toxinidir}/instrumentation/opentelemetry-instrumentation-genai-groq/tests/test_conformance.py {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-groq/tests {posargs}
+ test-instrumentation-genai-groq-conformance: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-groq/tests/test_conformance.py --vcr-record=none {posargs}
+ lint-instrumentation-genai-groq: sh -c "cd instrumentation && ruff check opentelemetry-instrumentation-genai-groq"
+
test-instrumentation-genai-claude-agent-sdk: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-claude-agent-sdk/tests --vcr-record=none {posargs}
lint-instrumentation-genai-claude-agent-sdk: sh -c "cd instrumentation && ruff check opentelemetry-instrumentation-genai-claude-agent-sdk"
@@ -296,11 +317,11 @@ commands =
[testenv:shellcheck]
-commands_pre =
- sh -c "sudo apt update -y && sudo apt install --assume-yes shellcheck"
+# commands_pre =
+# sh -c "sudo apt update -y && sudo apt install --assume-yes shellcheck"
commands =
- sh -c "find {toxinidir} -name \*.sh | xargs shellcheck --severity=warning"
+ sh -c "find {toxinidir} -type f -name \*.sh -not -path '*/.tox/*' -not -path '*/.venv/*' -not -path '*/test_env/*' | xargs --no-run-if-empty shellcheck --severity=warning"
[testenv:{precommit,ruff}]
basepython: python3
@@ -319,6 +340,7 @@ deps =
-e {toxinidir}/instrumentation/opentelemetry-instrumentation-google-genai[instruments]
-e {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-agno[instruments]
-e {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-anthropic[instruments]
+ -e {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-groq[instruments]
-e {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-langchain[instruments]
-e {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-llama-index[instruments]
-e {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-claude-agent-sdk[instruments]
diff --git a/uv.lock b/uv.lock
index 13c7208d8..c3c5fda67 100644
--- a/uv.lock
+++ b/uv.lock
@@ -17,6 +17,7 @@ members = [
"opentelemetry-instrumentation-genai-agno",
"opentelemetry-instrumentation-genai-anthropic",
"opentelemetry-instrumentation-genai-claude-agent-sdk",
+ "opentelemetry-instrumentation-genai-groq",
"opentelemetry-instrumentation-genai-langchain",
"opentelemetry-instrumentation-genai-llama-index",
"opentelemetry-instrumentation-genai-openai",
@@ -1054,6 +1055,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" },
]
+[[package]]
+name = "groq"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "distro" },
+ { name = "httpx" },
+ { name = "pydantic" },
+ { name = "sniffio" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/67/3e/687064918e861082cb02165872bbda31269126a108a78794dae86ddadae2/groq-1.6.0.tar.gz", hash = "sha256:c4237ecf0053ba85fd926bb31cb4b178996310474b4618867994e3e40d8e3c02", size = 158424, upload-time = "2026-07-24T17:25:49.517Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/40/164338d796cf41ea5930a42023e986d8c993720bed670d5aa143e5854873/groq-1.6.0-py3-none-any.whl", hash = "sha256:fa16db582455db324adcff1b5908519474736872e28cd5c8fa2b8bef6860e12a", size = 143693, upload-time = "2026-07-24T17:25:48.2Z" },
+]
+
[[package]]
name = "grpcio"
version = "1.78.0"
@@ -2303,6 +2321,31 @@ requires-dist = [
]
provides-extras = ["instruments"]
+[[package]]
+name = "opentelemetry-instrumentation-genai-groq"
+source = { editable = "instrumentation/opentelemetry-instrumentation-genai-groq" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "opentelemetry-util-genai" },
+]
+
+[package.optional-dependencies]
+instruments = [
+ { name = "groq" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "groq", marker = "extra == 'instruments'", specifier = ">=0.6.0" },
+ { name = "opentelemetry-api", specifier = "~=1.43" },
+ { name = "opentelemetry-instrumentation", specifier = ">=0.64b0,<1" },
+ { name = "opentelemetry-semantic-conventions", specifier = ">=0.64b0,<1" },
+ { name = "opentelemetry-util-genai", editable = "util/opentelemetry-util-genai" },
+]
+provides-extras = ["instruments"]
+
[[package]]
name = "opentelemetry-instrumentation-genai-langchain"
source = { editable = "instrumentation/opentelemetry-instrumentation-genai-langchain" }