Repository: traceloop/openllmetry

Issue:
🐛 Bug Report: OpenAI instrumentation fails when sending a response message from a previous call
### Which component is this bug for?

OpenAI Instrumentation

### 📜 Description

OpenAI docs and the SDK allow passing the message directly from a previous response to a subsequent call. We don't cast those messages to dict, so instrumentation fails to record messages past the error

### 👟 Reproduction steps

```python
first_user_message = {
    "role": "user",
    "content": "Generate a random noun in French. Respond with just that word.",
}
second_user_message = {
    "role": "user",
    "content": "Now, generate a sentence using the word you just gave me.",
}
first_response = openai_client.chat.completions.create(
    model="gpt-4.1-nano",
    messages=[first_user_message],
)

second_response = openai_client.chat.completions.create(
    model="gpt-4.1-nano",
    messages=[
        first_user_message,
        first_response.choices[0].message,
        second_user_message,
    ],
)
```

### 👍 Expected behavior

The following attributes must be set

```
gen_ai.prompts.0.content
gen_ai.prompts.0.role
gen_ai.prompts.1.content
gen_ai.prompts.1.role
gen_ai.prompts.2.content
gen_ai.prompts.2.role
```

### 👎 Actual Behavior with Screenshots

Only the following attributes are set

```
gen_ai.prompts.0.content
gen_ai.prompts.0.role
```

### 🤖 Python Version

3.13

### 📃 Provide any additional context for the Bug.

It actually works with raw dicts

```python
first_user_message = {
    "role": "user",
    "content": "Generate a random noun in French. Respond with just that word.",
}
second_user_message = {
    "role": "user",
    "content": "Now, generate a sentence using the word you just gave me.",
}
first_response = openai_client.chat.completions.create(
    model="gpt-4.1-nano",
    messages=[first_user_message],
)

second_response = openai_client.chat.completions.create(
    model="gpt-4.1-nano",
    messages=[
        first_user_message,
        {
            "role": assistant,
            "content": first_response.choices[0].message.content
        },
        second_user_message,
    ],
)
```

### 👀 Have you spent some time to check if this bug has been raised before?

- [x] I checked and didn't find similar issue

### Are you willing to submit PR?

Yes I am willing to submit a PR!


Discussion:

Patch:
diff --git a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py
index cfc479956d..f9b4fbc6d2 100644
--- a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py
+++ b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py
@@ -369,6 +369,7 @@ async def _set_prompts(span, messages):
 
     for i, msg in enumerate(messages):
         prefix = f"{SpanAttributes.LLM_PROMPTS}.{i}"
+        msg = msg if isinstance(msg, dict) else model_as_dict(msg)
 
         _set_span_attribute(span, f"{prefix}.role", msg.get("role"))
         if msg.get("content"):


