diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/__init__.py b/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/__init__.py index 9c42de4..02beefb 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/__init__.py +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/__init__.py @@ -176,7 +176,9 @@ def _chat_completion_wrapper(self, wrapped, instance, args, kwargs): logger.debug(f"openai.resources.chat.completions.Completions.create result: {result}") if span.is_recording(): - _set_span_attributes_from_response(span, result.id, result.model, result.choices, result.usage) + _set_span_attributes_from_response( + span, result.id, result.model, result.choices, result.usage, getattr(result, "service_tier", None) + ) _record_token_usage_metrics(self.token_usage_metric, span, result.usage) _record_operation_duration_metric(self.operation_duration_metric, span, start_time) @@ -231,7 +233,9 @@ async def _async_chat_completion_wrapper(self, wrapped, instance, args, kwargs): logger.debug(f"openai.resources.chat.completions.AsyncCompletions.create result: {result}") if span.is_recording(): - _set_span_attributes_from_response(span, result.id, result.model, result.choices, result.usage) + _set_span_attributes_from_response( + span, result.id, result.model, result.choices, result.usage, getattr(result, "service_tier", None) + ) _record_token_usage_metrics(self.token_usage_metric, span, result.usage) _record_operation_duration_metric(self.operation_duration_metric, span, start_time) diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/helpers.py b/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/helpers.py index f35ea35..9b45158 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/helpers.py +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/helpers.py @@ -16,10 +16,14 @@ from collections.abc import Iterable, Mapping from timeit import default_timer -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from opentelemetry._events import Event, EventLogger from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import ( + GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT, + GEN_AI_OPENAI_REQUEST_SEED, + GEN_AI_OPENAI_REQUEST_SERVICE_TIER, + GEN_AI_OPENAI_RESPONSE_SERVICE_TIER, GEN_AI_OPERATION_NAME, GEN_AI_REQUEST_FREQUENCY_PENALTY, GEN_AI_REQUEST_MAX_TOKENS, @@ -65,7 +69,12 @@ def _set_span_attributes_from_response( - span: Span, response_id: str, model: str, choices, usage: CompletionUsage + span: Span, + response_id: str, + model: str, + choices, + usage: CompletionUsage, + service_tier: Optional[str], ) -> None: span.set_attribute(GEN_AI_RESPONSE_ID, response_id) span.set_attribute(GEN_AI_RESPONSE_MODEL, model) @@ -76,6 +85,9 @@ def _set_span_attributes_from_response( if usage: span.set_attribute(GEN_AI_USAGE_INPUT_TOKENS, usage.prompt_tokens) span.set_attribute(GEN_AI_USAGE_OUTPUT_TOKENS, usage.completion_tokens) + # this is available only if requested + if service_tier: + span.set_attribute(GEN_AI_OPENAI_RESPONSE_SERVICE_TIER, service_tier) def _set_embeddings_span_attributes_from_response(span: Span, model: str, usage: CompletionUsage) -> None: @@ -126,6 +138,17 @@ def _get_span_attributes_from_wrapper(instance, kwargs) -> Attributes: if isinstance(stop_sequences, str): stop_sequences = [stop_sequences] span_attributes[GEN_AI_REQUEST_STOP_SEQUENCES] = stop_sequences + if (seed := kwargs.get("seed")) is not None: + span_attributes[GEN_AI_OPENAI_REQUEST_SEED] = seed + if (service_tier := kwargs.get("service_tier")) is not None: + span_attributes[GEN_AI_OPENAI_REQUEST_SERVICE_TIER] = service_tier + if (response_format := kwargs.get("response_format")) is not None: + # response_format may be string or object with a string in the `type` key + if isinstance(response_format, Mapping): + if (response_format_type := response_format.get("type")) is not None: + span_attributes[GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT] = response_format_type + else: + span_attributes[GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT] = response_format return span_attributes diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/wrappers.py b/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/wrappers.py index 5ef5e8d..c05c077 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/wrappers.py +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/src/opentelemetry/instrumentation/openai/wrappers.py @@ -59,6 +59,7 @@ def __init__( self.model = None self.choices = [] self.usage = None + self.service_tier = None def end(self, exc=None): # StopIteration is not an error, it signals that we have consumed all the stream @@ -70,7 +71,9 @@ def end(self, exc=None): return if self.span.is_recording(): - _set_span_attributes_from_response(self.span, self.response_id, self.model, self.choices, self.usage) + _set_span_attributes_from_response( + self.span, self.response_id, self.model, self.choices, self.usage, self.service_tier + ) _record_operation_duration_metric(self.operation_duration_metric, self.span, self.start_time) if self.usage: @@ -92,6 +95,8 @@ def process_chunk(self, chunk): # with `include_usage` in `stream_options` we will get a last chunk without choices if chunk.choices: self.choices += chunk.choices + if hasattr(chunk, "service_tier"): + self.service_tier = chunk.service_tier def __enter__(self): return self diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_all_the_client_options[azure_provider_chat_completions].yaml b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_all_the_client_options[azure_provider_chat_completions].yaml index 0b7cbdc..7d9b5cf 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_all_the_client_options[azure_provider_chat_completions].yaml +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_all_the_client_options[azure_provider_chat_completions].yaml @@ -12,6 +12,11 @@ interactions: "frequency_penalty": 0, "max_tokens": 100, "presence_penalty": 0, + "response_format": { + "type": "text" + }, + "seed": 100, + "service_tier": "default", "stop": "foo", "temperature": 1, "top_p": 1 @@ -28,29 +33,29 @@ interactions: connection: - keep-alive content-length: - - '244' + - '321' content-type: - application/json host: - test.openai.azure.com user-agent: - - AzureOpenAI/Python 1.54.3 + - AzureOpenAI/Python 1.54.5 x-stainless-arch: - - arm64 + - x64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - - MacOS + - Linux x-stainless-package-version: - - 1.54.3 + - 1.54.5 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - - 3.12.6 + - 3.10.12 method: POST uri: https://test.openai.azure.com/openai/deployments/test-azure-deployment/chat/completions?api-version=2024-08-01-preview response: @@ -64,6 +69,14 @@ interactions: "filtered": false, "severity": "safe" }, + "protected_material_code": { + "filtered": false, + "detected": false + }, + "protected_material_text": { + "filtered": false, + "detected": false + }, "self_harm": { "filtered": false, "severity": "safe" @@ -81,14 +94,14 @@ interactions: "index": 0, "logprobs": null, "message": { - "content": "Atlantic Ocean", + "content": "South Atlantic Ocean.", "role": "assistant" } } ], - "created": 1731466203, - "id": "chatcmpl-ASxkBZGOa53uXX1Ciygl77IrF8PbB", - "model": "gpt-4-32k", + "created": 1733409253, + "id": "chatcmpl-Ab7DhFk7vSvmMW4ICIZh0gkvTZn7G", + "model": "gpt-4o-mini", "object": "chat.completion", "prompt_filter_results": [ { @@ -98,6 +111,10 @@ interactions: "filtered": false, "severity": "safe" }, + "jailbreak": { + "filtered": false, + "detected": false + }, "self_harm": { "filtered": false, "severity": "safe" @@ -113,48 +130,46 @@ interactions: } } ], - "system_fingerprint": null, + "system_fingerprint": "fp_04751d0b65", "usage": { - "completion_tokens": 2, + "completion_tokens": 4, "prompt_tokens": 24, - "total_tokens": 26 + "total_tokens": 28 } } headers: - Cache-Control: - - no-cache, must-revalidate Content-Length: - - '805' + - '997' Content-Type: - application/json Date: - - Wed, 13 Nov 2024 02:50:02 GMT + - Thu, 05 Dec 2024 14:34:13 GMT Set-Cookie: test_set_cookie Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload - access-control-allow-origin: - - '*' apim-request-id: - - f0e5ae5b-b609-4908-bedb-533ec71e9bfa + - ad6ebb52-6f0c-427c-b4cd-a186597cff93 azureml-model-session: - - d156-20241010120317 + - d029-20241115170135 openai-organization: test_openai_org_id x-accel-buffering: - 'no' x-content-type-options: - nosniff + x-envoy-upstream-service-time: + - '180' x-ms-client-request-id: - - f0e5ae5b-b609-4908-bedb-533ec71e9bfa + - ad6ebb52-6f0c-427c-b4cd-a186597cff93 x-ms-rai-invoked: - 'true' x-ms-region: - - Switzerland North + - East US x-ratelimit-remaining-requests: - - '78' + - '909' x-ratelimit-remaining-tokens: - - '79884' + - '90883' x-request-id: - - 3a1ee803-cce9-472f-ad04-2d0757009288 + - 80dd2ee4-7ce2-4d04-a114-efb137a58ed4 status: code: 200 message: OK diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_all_the_client_options[ollama_provider_chat_completions].yaml b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_all_the_client_options[ollama_provider_chat_completions].yaml index fb66019..e522dc4 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_all_the_client_options[ollama_provider_chat_completions].yaml +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_all_the_client_options[ollama_provider_chat_completions].yaml @@ -12,6 +12,11 @@ interactions: "frequency_penalty": 0, "max_tokens": 100, "presence_penalty": 0, + "response_format": { + "type": "text" + }, + "seed": 100, + "service_tier": "default", "stop": "foo", "temperature": 1, "top_p": 1 @@ -26,38 +31,38 @@ interactions: connection: - keep-alive content-length: - - '250' + - '327' content-type: - application/json host: - localhost:11434 user-agent: - - OpenAI/Python 1.50.2 + - OpenAI/Python 1.54.5 x-stainless-arch: - - arm64 + - x64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - - MacOS + - Linux x-stainless-package-version: - - 1.50.2 + - 1.54.5 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - - 3.12.6 + - 3.10.12 method: POST uri: http://localhost:11434/v1/chat/completions response: body: string: |- { - "id": "chatcmpl-46", + "id": "chatcmpl-593", "object": "chat.completion", - "created": 1731311779, + "created": 1733409255, "model": "qwen2.5:0.5b", "system_fingerprint": "fp_ollama", "choices": [ @@ -65,26 +70,26 @@ interactions: "index": 0, "message": { "role": "assistant", - "content": "The Falklands Islands are located in Atlantic Oceans." + "content": "Amalfis Sea" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 46, - "completion_tokens": 12, - "total_tokens": 58 + "completion_tokens": 5, + "total_tokens": 51 } } headers: Content-Length: - - '339' + - '297' Content-Type: - application/json Date: - - Mon, 11 Nov 2024 07:56:19 GMT + - Thu, 05 Dec 2024 14:34:15 GMT Set-Cookie: test_set_cookie - openai-organization: test_openai_org_key + openai-organization: test_openai_org_id status: code: 200 message: OK diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_all_the_client_options[openai_provider_chat_completions].yaml b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_all_the_client_options[openai_provider_chat_completions].yaml index 10ac250..bb5dcd0 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_all_the_client_options[openai_provider_chat_completions].yaml +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_all_the_client_options[openai_provider_chat_completions].yaml @@ -12,6 +12,11 @@ interactions: "frequency_penalty": 0, "max_tokens": 100, "presence_penalty": 0, + "response_format": { + "type": "text" + }, + "seed": 100, + "service_tier": "default", "stop": "foo", "temperature": 1, "top_p": 1 @@ -26,38 +31,38 @@ interactions: connection: - keep-alive content-length: - - '249' + - '326' content-type: - application/json host: - api.openai.com user-agent: - - OpenAI/Python 1.54.3 + - OpenAI/Python 1.54.5 x-stainless-arch: - - arm64 + - x64 x-stainless-async: - 'false' x-stainless-lang: - python x-stainless-os: - - MacOS + - Linux x-stainless-package-version: - - 1.54.3 + - 1.54.5 x-stainless-retry-count: - '0' x-stainless-runtime: - CPython x-stainless-runtime-version: - - 3.12.6 + - 3.10.12 method: POST uri: https://api.openai.com/v1/chat/completions response: body: string: |- { - "id": "chatcmpl-ASfa7XMQNQ9K4GKZSSrBU3zSmIyx5", + "id": "chatcmpl-Ab7DgqASxxcxoeuHYGRPqWaL6rJok", "object": "chat.completion", - "created": 1731396387, + "created": 1733409252, "model": "gpt-4o-mini-2024-07-18", "choices": [ { @@ -86,19 +91,20 @@ interactions: "rejected_prediction_tokens": 0 } }, - "system_fingerprint": "fp_0ba0d124f1" + "service_tier": "default", + "system_fingerprint": "fp_0705bf87c0" } headers: CF-Cache-Status: - DYNAMIC CF-RAY: - - 8e14cb399f01ce22-SIN + - 8ed4c16dee6a0e22-MXP Connection: - keep-alive Content-Type: - application/json Date: - - Tue, 12 Nov 2024 07:26:27 GMT + - Thu, 05 Dec 2024 14:34:12 GMT Server: - cloudflare Set-Cookie: test_set_cookie @@ -111,10 +117,10 @@ interactions: alt-svc: - h3=":443"; ma=86400 content-length: - - '771' - openai-organization: test_openai_org_key + - '800' + openai-organization: test_openai_org_id openai-processing-ms: - - '361' + - '691' openai-version: - '2020-10-01' strict-transport-security: @@ -124,15 +130,15 @@ interactions: x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - - '9998' + - '9999' x-ratelimit-remaining-tokens: - '199882' x-ratelimit-reset-requests: - - 16.631s + - 8.64s x-ratelimit-reset-tokens: - 35ms x-request-id: - - req_120179cc582bf2745ab0b81a089ea639 + - req_56da94fadb360ae462260ccc24d8eb62 status: code: 200 message: OK diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_stream_all_the_client_options[azure_provider_chat_completions].yaml b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_stream_all_the_client_options[azure_provider_chat_completions].yaml new file mode 100644 index 0000000..259ccc6 --- /dev/null +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_stream_all_the_client_options[azure_provider_chat_completions].yaml @@ -0,0 +1,117 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": "Answer in up to 3 words: Which ocean contains the falkland islands?" + } + ], + "model": "unused", + "frequency_penalty": 0, + "max_tokens": 100, + "presence_penalty": 0, + "response_format": { + "type": "text" + }, + "seed": 100, + "service_tier": "default", + "stop": "foo", + "stream": true, + "temperature": 1, + "top_p": 1 + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + api-key: + - test_azure_api_key + authorization: + - Bearer test_openai_api_key + connection: + - keep-alive + content-length: + - '337' + content-type: + - application/json + host: + - test.openai.azure.com + user-agent: + - AzureOpenAI/Python 1.54.5 + x-stainless-arch: + - x64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - Linux + x-stainless-package-version: + - 1.54.5 + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.10.12 + method: POST + uri: https://test.openai.azure.com/openai/deployments/test-azure-deployment/chat/completions?api-version=2024-08-01-preview + response: + body: + string: |+ + data: {"choices":[],"created":0,"id":"","model":"","object":"","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"jailbreak":{"filtered":false,"detected":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}]} + + data: {"choices":[{"content_filter_results":{},"delta":{"content":"","role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1733410752,"id":"chatcmpl-Ab7bsWDRtmzRV9yhkTN8fEPJW0Z8r","model":"gpt-4o-mini","object":"chat.completion.chunk","system_fingerprint":"fp_04751d0b65"} + + data: {"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":"South"},"finish_reason":null,"index":0,"logprobs":null}],"created":1733410752,"id":"chatcmpl-Ab7bsWDRtmzRV9yhkTN8fEPJW0Z8r","model":"gpt-4o-mini","object":"chat.completion.chunk","system_fingerprint":"fp_04751d0b65"} + + data: {"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":" Atlantic"},"finish_reason":null,"index":0,"logprobs":null}],"created":1733410752,"id":"chatcmpl-Ab7bsWDRtmzRV9yhkTN8fEPJW0Z8r","model":"gpt-4o-mini","object":"chat.completion.chunk","system_fingerprint":"fp_04751d0b65"} + + data: {"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":" Ocean"},"finish_reason":null,"index":0,"logprobs":null}],"created":1733410752,"id":"chatcmpl-Ab7bsWDRtmzRV9yhkTN8fEPJW0Z8r","model":"gpt-4o-mini","object":"chat.completion.chunk","system_fingerprint":"fp_04751d0b65"} + + data: {"choices":[{"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}},"delta":{"content":"."},"finish_reason":null,"index":0,"logprobs":null}],"created":1733410752,"id":"chatcmpl-Ab7bsWDRtmzRV9yhkTN8fEPJW0Z8r","model":"gpt-4o-mini","object":"chat.completion.chunk","system_fingerprint":"fp_04751d0b65"} + + data: {"choices":[{"content_filter_results":{},"delta":{},"finish_reason":"stop","index":0,"logprobs":null}],"created":1733410752,"id":"chatcmpl-Ab7bsWDRtmzRV9yhkTN8fEPJW0Z8r","model":"gpt-4o-mini","object":"chat.completion.chunk","system_fingerprint":"fp_04751d0b65"} + + data: [DONE] + + headers: + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 05 Dec 2024 14:59:12 GMT + Set-Cookie: test_set_cookie + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + apim-request-id: + - 46fd42a4-8ef2-4fc7-aca7-be2d8e2b2727 + azureml-model-session: + - d013-20241115150648 + openai-organization: test_openai_org_id + x-accel-buffering: + - 'no' + x-content-type-options: + - nosniff + x-envoy-upstream-service-time: + - '201' + x-ms-client-request-id: + - 46fd42a4-8ef2-4fc7-aca7-be2d8e2b2727 + x-ms-rai-invoked: + - 'true' + x-ms-region: + - East US + x-ratelimit-remaining-requests: + - '909' + x-ratelimit-remaining-tokens: + - '90883' + x-request-id: + - 5d6374e0-ef6e-41bf-b4f5-f42e9ac5cc93 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_stream_all_the_client_options[ollama_provider_chat_completions].yaml b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_stream_all_the_client_options[ollama_provider_chat_completions].yaml new file mode 100644 index 0000000..ee954c9 --- /dev/null +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_stream_all_the_client_options[ollama_provider_chat_completions].yaml @@ -0,0 +1,85 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": "Answer in up to 3 words: Which ocean contains the falkland islands?" + } + ], + "model": "qwen2.5:0.5b", + "frequency_penalty": 0, + "max_tokens": 100, + "presence_penalty": 0, + "response_format": { + "type": "text" + }, + "seed": 100, + "service_tier": "default", + "stop": "foo", + "stream": true, + "temperature": 1, + "top_p": 1 + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + authorization: + - Bearer test_openai_api_key + connection: + - keep-alive + content-length: + - '343' + content-type: + - application/json + host: + - localhost:11434 + user-agent: + - OpenAI/Python 1.54.5 + x-stainless-arch: + - x64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - Linux + x-stainless-package-version: + - 1.54.5 + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.10.12 + method: POST + uri: http://localhost:11434/v1/chat/completions + response: + body: + string: |+ + data: {"id":"chatcmpl-75","object":"chat.completion.chunk","created":1733410754,"model":"qwen2.5:0.5b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"Am"},"finish_reason":null}]} + + data: {"id":"chatcmpl-75","object":"chat.completion.chunk","created":1733410754,"model":"qwen2.5:0.5b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":"alfis"},"finish_reason":null}]} + + data: {"id":"chatcmpl-75","object":"chat.completion.chunk","created":1733410754,"model":"qwen2.5:0.5b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":" Sea"},"finish_reason":null}]} + + data: {"id":"chatcmpl-75","object":"chat.completion.chunk","created":1733410754,"model":"qwen2.5:0.5b","system_fingerprint":"fp_ollama","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":"stop"}]} + + data: [DONE] + + headers: + Content-Type: + - text/event-stream + Date: + - Thu, 05 Dec 2024 14:59:14 GMT + Set-Cookie: test_set_cookie + Transfer-Encoding: + - chunked + openai-organization: test_openai_org_id + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_stream_all_the_client_options[openai_provider_chat_completions].yaml b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_stream_all_the_client_options[openai_provider_chat_completions].yaml new file mode 100644 index 0000000..96e6cc9 --- /dev/null +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/.test_chat_completions/test_stream_all_the_client_options[openai_provider_chat_completions].yaml @@ -0,0 +1,123 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": "Answer in up to 3 words: Which ocean contains the falkland islands?" + } + ], + "model": "gpt-4o-mini", + "frequency_penalty": 0, + "max_tokens": 100, + "presence_penalty": 0, + "response_format": { + "type": "text" + }, + "seed": 100, + "service_tier": "default", + "stop": "foo", + "stream": true, + "temperature": 1, + "top_p": 1 + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + authorization: + - Bearer test_openai_api_key + connection: + - keep-alive + content-length: + - '342' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.54.5 + x-stainless-arch: + - x64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - Linux + x-stainless-package-version: + - 1.54.5 + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.10.12 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: |+ + data: {"id":"chatcmpl-Ab7br3ArYb5ZSjD5Z4ujJO3zlnmU6","object":"chat.completion.chunk","created":1733410751,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_0705bf87c0","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-Ab7br3ArYb5ZSjD5Z4ujJO3zlnmU6","object":"chat.completion.chunk","created":1733410751,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_0705bf87c0","choices":[{"index":0,"delta":{"content":"South"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-Ab7br3ArYb5ZSjD5Z4ujJO3zlnmU6","object":"chat.completion.chunk","created":1733410751,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_0705bf87c0","choices":[{"index":0,"delta":{"content":" Atlantic"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-Ab7br3ArYb5ZSjD5Z4ujJO3zlnmU6","object":"chat.completion.chunk","created":1733410751,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_0705bf87c0","choices":[{"index":0,"delta":{"content":" Ocean"},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-Ab7br3ArYb5ZSjD5Z4ujJO3zlnmU6","object":"chat.completion.chunk","created":1733410751,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_0705bf87c0","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}]} + + data: {"id":"chatcmpl-Ab7br3ArYb5ZSjD5Z4ujJO3zlnmU6","object":"chat.completion.chunk","created":1733410751,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_0705bf87c0","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} + + data: [DONE] + + headers: + CF-Cache-Status: + - DYNAMIC + CF-RAY: + - 8ed4e60e7a38374c-MXP + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 05 Dec 2024 14:59:12 GMT + Server: + - cloudflare + Set-Cookie: test_set_cookie + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + openai-organization: test_openai_org_id + openai-processing-ms: + - '176' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '199881' + x-ratelimit-reset-requests: + - 8.64s + x-ratelimit-reset-tokens: + - 35ms + x-request-id: + - req_db36c32e7855b5b084ee81e6ba58691f + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/test_chat_completions.py b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/test_chat_completions.py index 57883e8..47cc2c8 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/test_chat_completions.py +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/test_chat_completions.py @@ -26,6 +26,10 @@ from opentelemetry._logs import LogRecord from opentelemetry.instrumentation.openai import OpenAIInstrumentor from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import ( + GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT, + GEN_AI_OPENAI_REQUEST_SEED, + GEN_AI_OPENAI_REQUEST_SERVICE_TIER, + GEN_AI_OPENAI_RESPONSE_SERVICE_TIER, GEN_AI_OPERATION_NAME, GEN_AI_REQUEST_FREQUENCY_PENALTY, GEN_AI_REQUEST_MAX_TOKENS, @@ -170,7 +174,7 @@ def test_basic( "gpt-4o-mini", "gpt-4o-mini-2024-07-18", "South Atlantic Ocean.", - "chatcmpl-ASfa7XMQNQ9K4GKZSSrBU3zSmIyx5", + "chatcmpl-Ab7DgqASxxcxoeuHYGRPqWaL6rJok", 24, 4, 0.006761051714420319, @@ -178,26 +182,27 @@ def test_basic( ( "azure_provider_chat_completions", "unused", - "gpt-4-32k", - "Atlantic Ocean", - "chatcmpl-ASxkBZGOa53uXX1Ciygl77IrF8PbB", + "gpt-4o-mini", + "South Atlantic Ocean.", + "chatcmpl-Ab7DhFk7vSvmMW4ICIZh0gkvTZn7G", 24, - 2, + 4, 0.002889830619096756, ), ( "ollama_provider_chat_completions", "qwen2.5:0.5b", "qwen2.5:0.5b", - "The Falklands Islands are located in Atlantic Oceans.", - "chatcmpl-46", + "Amalfis Sea", + "chatcmpl-593", 46, - 12, + 5, 0.002600736916065216, ), ] +@pytest.mark.skipif(OPENAI_VERSION < (1, 35, 0), reason="service tieri added in 1.35.0") @pytest.mark.vcr() @pytest.mark.parametrize( "provider_str,model,response_model,content,response_id,input_tokens,output_tokens,duration", @@ -236,6 +241,9 @@ def test_all_the_client_options( temperature=1, top_p=1, stop="foo", + seed=100, + service_tier="default", + response_format={"type": "text"}, ) assert chat_completion.choices[0].message.content == content @@ -248,7 +256,11 @@ def test_all_the_client_options( assert span.kind == SpanKind.CLIENT assert span.status.status_code == StatusCode.UNSET - assert dict(span.attributes) == { + expected_attrs = { + GEN_AI_OPENAI_REQUEST_SEED: 100, + GEN_AI_OPENAI_REQUEST_SERVICE_TIER: "default", + GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT: "text", + GEN_AI_OPENAI_RESPONSE_SERVICE_TIER: "default", GEN_AI_OPERATION_NAME: "chat", GEN_AI_REQUEST_FREQUENCY_PENALTY: 0, GEN_AI_REQUEST_MAX_TOKENS: 100, @@ -266,6 +278,9 @@ def test_all_the_client_options( SERVER_ADDRESS: provider.server_address, SERVER_PORT: provider.server_port, } + if provider_str != "openai_provider_chat_completions": + del expected_attrs[GEN_AI_OPENAI_RESPONSE_SERVICE_TIER] + assert dict(span.attributes) == expected_attrs logs = logs_exporter.get_finished_logs() assert len(logs) == 0 @@ -1190,6 +1205,133 @@ def test_stream( ) +test_stream_all_the_client_options_test_data = [ + ( + "openai_provider_chat_completions", + "gpt-4o-mini", + "gpt-4o-mini-2024-07-18", + "South Atlantic Ocean.", + "chatcmpl-Ab7br3ArYb5ZSjD5Z4ujJO3zlnmU6", + 24, + 4, + 0.006761051714420319, + ), + ( + "azure_provider_chat_completions", + "unused", + "gpt-4o-mini", + "South Atlantic Ocean.", + "chatcmpl-Ab7bsWDRtmzRV9yhkTN8fEPJW0Z8r", + 24, + 4, + 0.002889830619096756, + ), + ( + "ollama_provider_chat_completions", + "qwen2.5:0.5b", + "qwen2.5:0.5b", + "Amalfis Sea", + "chatcmpl-75", + 46, + 5, + 0.002600736916065216, + ), +] + + +@pytest.mark.skipif(OPENAI_VERSION < (1, 35, 0), reason="service tier added in 1.35.0") +@pytest.mark.vcr() +@pytest.mark.parametrize( + "provider_str,model,response_model,content,response_id,input_tokens,output_tokens,duration", + test_stream_all_the_client_options_test_data, +) +def test_stream_all_the_client_options( + provider_str, + model, + response_model, + content, + response_id, + input_tokens, + output_tokens, + duration, + trace_exporter, + metrics_reader, + logs_exporter, + request, +): + provider = request.getfixturevalue(provider_str) + client = provider.get_client() + + messages = [ + { + "role": "user", + "content": "Answer in up to 3 words: Which ocean contains the falkland islands?", + } + ] + + chat_completion = client.chat.completions.create( + model=model, + messages=messages, + frequency_penalty=0, + max_tokens=100, # AzureOpenAI still does not support max_completions_tokens + presence_penalty=0, + temperature=1, + top_p=1, + stop="foo", + seed=100, + service_tier="default", + response_format={"type": "text"}, + stream=True, + ) + + chunks = [chunk.choices[0].delta.content or "" for chunk in chat_completion if chunk.choices] + assert "".join(chunks) == content + + spans = trace_exporter.get_finished_spans() + assert len(spans) == 1 + + span = spans[0] + assert span.name == f"chat {model}" + assert span.kind == SpanKind.CLIENT + assert span.status.status_code == StatusCode.UNSET + + expected_attrs = { + GEN_AI_OPENAI_REQUEST_SEED: 100, + GEN_AI_OPENAI_REQUEST_SERVICE_TIER: "default", + GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT: "text", + GEN_AI_OPENAI_RESPONSE_SERVICE_TIER: "default", + GEN_AI_OPERATION_NAME: "chat", + GEN_AI_REQUEST_FREQUENCY_PENALTY: 0, + GEN_AI_REQUEST_MAX_TOKENS: 100, + GEN_AI_REQUEST_MODEL: model, + GEN_AI_REQUEST_PRESENCE_PENALTY: 0, + GEN_AI_REQUEST_STOP_SEQUENCES: ("foo",), + GEN_AI_REQUEST_TEMPERATURE: 1, + GEN_AI_REQUEST_TOP_P: 1, + GEN_AI_SYSTEM: "openai", + GEN_AI_RESPONSE_ID: response_id, + GEN_AI_RESPONSE_MODEL: response_model, + GEN_AI_RESPONSE_FINISH_REASONS: ("stop",), + SERVER_ADDRESS: provider.server_address, + SERVER_PORT: provider.server_port, + } + if provider_str != "openai_provider_chat_completions": + del expected_attrs[GEN_AI_OPENAI_RESPONSE_SERVICE_TIER] + assert dict(span.attributes) == expected_attrs + + logs = logs_exporter.get_finished_logs() + assert len(logs) == 0 + + (operation_duration_metric,) = get_sorted_metrics(metrics_reader) + attributes = { + GEN_AI_REQUEST_MODEL: model, + GEN_AI_RESPONSE_MODEL: response_model, + } + assert_operation_duration_metric( + provider, operation_duration_metric, attributes=attributes, min_data_point=duration + ) + + # FIXME: add custom ollama test_stream_with_include_usage_option_test_data = [ (