Skip to content

Commit de4eb31

Browse files
wip: tox validation for pydantic 1.x version.
1 parent f3ca98f commit de4eb31

4 files changed

Lines changed: 112 additions & 39 deletions

File tree

instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/response_extractors.py

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,19 @@
1818
from collections.abc import Mapping, Sequence
1919
from typing import TYPE_CHECKING, List, Optional, TypeVar, Union
2020

21-
from pydantic import BaseModel, Field, ValidationError
22-
23-
try:
24-
from pydantic import ConfigDict
25-
26-
_PYDANTIC_V2 = True
27-
except ImportError:
28-
ConfigDict = None
29-
_PYDANTIC_V2 = False
21+
from pydantic import BaseModel, Field, StrictInt, StrictStr, ValidationError
3022

3123
from opentelemetry.semconv._incubating.attributes import (
3224
openai_attributes as OpenAIAttributes,
3325
)
3426

27+
_PYDANTIC_V2 = hasattr(BaseModel, "model_validate")
28+
29+
if _PYDANTIC_V2:
30+
from pydantic import ConfigDict
31+
else:
32+
ConfigDict = None
33+
3534
if TYPE_CHECKING:
3635
from opentelemetry.util.genai.types import (
3736
InputMessage,
@@ -71,51 +70,53 @@ class Config:
7170

7271

7372
class _ResponseTextFormatModel(_ExtractorModel):
74-
type: Optional[str] = None
73+
type: Optional[StrictStr] = None
7574

7675

7776
class _ResponseTextConfigModel(_ExtractorModel):
7877
format: Optional[_ResponseTextFormatModel] = None
7978

8079

8180
class _ResponseInputContentModel(_ExtractorModel):
82-
text: Optional[str] = None
81+
text: Optional[StrictStr] = None
8382

8483

8584
class _ResponseInputItemModel(_ExtractorModel):
86-
role: Optional[str] = None
87-
content: Optional[Union[str, List[_ResponseInputContentModel]]] = None
85+
role: Optional[StrictStr] = None
86+
content: Optional[Union[StrictStr, List[_ResponseInputContentModel]]] = (
87+
None
88+
)
8889

8990

9091
class _ResponsesRequestModel(_ExtractorModel):
91-
instructions: Optional[str] = None
92-
input: Optional[Union[str, List[_ResponseInputItemModel]]] = None
92+
instructions: Optional[StrictStr] = None
93+
input: Optional[Union[StrictStr, List[_ResponseInputItemModel]]] = None
9394
text: Optional[_ResponseTextConfigModel] = None
9495

9596

9697
class _ResponseOutputContentModel(_ExtractorModel):
97-
type: Optional[str] = None
98-
text: Optional[str] = None
99-
refusal: Optional[str] = None
98+
type: Optional[StrictStr] = None
99+
text: Optional[StrictStr] = None
100+
refusal: Optional[StrictStr] = None
100101

101102

102103
class _ResponseOutputItemModel(_ExtractorModel):
103-
type: Optional[str] = None
104-
role: Optional[str] = None
105-
status: Optional[str] = None
104+
type: Optional[StrictStr] = None
105+
role: Optional[StrictStr] = None
106+
status: Optional[StrictStr] = None
106107
content: List[_ResponseOutputContentModel] = Field(default_factory=list)
107108

108109

109110
class _UsageDetailsModel(_ExtractorModel):
110-
cached_tokens: Optional[int] = None
111-
cache_creation_input_tokens: Optional[int] = None
111+
cached_tokens: Optional[StrictInt] = None
112+
cache_creation_input_tokens: Optional[StrictInt] = None
112113

113114

114115
class _UsageModel(_ExtractorModel):
115-
input_tokens: Optional[int] = None
116-
output_tokens: Optional[int] = None
117-
prompt_tokens: Optional[int] = None
118-
completion_tokens: Optional[int] = None
116+
input_tokens: Optional[StrictInt] = None
117+
output_tokens: Optional[StrictInt] = None
118+
prompt_tokens: Optional[StrictInt] = None
119+
completion_tokens: Optional[StrictInt] = None
119120
input_tokens_details: Optional[_UsageDetailsModel] = None
120121
prompt_tokens_details: Optional[_UsageDetailsModel] = None
121122

@@ -125,9 +126,9 @@ class _ResponsesResultModel(_ExtractorModel):
125126
# terminal message items available yet" so downstream extraction helpers
126127
# naturally return empty lists instead of raising.
127128
output: List[_ResponseOutputItemModel] = Field(default_factory=list)
128-
model: Optional[str] = None
129-
id: Optional[str] = None
130-
service_tier: Optional[str] = None
129+
model: Optional[StrictStr] = None
130+
id: Optional[StrictStr] = None
131+
service_tier: Optional[StrictStr] = None
131132
usage: Optional[_UsageModel] = None
132133

133134

@@ -138,12 +139,19 @@ def _rebuild_model(model_type: type[BaseModel]) -> None:
138139
model_type.update_forward_refs(**globals())
139140

140141

141-
_rebuild_model(_ResponseTextConfigModel)
142-
_rebuild_model(_ResponseInputItemModel)
143-
_rebuild_model(_ResponsesRequestModel)
144-
_rebuild_model(_ResponseOutputItemModel)
145-
_rebuild_model(_UsageModel)
146-
_rebuild_model(_ResponsesResultModel)
142+
for _model_type in (
143+
_ResponseTextFormatModel,
144+
_ResponseTextConfigModel,
145+
_ResponseInputContentModel,
146+
_ResponseInputItemModel,
147+
_ResponsesRequestModel,
148+
_ResponseOutputContentModel,
149+
_ResponseOutputItemModel,
150+
_UsageDetailsModel,
151+
_UsageModel,
152+
_ResponsesResultModel,
153+
):
154+
_rebuild_model(_model_type)
147155

148156

149157
def _validate_model(
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
# ********************************
17+
# WARNING: NOT HERMETIC !!!!!!!!!!
18+
# ********************************
19+
#
20+
# This "requirements.txt" is installed in conjunction
21+
# with multiple other dependencies in the top-level "tox.ini"
22+
# file. In particular, please see:
23+
#
24+
# openai-pydantic1: {[testenv]test_deps}
25+
# openai-pydantic1: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/requirements.pydantic1.txt
26+
#
27+
# This provides additional dependencies, namely:
28+
#
29+
# opentelemetry-api
30+
# opentelemetry-sdk
31+
# opentelemetry-semantic-conventions
32+
#
33+
# ... with a "dev" version based on the latest distribution.
34+
35+
36+
# This variant of the requirements aims to test the system using
37+
# the newest supported version of external dependencies with Pydantic 1.x.
38+
39+
openai==1.109.1
40+
pydantic==1.10.24
41+
httpx==0.27.2
42+
# older jiter is required for PyPy < 3.11
43+
jiter==0.11.1
44+
Deprecated==1.2.14
45+
importlib-metadata==6.11.0
46+
packaging==24.0
47+
pytest==7.4.4
48+
pytest-vcr==1.0.2
49+
pytest-asyncio==0.21.0
50+
wrapt==1.16.0
51+
# test with the latest version of opentelemetry-api, sdk, and semantic conventions
52+
53+
-e opentelemetry-instrumentation
54+
-e instrumentation-genai/opentelemetry-instrumentation-openai-v2
55+
-e util/opentelemetry-util-genai

instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_response_extractors.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ def _module(block_genai_types_import=False):
6767
return _load_module(block_genai_types_import)
6868

6969

70+
def _validate_compat_model(loaded_module, model_type, value):
71+
return loaded_module._validate_model(model_type, value, "test")
72+
73+
7074
@pytest.fixture(scope="module", name="loaded_module")
7175
def _loaded_module_fixture():
7276
return _module()
@@ -329,7 +333,9 @@ def test_set_invocation_response_attributes_populates_output_messages(
329333
def test_prevalidated_response_model_skips_revalidation(
330334
loaded_module, monkeypatch
331335
):
332-
validated_result = loaded_module._ResponsesResultModel.model_validate(
336+
validated_result = _validate_compat_model(
337+
loaded_module,
338+
loaded_module._ResponsesResultModel,
333339
SimpleNamespace(
334340
output=[
335341
SimpleNamespace(
@@ -338,8 +344,9 @@ def test_prevalidated_response_model_skips_revalidation(
338344
content=[SimpleNamespace(type="output_text", text="Done")],
339345
)
340346
]
341-
)
347+
),
342348
)
349+
assert validated_result is not None
343350

344351
def _unexpected_validation(_result):
345352
raise AssertionError("unexpected response revalidation")
@@ -358,7 +365,7 @@ def _unexpected_validation(_result):
358365
[
359366
({"instructions": ["not-a-string"]}, "_extract_system_instruction"),
360367
({"input": 42}, "_extract_input_messages"),
361-
({"text": {"format": 42}}, "_extract_output_type"),
368+
({"text": {"format": {"type": 42}}}, "_extract_output_type"),
362369
],
363370
)
364371
def test_request_validation_errors_are_logged_and_ignored(

tox.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ envlist =
1010

1111
; instrumentation-openai
1212
py3{9,10,11,12,13,14}-test-instrumentation-openai-v2-{oldest,latest}
13+
py313-test-instrumentation-openai-v2-pydantic1
1314
pypy3-test-instrumentation-openai-v2-{oldest,latest}
1415
lint-instrumentation-openai-v2
1516

@@ -482,6 +483,8 @@ deps =
482483
# and the latest version of OTel API and SDK
483484
openai-latest: {[testenv]test_deps}
484485
openai-latest: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/requirements.latest.txt
486+
openai-pydantic1: {[testenv]test_deps}
487+
openai-pydantic1: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/requirements.pydantic1.txt
485488
lint-instrumentation-openai-v2: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/requirements.oldest.txt
486489
openai_agents-oldest: -r {toxinidir}/instrumentation-genai/opentelemetry-instrumentation-openai-agents-v2/tests/requirements.oldest.txt
487490
openai_agents-latest: {[testenv]test_deps}

0 commit comments

Comments
 (0)