Test patch:
diff --git a/packages/opentelemetry-instrumentation-openai/tests/traces/cassettes/test_chat/test_chat_history_message_dict.yaml b/packages/opentelemetry-instrumentation-openai/tests/traces/cassettes/test_chat/test_chat_history_message_dict.yaml
new file mode 100644
index 0000000000..23a22023e0
--- /dev/null
+++ b/packages/opentelemetry-instrumentation-openai/tests/traces/cassettes/test_chat/test_chat_history_message_dict.yaml
@@ -0,0 +1,212 @@
+interactions:
+- request:
+    body: '{"messages": [{"role": "user", "content": "Generate a random noun in Korean.
+      Respond with just that word."}], "model": "gpt-4.1-nano"}'
+    headers:
+      accept:
+      - application/json
+      accept-encoding:
+      - gzip, deflate
+      connection:
+      - keep-alive
+      content-length:
+      - '134'
+      content-type:
+      - application/json
+      host:
+      - api.openai.com
+      traceparent:
+      - 00-50385c6266930a3c2571ae06547d3d66-bb7cf190452daec9-01
+      user-agent:
+      - OpenAI/Python 1.51.2
+      x-stainless-arch:
+      - arm64
+      x-stainless-async:
+      - 'false'
+      x-stainless-lang:
+      - python
+      x-stainless-os:
+      - MacOS
+      x-stainless-package-version:
+      - 1.51.2
+      x-stainless-retry-count:
+      - '0'
+      x-stainless-runtime:
+      - CPython
+      x-stainless-runtime-version:
+      - 3.9.6
+    method: POST
+    uri: https://api.openai.com/v1/chat/completions
+  response:
+    body:
+      string: !!binary |
+        H4sIAAAAAAAAAwAAAP//jJJNbtswEIX3OgUxayuwLKWJvUs23bQoUHjRoggEmhzJTCgOQY6CFIFP
+        0G2P0AP0XrlEQflHctsA2Wgx37zhe6N5zoQAo2ElQG0lq87b/La5WofPj09cfnrHuMby/eXNuth8
+        2Hz0X7/ALCloc4+Kj6oLRZ23yIbcHquAkjFNLa6qZVEul/NyAB1ptEnWes6riyJ30lG+mC8u83mV
+        F9VBviWjMMJKfMuEEOJ5+CajTuMTrMR8dqx0GKNsEVanJiEgkE0VkDGayNIxzEaoyDG6wfvL758v
+        P35NYcCmjzI5dL21EyCdI5Yp4WDr7kB2JyOWWh9oE/+SQmOcids6oIzk0qORycNAd5kQd0Pg/iwD
+        +ECd55rpAYfnFoe8MO55Ag+MiaWdlI/1s2G1RpbGxsm+QEm1RT0qx+XKXhuagGwS+V8v/5u9j21c
+        +5bxI1AKPaOufUBt1HnesS1gOsLX2k4rHgxDxPBoFNZsMKTfoLGRvd1fBsTvkbGrG+NaDD6Y/Xk0
+        vi6vy6qUi+a6gWyX/QEAAP//AwB0yAt+LQMAAA==
+    headers:
+      CF-RAY:
+      - 94b0f287caba4428-LHR
+      Connection:
+      - keep-alive
+      Content-Encoding:
+      - gzip
+      Content-Type:
+      - application/json
+      Date:
+      - Thu, 05 Jun 2025 16:11:43 GMT
+      Server:
+      - cloudflare
+      Set-Cookie:
+      - __cf_bm=O9eAUpHGiHGrhiQetabnRwb5V_aaS9fmKt5mZA87bck-1749139903-1.0.1.1-T_6g4yzo0VSmeacWjX_XWznTeFpzZp1eH2c1my1gnQbigjPg.5O0cfciYSHV5yPw6M4QtnD7W_zYMy32piI1Tl9hrsNGDlCYA.0m8NiGUo4;
+        path=/; expires=Thu, 05-Jun-25 16:41:43 GMT; domain=.api.openai.com; HttpOnly;
+        Secure; SameSite=None
+      - _cfuvid=JHo08JDcm1asanqe2mjXa5aF_Xl_UlmIQQmV15ZbM28-1749139903524-0.0.1.1-604800000;
+        path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
+      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
+      openai-organization:
+      - user-xzaeeoqlanzncr8pomspsknu
+      openai-processing-ms:
+      - '201'
+      openai-version:
+      - '2020-10-01'
+      strict-transport-security:
+      - max-age=31536000; includeSubDomains; preload
+      x-envoy-upstream-service-time:
+      - '205'
+      x-ratelimit-limit-requests:
+      - '30000'
+      x-ratelimit-limit-tokens:
+      - '150000000'
+      x-ratelimit-remaining-requests:
+      - '29999'
+      x-ratelimit-remaining-tokens:
+      - '149999982'
+      x-ratelimit-reset-requests:
+      - 2ms
+      x-ratelimit-reset-tokens:
+      - 0s
+      x-request-id:
+      - req_cffda49c63c2b346e62f864e91161d3a
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"messages": [{"role": "user", "content": "Generate a random noun in Korean.
+      Respond with just that word."}, {"role": "assistant", "content": "\ubc14\ub2e4"},
+      {"role": "user", "content": "Now, generate a sentence using the word you just
+      gave me."}], "model": "gpt-4.1-nano"}'
+    headers:
+      accept:
+      - application/json
+      accept-encoding:
+      - gzip, deflate
+      connection:
+      - keep-alive
+      content-length:
+      - '274'
+      content-type:
+      - application/json
+      cookie:
+      - __cf_bm=O9eAUpHGiHGrhiQetabnRwb5V_aaS9fmKt5mZA87bck-1749139903-1.0.1.1-T_6g4yzo0VSmeacWjX_XWznTeFpzZp1eH2c1my1gnQbigjPg.5O0cfciYSHV5yPw6M4QtnD7W_zYMy32piI1Tl9hrsNGDlCYA.0m8NiGUo4;
+        _cfuvid=JHo08JDcm1asanqe2mjXa5aF_Xl_UlmIQQmV15ZbM28-1749139903524-0.0.1.1-604800000
+      host:
+      - api.openai.com
+      traceparent:
+      - 00-7ac5a7feb3b991b4bfe1e3a7aad93ccd-d81d0e2473995f47-01
+      user-agent:
+      - OpenAI/Python 1.51.2
+      x-stainless-arch:
+      - arm64
+      x-stainless-async:
+      - 'false'
+      x-stainless-lang:
+      - python
+      x-stainless-os:
+      - MacOS
+      x-stainless-package-version:
+      - 1.51.2
+      x-stainless-retry-count:
+      - '0'
+      x-stainless-runtime:
+      - CPython
+      x-stainless-runtime-version:
+      - 3.9.6
+    method: POST
+    uri: https://api.openai.com/v1/chat/completions
+  response:
+    body:
+      string: !!binary |
+        H4sIAAAAAAAAAwAAAP//jFI9j9QwFOzzKyzXm1Wym7AfJaKGhgahU+S1X7IGxzb2Cwd32gZRce2e
+        hCjR0VNQ8o+y9x9OTvY2OTgkmhRv3kxmxu8yIoRKQdeE8i1DXlsVPy0XL7GqlssL8fyZvMjP5+82
+        L+BVY/K6OaeTwDCbN8DxnjXlprYKUBrdw9wBQwiq6SJbpfPVKsk7oDYCVKBVFuNsmsaaaRPPklke
+        J1mcZkf61kgOnq7J64gQQi67bzCqBXyga5JM7ic1eM8qoOvTEiHUGRUmlHkvPTKNdDKA3GgE3Xlv
+        P31tv+xJ+3PfXt20P36Tw/erw/Xn2+tfh2/76ZjkoGw8C851o9QIYFobZCF5Z/fsiOxOBpWprDMb
+        /weVllJLvy0cMG90MOPRWNqhu4iQs66I5kE2ap2pLRZo3kL3u2zey9Gh/wFcHjE0yNQwztPJI2KF
+        AGRS+VGPlDO+BTEwh9JZI6QZAdEo8t9eHtPuY0td/Y/8AHAOFkEU1oGQ/GHeYc1BOM5/rZ0q7gxT
+        D+695FCgBBeeQUDJGtVfDPUfPUJdlFJX4KyT/dmUtijTWfpksZkvEhrtojsAAAD//wMA5dqzDkUD
+        AAA=
+    headers:
+      CF-RAY:
+      - 94b0f28d8eef4428-LHR
+      Connection:
+      - keep-alive
+      Content-Encoding:
+      - gzip
+      Content-Type:
+      - application/json
+      Date:
+      - Thu, 05 Jun 2025 16:11:45 GMT
+      Server:
+      - cloudflare
+      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
+      openai-organization:
+      - user-xzaeeoqlanzncr8pomspsknu
+      openai-processing-ms:
+      - '299'
+      openai-version:
+      - '2020-10-01'
+      strict-transport-security:
+      - max-age=31536000; includeSubDomains; preload
+      x-envoy-upstream-service-time:
+      - '304'
+      x-ratelimit-limit-requests:
+      - '30000'
+      x-ratelimit-limit-tokens:
+      - '150000000'
+      x-ratelimit-remaining-requests:
+      - '29999'
+      x-ratelimit-remaining-tokens:
+      - '149999963'
+      x-ratelimit-reset-requests:
+      - 2ms
+      x-ratelimit-reset-tokens:
+      - 0s
+      x-request-id:
+      - req_1989c72bc545874e463ac5ed5c4d3595
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/packages/opentelemetry-instrumentation-openai/tests/traces/cassettes/test_chat/test_chat_history_message_pydantic.yaml b/packages/opentelemetry-instrumentation-openai/tests/traces/cassettes/test_chat/test_chat_history_message_pydantic.yaml
new file mode 100644
index 0000000000..075515c80c
--- /dev/null
+++ b/packages/opentelemetry-instrumentation-openai/tests/traces/cassettes/test_chat/test_chat_history_message_pydantic.yaml
@@ -0,0 +1,212 @@
+interactions:
+- request:
+    body: '{"messages": [{"role": "user", "content": "Generate a random noun in Korean.
+      Respond with just that word."}], "model": "gpt-4.1-nano"}'
+    headers:
+      accept:
+      - application/json
+      accept-encoding:
+      - gzip, deflate
+      connection:
+      - keep-alive
+      content-length:
+      - '134'
+      content-type:
+      - application/json
+      host:
+      - api.openai.com
+      traceparent:
+      - 00-6978a6193c92e3731880e853960f7f67-3041d13db03a84d1-01
+      user-agent:
+      - OpenAI/Python 1.51.2
+      x-stainless-arch:
+      - arm64
+      x-stainless-async:
+      - 'false'
+      x-stainless-lang:
+      - python
+      x-stainless-os:
+      - MacOS
+      x-stainless-package-version:
+      - 1.51.2
+      x-stainless-retry-count:
+      - '0'
+      x-stainless-runtime:
+      - CPython
+      x-stainless-runtime-version:
+      - 3.9.6
+    method: POST
+    uri: https://api.openai.com/v1/chat/completions
+  response:
+    body:
+      string: !!binary |
+        H4sIAAAAAAAAAwAAAP//jJJNbtswEIX3OgUxayuQHCeGvCwCJM2mRVGgiyIQaHIkM6E4BDkqHATe
+        9BQ9RJe9VS5RUP6R3B8gGy3mmzd8bzQvmRBgNKwEqI1k1Xmbv2uWn3v36b7H2y+Vvrr5UL3XgT+G
+        u/vn7RZmSUHrR1R8VF0o6rxFNuT2WAWUjGlquVxU5WVVFdcD6EijTbLWc764KHMnHeXzYn6VF4u8
+        XBzkGzIKI6zE10wIIV6GbzLqNG5hJYrZsdJhjLJFWJ2ahIBANlVAxmgiS8cwG6Eix+gG76/ff7z+
+        /DWFAZs+yuTQ9dZOgHSOWKaEg62HA9mdjFhqfaB1/EMKjXEmbuqAMpJLj0YmDwPdZUI8DIH7swzg
+        A3Wea6YnHJ6bH/LCuOcJPDAmlnZSPtbPhtUaWRobJ/sCJdUG9agclyt7bWgCsknkv738a/Y+tnHt
+        W8aPQCn0jLr2AbVR53nHtoDpCP/XdlrxYBgihm9GYc0GQ/oNGhvZ2/1lQHyOjF3dGNdi8MHsz6Px
+        dVPOy+vl+nJZQLbLfgMAAP//AwD7osyqLQMAAA==
+    headers:
+      CF-RAY:
+      - 94b0f29a7bfc76fc-LHR
+      Connection:
+      - keep-alive
+      Content-Encoding:
+      - gzip
+      Content-Type:
+      - application/json
+      Date:
+      - Thu, 05 Jun 2025 16:11:47 GMT
+      Server:
+      - cloudflare
+      Set-Cookie:
+      - __cf_bm=GJDapSv.2zGYBXraKMhjWywBu.Q0T1faqZ19Ma2BY_o-1749139907-1.0.1.1-Xy1Ciy0YSpQbk2el1TpIcZv6_HG8xw8HErV.lIkt.NIEMt71jjodhBCkoCRdJyXnfRHs3xYmM0.JXFnH.o1KwQtzMUTl8o.3.UG9llqBd6o;
+        path=/; expires=Thu, 05-Jun-25 16:41:47 GMT; domain=.api.openai.com; HttpOnly;
+        Secure; SameSite=None
+      - _cfuvid=sLuRMjHdZaHei0wCbTX5NmtG8_Y5OaExqQEK13SnCrI-1749139907284-0.0.1.1-604800000;
+        path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
+      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
+      openai-organization:
+      - user-xzaeeoqlanzncr8pomspsknu
+      openai-processing-ms:
+      - '398'
+      openai-version:
+      - '2020-10-01'
+      strict-transport-security:
+      - max-age=31536000; includeSubDomains; preload
+      x-envoy-upstream-service-time:
+      - '421'
+      x-ratelimit-limit-requests:
+      - '30000'
+      x-ratelimit-limit-tokens:
+      - '150000000'
+      x-ratelimit-remaining-requests:
+      - '29999'
+      x-ratelimit-remaining-tokens:
+      - '149999982'
+      x-ratelimit-reset-requests:
+      - 2ms
+      x-ratelimit-reset-tokens:
+      - 0s
+      x-request-id:
+      - req_b54fe4fb1b7083ea8187bbf16e7c2784
+    status:
+      code: 200
+      message: OK
+- request:
+    body: '{"messages": [{"role": "user", "content": "Generate a random noun in Korean.
+      Respond with just that word."}, {"content": "\ub098\ubb34", "refusal": null,
+      "role": "assistant", "annotations": []}, {"role": "user", "content": "Now, generate
+      a sentence using the word you just gave me."}], "model": "gpt-4.1-nano"}'
+    headers:
+      accept:
+      - application/json
+      accept-encoding:
+      - gzip, deflate
+      connection:
+      - keep-alive
+      content-length:
+      - '310'
+      content-type:
+      - application/json
+      cookie:
+      - __cf_bm=GJDapSv.2zGYBXraKMhjWywBu.Q0T1faqZ19Ma2BY_o-1749139907-1.0.1.1-Xy1Ciy0YSpQbk2el1TpIcZv6_HG8xw8HErV.lIkt.NIEMt71jjodhBCkoCRdJyXnfRHs3xYmM0.JXFnH.o1KwQtzMUTl8o.3.UG9llqBd6o;
+        _cfuvid=sLuRMjHdZaHei0wCbTX5NmtG8_Y5OaExqQEK13SnCrI-1749139907284-0.0.1.1-604800000
+      host:
+      - api.openai.com
+      traceparent:
+      - 00-841bbdaedae214ebae7eb1a59eac87ad-982db3b55a9e6d2a-01
+      user-agent:
+      - OpenAI/Python 1.51.2
+      x-stainless-arch:
+      - arm64
+      x-stainless-async:
+      - 'false'
+      x-stainless-lang:
+      - python
+      x-stainless-os:
+      - MacOS
+      x-stainless-package-version:
+      - 1.51.2
+      x-stainless-retry-count:
+      - '0'
+      x-stainless-runtime:
+      - CPython
+      x-stainless-runtime-version:
+      - 3.9.6
+    method: POST
+    uri: https://api.openai.com/v1/chat/completions
+  response:
+    body:
+      string: !!binary |
+        H4sIAAAAAAAAA4xSwW7UMBC95yssnzerzW7asHuk6hEhJJCgqIq89iTrktiW7VRU1R5Ai1TBpYei
+        VoJWHHrsAVQQXPihTfgH5GS7SaFIXHyYN+/5vZk59BDCnOEJwnRGLM1V5t9Posf7D0bsydNnBzvT
+        rS2zR/mOvrdttsnDR7jnGHK6B9TesPpU5ioDy6VoYKqBWHCqQRSOg9F4PIhqIJcMMkdLlfXDfuAL
+        IqQ/HAw3/EHoB+GKPpOcgsET9NxDCKHD+nVGBYOXeIIGvZtKDsaQFPBk3YQQ1jJzFUyM4cYSYXGv
+        BakUFkTtffn9R/n2BC2vv1UfjqvT42rxEf169RmVr8/Kq6+oer8oL85W9erLm+p8garzn8vrT6i6
+        OKpOj8p3l/2utIakMMTlE0WWdQAihLTEzacOtbtC5usYmUyVllPzBxUnXHAzizUQI4WzbKxUuEbn
+        HkK79biKWxPASstc2djKF1B/F44aOdxuqQWDzRVopSVZW98Y9+5QixlYwjPTGTemhM6Atcx2N6Rg
+        XHYAr5P5bzN3aTe5uUj/R74FKAVlgcVKA+P0duC2TYO74X+1rWdcG8YG9D6nEFsO2u2BQUKKrDks
+        bA6MhTxOuEhBK82b60pUnATDYDOajqIB9ubebwAAAP//AwCLYcclbAMAAA==
+    headers:
+      CF-RAY:
+      - 94b0f2a53b2376fc-LHR
+      Connection:
+      - keep-alive
+      Content-Encoding:
+      - gzip
+      Content-Type:
+      - application/json
+      Date:
+      - Thu, 05 Jun 2025 16:11:48 GMT
+      Server:
+      - cloudflare
+      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
+      openai-organization:
+      - user-xzaeeoqlanzncr8pomspsknu
+      openai-processing-ms:
+      - '421'
+      openai-version:
+      - '2020-10-01'
+      strict-transport-security:
+      - max-age=31536000; includeSubDomains; preload
+      x-envoy-upstream-service-time:
+      - '426'
+      x-ratelimit-limit-requests:
+      - '30000'
+      x-ratelimit-limit-tokens:
+      - '150000000'
+      x-ratelimit-remaining-requests:
+      - '29999'
+      x-ratelimit-remaining-tokens:
+      - '149999963'
+      x-ratelimit-reset-requests:
+      - 2ms
+      x-ratelimit-reset-tokens:
+      - 0s
+      x-request-id:
+      - req_f455e55032be23b1c002e3835280ed01
+    status:
+      code: 200
+      message: OK
+version: 1
diff --git a/packages/opentelemetry-instrumentation-openai/tests/traces/test_chat.py b/packages/opentelemetry-instrumentation-openai/tests/traces/test_chat.py
index 177be93909..e12bf8c65a 100644
--- a/packages/opentelemetry-instrumentation-openai/tests/traces/test_chat.py
+++ b/packages/opentelemetry-instrumentation-openai/tests/traces/test_chat.py
@@ -301,3 +301,112 @@ async def test_chat_async_context_propagation(exporter, async_vllm_openai_client
     request = args[0]
 
     assert_request_contains_tracecontext(request, open_ai_span)
+
+
+@pytest.mark.vcr
+def test_chat_history_message_dict(exporter, openai_client):
+    first_user_message = {
+        "role": "user",
+        "content": "Generate a random noun in Korean. Respond with just that word.",
+    }
+    second_user_message = {
+        "role": "user",
+        "content": "Now, generate a sentence using the word you just gave me.",
+    }
+    first_response = openai_client.chat.completions.create(
+        model="gpt-4.1-nano",
+        messages=[first_user_message],
+    )
+
+    second_response = openai_client.chat.completions.create(
+        model="gpt-4.1-nano",
+        messages=[
+            first_user_message,
+            {
+                "role": "assistant",
+                "content": first_response.choices[0].message.content,
+            },
+            second_user_message,
+        ],
+    )
+
+    spans = exporter.get_finished_spans()
+
+    assert len(spans) == 2
+    first_span = spans[0]
+    assert first_span.name == "openai.chat"
+    assert first_span.attributes[f"{SpanAttributes.LLM_PROMPTS}.0.content"] == first_user_message["content"]
+    assert first_span.attributes[f"{SpanAttributes.LLM_PROMPTS}.0.role"] == first_user_message["role"]
+    assert (
+        first_span.attributes[f"{SpanAttributes.LLM_COMPLETIONS}.0.content"]
+        == first_response.choices[0].message.content
+    )
+    assert first_span.attributes[f"{SpanAttributes.LLM_COMPLETIONS}.0.role"] == "assistant"
+
+    second_span = spans[1]
+    assert second_span.name == "openai.chat"
+    assert second_span.attributes[f"{SpanAttributes.LLM_PROMPTS}.0.content"] == first_user_message["content"]
+    assert (
+        second_span.attributes[f"{SpanAttributes.LLM_COMPLETIONS}.0.content"]
+        == second_response.choices[0].message.content
+    )
+    assert (
+        second_span.attributes[f"{SpanAttributes.LLM_PROMPTS}.1.content"]
+        == first_response.choices[0].message.content
+    )
+    assert second_span.attributes[f"{SpanAttributes.LLM_PROMPTS}.1.role"] == "assistant"
+    assert second_span.attributes[f"{SpanAttributes.LLM_PROMPTS}.2.content"] == second_user_message["content"]
+    assert second_span.attributes[f"{SpanAttributes.LLM_PROMPTS}.2.role"] == "user"
+
+
+@pytest.mark.vcr
+def test_chat_history_message_pydantic(exporter, openai_client):
+    first_user_message = {
+        "role": "user",
+        "content": "Generate a random noun in Korean. Respond with just that word.",
+    }
+    second_user_message = {
+        "role": "user",
+        "content": "Now, generate a sentence using the word you just gave me.",
+    }
+    first_response = openai_client.chat.completions.create(
+        model="gpt-4.1-nano",
+        messages=[first_user_message],
+    )
+
+    second_response = openai_client.chat.completions.create(
+        model="gpt-4.1-nano",
+        messages=[
+            first_user_message,
+            first_response.choices[0].message,
+            second_user_message,
+        ],
+    )
+
+    spans = exporter.get_finished_spans()
+
+    assert len(spans) == 2
+    first_span = spans[0]
+    assert first_span.name == "openai.chat"
+    assert first_span.attributes[f"{SpanAttributes.LLM_PROMPTS}.0.content"] == first_user_message["content"]
+    assert first_span.attributes[f"{SpanAttributes.LLM_PROMPTS}.0.role"] == first_user_message["role"]
+    assert (
+        first_span.attributes[f"{SpanAttributes.LLM_COMPLETIONS}.0.content"]
+        == first_response.choices[0].message.content
+    )
+    assert first_span.attributes[f"{SpanAttributes.LLM_COMPLETIONS}.0.role"] == "assistant"
+
+    second_span = spans[1]
+    assert second_span.name == "openai.chat"
+    assert second_span.attributes[f"{SpanAttributes.LLM_PROMPTS}.0.content"] == first_user_message["content"]
+    assert (
+        second_span.attributes[f"{SpanAttributes.LLM_COMPLETIONS}.0.content"]
+        == second_response.choices[0].message.content
+    )
+    assert (
+        second_span.attributes[f"{SpanAttributes.LLM_PROMPTS}.1.content"]
+        == first_response.choices[0].message.content
+    )
+    assert second_span.attributes[f"{SpanAttributes.LLM_PROMPTS}.1.role"] == "assistant"
+    assert second_span.attributes[f"{SpanAttributes.LLM_PROMPTS}.2.content"] == second_user_message["content"]
+    assert second_span.attributes[f"{SpanAttributes.LLM_PROMPTS}.2.role"] == "user"