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 98d862f..82173a2 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 @@ -25,6 +25,7 @@ GEN_AI_OPENAI_REQUEST_SERVICE_TIER, GEN_AI_OPENAI_RESPONSE_SERVICE_TIER, GEN_AI_OPERATION_NAME, + GEN_AI_REQUEST_CHOICE_COUNT, GEN_AI_REQUEST_FREQUENCY_PENALTY, GEN_AI_REQUEST_MAX_TOKENS, GEN_AI_REQUEST_MODEL, @@ -141,6 +142,8 @@ def _is_set(value): if client := getattr(instance, "_client", None): span_attributes.update(_attributes_from_client(client)) + if _is_set(choice_count := kwargs.get("n")) and choice_count != 1: + span_attributes[GEN_AI_REQUEST_CHOICE_COUNT] = choice_count if _is_set(frequency_penalty := kwargs.get("frequency_penalty")): span_attributes[GEN_AI_REQUEST_FREQUENCY_PENALTY] = frequency_penalty if _is_set(max_tokens := kwargs.get("max_completion_tokens", kwargs.get("max_tokens"))): diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/test_chat_n_1.yaml b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/test_chat_n_1.yaml new file mode 100644 index 0000000..ded2949 --- /dev/null +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/cassettes/test_chat_n_1.yaml @@ -0,0 +1,138 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": "Answer in up to 3 words: Which ocean contains Bouvet Island?" + } + ], + "model": "gpt-4o-mini", + "n": 1 + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + authorization: + - Bearer test_openai_api_key + connection: + - keep-alive + content-length: + - '139' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.66.5 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.66.5 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.0 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: |- + { + "id": "chatcmpl-BL8K8arBjCHMDOxqQd5YGBeYphZGG", + "object": "chat.completion", + "created": 1744376584, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Atlantic Ocean.", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 22, + "completion_tokens": 5, + "total_tokens": 27, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_44added55e" + } + headers: + CF-RAY: + - 92eaae915b15e51d-TXL + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 11 Apr 2025 13:03:04 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 + cf-cache-status: + - DYNAMIC + content-length: + - '827' + openai-organization: test_openai_org_id + openai-processing-ms: + - '170' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-ratelimit-limit-requests: + - '200' + x-ratelimit-limit-tokens: + - '100000' + x-ratelimit-remaining-requests: + - '197' + x-ratelimit-remaining-tokens: + - '99925' + x-ratelimit-reset-requests: + - 19m29.225s + x-ratelimit-reset-tokens: + - 32m9.545s + x-request-id: + - req_5ec52b920fea0d555ec3dbf813300fad + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/test_beta_chat_completions.py b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/test_beta_chat_completions.py index 383327a..91b775a 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/test_beta_chat_completions.py +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/test_beta_chat_completions.py @@ -33,6 +33,7 @@ GEN_AI_OPENAI_REQUEST_SERVICE_TIER, GEN_AI_OPENAI_RESPONSE_SERVICE_TIER, GEN_AI_OPERATION_NAME, + GEN_AI_REQUEST_CHOICE_COUNT, GEN_AI_REQUEST_FREQUENCY_PENALTY, GEN_AI_REQUEST_MAX_TOKENS, GEN_AI_REQUEST_MODEL, @@ -330,6 +331,7 @@ def test_chat_multiple_choices_with_capture_message_content( address, port = address_and_port(client) assert dict(span.attributes) == { GEN_AI_OPERATION_NAME: "chat", + GEN_AI_REQUEST_CHOICE_COUNT: 2, GEN_AI_REQUEST_MODEL: TEST_CHAT_MODEL, GEN_AI_SYSTEM: "openai", GEN_AI_RESPONSE_ID: "chatcmpl-AfhuHpVEbcYGlsFuHOP60MtU4tIq9", 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 5ce1a35..6a68e19 100644 --- a/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/test_chat_completions.py +++ b/instrumentation/elastic-opentelemetry-instrumentation-openai/tests/test_chat_completions.py @@ -32,6 +32,7 @@ GEN_AI_OPENAI_REQUEST_SERVICE_TIER, GEN_AI_OPENAI_RESPONSE_SERVICE_TIER, GEN_AI_OPERATION_NAME, + GEN_AI_REQUEST_CHOICE_COUNT, GEN_AI_REQUEST_FREQUENCY_PENALTY, GEN_AI_REQUEST_MAX_TOKENS, GEN_AI_REQUEST_MODEL, @@ -130,6 +131,28 @@ def test_chat(default_openai_env, trace_exporter, metrics_reader, logs_exporter) ) +@pytest.mark.vcr() +def test_chat_n_1(default_openai_env, trace_exporter, metrics_reader, logs_exporter): + client = openai.OpenAI() + + messages = [ + { + "role": "user", + "content": TEST_CHAT_INPUT, + } + ] + + chat_completion = client.chat.completions.create(model=TEST_CHAT_MODEL, messages=messages, n=1) + + assert chat_completion.choices[0].message.content == "Atlantic Ocean." + + spans = trace_exporter.get_finished_spans() + assert len(spans) == 1 + + span = spans[0] + assert GEN_AI_REQUEST_CHOICE_COUNT not in span.attributes + + @pytest.mark.skipif(OPENAI_VERSION < (1, 8, 0), reason="LegacyAPIResponse available") @pytest.mark.vcr() def test_chat_with_raw_response(default_openai_env, trace_exporter, metrics_reader, logs_exporter): @@ -471,6 +494,7 @@ def test_chat_multiple_choices_with_capture_message_content( address, port = address_and_port(client) assert dict(span.attributes) == { GEN_AI_OPERATION_NAME: "chat", + GEN_AI_REQUEST_CHOICE_COUNT: 2, GEN_AI_REQUEST_MODEL: TEST_CHAT_MODEL, GEN_AI_SYSTEM: "openai", GEN_AI_RESPONSE_ID: "chatcmpl-AfhuHpVEbcYGlsFuHOP60MtU4tIq9",