aidial-adapter-anthropic 0.1.0__py3-none-any.whl → 0.2.0rc0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ import os
2
+ import warnings
3
+
4
+ if os.getenv("PYDANTIC_V2", "0").lower() not in ("1", "true"):
5
+ warnings.warn("PYDANTIC_V2 env variable is expected to be set to True")
@@ -57,7 +57,7 @@ def _to_dict(obj: Any, **kwargs) -> Any:
57
57
  return tuple(rec(element) for element in obj)
58
58
 
59
59
  if isinstance(obj, BaseModel):
60
- return rec(obj.dict())
60
+ return rec(obj.model_dump())
61
61
 
62
62
  if hasattr(obj, "to_dict"):
63
63
  return rec(obj.to_dict())
@@ -15,10 +15,7 @@ from aidial_adapter_anthropic.dial.request import (
15
15
  )
16
16
 
17
17
 
18
- class ChatCompletionAdapter(ABC, BaseModel):
19
- class Config:
20
- arbitrary_types_allowed = True
21
-
18
+ class ChatCompletionAdapter(ABC):
22
19
  @abstractmethod
23
20
  async def chat(
24
21
  self,
@@ -210,6 +210,7 @@ async def create_adapter(
210
210
  )(model)
211
211
 
212
212
 
213
+ @dataclass
213
214
  class Adapter(ChatCompletionAdapter):
214
215
  deployment: str
215
216
  storage: Optional[FileStorage]
@@ -2,9 +2,11 @@ from typing import List, Literal
2
2
 
3
3
  from anthropic.types.anthropic_beta_param import AnthropicBetaParam
4
4
  from anthropic.types.beta import BetaThinkingConfigParam as ThinkingConfigParam
5
- from pydantic import Field
5
+ from pydantic import BaseModel, ConfigDict, Field
6
6
 
7
- from aidial_adapter_anthropic._utils.pydantic import ExtraForbidModel
7
+
8
+ class ExtraForbidModel(BaseModel):
9
+ model_config = ConfigDict(extra="forbid")
8
10
 
9
11
 
10
12
  class ThinkingConfigEnabled(ExtraForbidModel):
@@ -21,7 +21,7 @@ class MessageState(BaseModel):
21
21
  claude_message_content: List[ParsedContentBlock] | List[ContentBlock]
22
22
 
23
23
  def to_dict(self) -> dict:
24
- return self.dict(
24
+ return self.model_dump(
25
25
  # FIXME: a hack to exclude the private __json_buf field
26
26
  exclude={"claude_message_content": {"__all__": {"__json_buf"}}},
27
27
  # Excluding `citations: null`, since they could not be even parsed
@@ -35,7 +35,7 @@ def get_message_content_from_state(
35
35
  ) -> List[ContentBlockParam] | None:
36
36
  if (cc := message.custom_content) and (state_dict := cc.state):
37
37
  try:
38
- state = MessageState.parse_obj(state_dict)
38
+ state = MessageState.model_validate(state_dict)
39
39
  return [block.to_dict() for block in state.claude_message_content] # type: ignore
40
40
  except pydantic.ValidationError as e:
41
41
  _log.error(
@@ -1,3 +1,4 @@
1
+ from dataclasses import dataclass
1
2
  from typing import Callable, List
2
3
 
3
4
  from aidial_sdk.chat_completion import Message
@@ -9,6 +10,7 @@ from aidial_adapter_anthropic.dial.consumer import Consumer
9
10
  from aidial_adapter_anthropic.dial.request import ModelParameters
10
11
 
11
12
 
13
+ @dataclass
12
14
  class ChatCompletionDecorator(ChatCompletionAdapter):
13
15
  adapter: ChatCompletionAdapter
14
16
 
@@ -1,3 +1,4 @@
1
+ from dataclasses import dataclass
1
2
  from typing import Callable, List
2
3
 
3
4
  from aidial_sdk.chat_completion import Message
@@ -20,6 +21,7 @@ def preprocess_messages_decorator(
20
21
  )
21
22
 
22
23
 
24
+ @dataclass
23
25
  class PreprocessMessagesDecorator(ChatCompletionDecorator):
24
26
  on_messages: Callable[[List[Message]], ListProjection[Message]]
25
27
 
@@ -22,7 +22,7 @@ class ReplicatorDecorator(ChatCompletionDecorator):
22
22
  params: ModelParameters,
23
23
  messages: List[Message],
24
24
  ) -> None:
25
- params1 = params.copy()
25
+ params1 = params.model_copy()
26
26
  params1.n = 1
27
27
 
28
28
  async def _chat(root_consumer: Consumer):
@@ -1,4 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
2
3
  from typing import Awaitable, Callable, List, Optional, Set, Tuple, TypeVar
3
4
 
4
5
  from aidial_sdk.exceptions import ContextLengthExceededError
@@ -7,7 +8,6 @@ from aidial_sdk.exceptions import (
7
8
  InvalidRequestError,
8
9
  TruncatePromptSystemAndLastUserError,
9
10
  )
10
- from pydantic import BaseModel
11
11
 
12
12
  from aidial_adapter_anthropic._utils.list import (
13
13
  omit_by_indices,
@@ -15,7 +15,7 @@ from aidial_adapter_anthropic._utils.list import (
15
15
  )
16
16
 
17
17
 
18
- class TruncatePromptError(ABC, BaseModel):
18
+ class TruncatePromptError(ABC):
19
19
  @abstractmethod
20
20
  def to_dial_exception(self) -> DialException:
21
21
  pass
@@ -24,6 +24,7 @@ class TruncatePromptError(ABC, BaseModel):
24
24
  return self.to_dial_exception().message
25
25
 
26
26
 
27
+ @dataclass
27
28
  class InconsistentLimitsError(TruncatePromptError):
28
29
  user_limit: int
29
30
  model_limit: int
@@ -35,6 +36,7 @@ class InconsistentLimitsError(TruncatePromptError):
35
36
  )
36
37
 
37
38
 
39
+ @dataclass
38
40
  class ModelLimitOverflow(TruncatePromptError):
39
41
  model_limit: int
40
42
  token_count: int
@@ -43,6 +45,7 @@ class ModelLimitOverflow(TruncatePromptError):
43
45
  return ContextLengthExceededError(self.model_limit, self.token_count)
44
46
 
45
47
 
48
+ @dataclass
46
49
  class UserLimitOverflow(TruncatePromptError):
47
50
  user_limit: int
48
51
  token_count: int
@@ -50,10 +50,8 @@ class HandlerWithConfig(Protocol, Generic[_T, _Config]):
50
50
  def __call__(self, resource: Resource, config: _Config | None) -> _T: ...
51
51
 
52
52
 
53
- class AttachmentProcessor(BaseModel, Generic[_T, _Config]):
54
- class Config:
55
- arbitrary_types_allowed = True
56
-
53
+ @dataclass
54
+ class AttachmentProcessor(Generic[_T, _Config]):
57
55
  supported_types: Dict[str, Set[str]]
58
56
  """MIME type to file extensions mapping"""
59
57
 
@@ -89,11 +87,12 @@ class WithResources(Generic[_T]):
89
87
  return WithResources(payload=payload, resources=resources)
90
88
 
91
89
 
92
- class AttachmentProcessors(BaseModel, Generic[_Txt, _T, _Config]):
93
- config: _Config | None = None
90
+ @dataclass
91
+ class AttachmentProcessors(Generic[_Txt, _T, _Config]):
94
92
  attachment_processors: Sequence[AttachmentProcessor[_T, _Config]]
95
93
  text_handler: Callable[[str], _Txt]
96
94
  file_storage: FileStorage | None
95
+ config: _Config | None = field(default=None)
97
96
 
98
97
  @property
99
98
  def supported_types(self) -> Dict[str, Set[str]]:
@@ -179,7 +179,7 @@ class ChoiceConsumer(Consumer):
179
179
  self._citations[document_id] = (display_index, document)
180
180
 
181
181
  if document:
182
- document = document.copy()
182
+ document = document.model_copy()
183
183
  document.title = f"[{display_index}] {document.title or ''}".strip()
184
184
  document.reference_type = document.reference_type or document.type
185
185
  document.reference_url = document.reference_url or document.url
@@ -20,7 +20,7 @@ from aidial_sdk.chat_completion.request import (
20
20
  )
21
21
  from aidial_sdk.exceptions import RequestValidationError
22
22
  from pydantic import BaseModel
23
- from pydantic.v1 import ValidationError as PydanticValidationError
23
+ from pydantic import ValidationError as PydanticValidationError
24
24
 
25
25
  from aidial_adapter_anthropic.adapter._errors import ValidationError
26
26
  from aidial_adapter_anthropic.dial.tools import (
@@ -82,9 +82,6 @@ class ModelParameters(BaseModel):
82
82
  configuration=configuration,
83
83
  )
84
84
 
85
- def add_stop_sequences(self, stop: List[str]) -> "ModelParameters":
86
- return self.copy(update={"stop": [*self.stop, *stop]})
87
-
88
85
  @property
89
86
  def tools_mode(self) -> ToolsMode | None:
90
87
  if self.tool_config is not None:
@@ -93,7 +90,7 @@ class ModelParameters(BaseModel):
93
90
 
94
91
  def parse_configuration(self, cls: Type[_Model]) -> _Model:
95
92
  try:
96
- return cls.parse_obj(self.configuration or {})
93
+ return cls.model_validate(self.configuration or {})
97
94
  except PydanticValidationError as e:
98
95
  if self.configuration is None:
99
96
  msg = "The configuration at path 'custom_fields.configuration' is missing."
@@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
4
4
  from typing import List
5
5
 
6
6
  from aidial_sdk.chat_completion import Attachment
7
- from pydantic import BaseModel, Field, root_validator, validator
7
+ from pydantic import BaseModel, Field, field_validator, model_validator
8
8
 
9
9
  from aidial_adapter_anthropic._utils.resource import Resource
10
10
  from aidial_adapter_anthropic._utils.text import truncate_string
@@ -34,7 +34,7 @@ class UnsupportedContentType(ValidationError):
34
34
 
35
35
 
36
36
  class DialResource(ABC, BaseModel):
37
- entity_name: str = Field(default=None)
37
+ entity_name: str | None = None
38
38
  supported_types: List[str] | None = Field(default=None)
39
39
 
40
40
  @abstractmethod
@@ -77,10 +77,10 @@ class URLResource(DialResource):
77
77
  def to_attachment(self) -> Attachment:
78
78
  return Attachment(type=self.content_type, url=self.url)
79
79
 
80
- @root_validator
81
- def validator(cls, values):
82
- values["entity_name"] = values.get("entity_name") or "URL"
83
- return values
80
+ @model_validator(mode="after")
81
+ def default_entity_name(self):
82
+ self.entity_name = self.entity_name or "URL"
83
+ return self
84
84
 
85
85
  async def download(self, storage: FileStorage | None) -> Resource:
86
86
  type = await self.get_content_type()
@@ -114,10 +114,10 @@ class AttachmentResource(DialResource):
114
114
  def to_attachment(self) -> Attachment:
115
115
  return self.attachment
116
116
 
117
- @validator("attachment", pre=True)
117
+ @field_validator("attachment", mode="before")
118
118
  def parse_attachment(cls, value):
119
119
  if isinstance(value, dict):
120
- attachment = Attachment.parse_obj(value)
120
+ attachment = Attachment.model_validate(value)
121
121
  # Working around the issue of defaulting missing type to a markdown:
122
122
  # https://github.com/epam/ai-dial-sdk/blob/2835107e950c89645a2b619fecba2518fa2d7bb1/aidial_sdk/chat_completion/request.py#L22
123
123
  if "type" not in value:
@@ -125,10 +125,10 @@ class AttachmentResource(DialResource):
125
125
  return attachment
126
126
  return value
127
127
 
128
- @root_validator(pre=True)
129
- def validator(cls, values):
130
- values["entity_name"] = values.get("entity_name") or "attachment"
131
- return values
128
+ @model_validator(mode="after")
129
+ def default_entity_name(self):
130
+ self.entity_name = self.entity_name or "attachment"
131
+ return self
132
132
 
133
133
  async def download(self, storage: FileStorage | None) -> Resource:
134
134
  type = await self.get_content_type()
@@ -3,11 +3,12 @@ import hashlib
3
3
  import io
4
4
  import logging
5
5
  import mimetypes
6
- from typing import Mapping, Optional, TypedDict
6
+ from typing import Mapping, Optional
7
7
  from urllib.parse import unquote, urljoin
8
8
 
9
9
  import aiohttp
10
10
  from pydantic import BaseModel
11
+ from typing_extensions import TypedDict
11
12
 
12
13
  _log = logging.getLogger(__name__)
13
14
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aidial-adapter-anthropic
3
- Version: 0.1.0
3
+ Version: 0.2.0rc0
4
4
  Summary: Package implementing adapter from DIAL Chat Completions API to Anthropic API
5
5
  License: Apache-2.0
6
6
  Keywords: ai
@@ -12,7 +12,7 @@ Requires-Dist: aidial-sdk (>=0.28.0,<1)
12
12
  Requires-Dist: aiohttp (>=3.13.3,<4)
13
13
  Requires-Dist: anthropic (>=0.75.0,<1)
14
14
  Requires-Dist: pillow (>=10.4.0,<11)
15
- Requires-Dist: pydantic (>=1.10.17,<3)
15
+ Requires-Dist: pydantic (>=2.8.2,<3)
16
16
  Project-URL: Documentation, https://epam-rail.com/dial_api
17
17
  Project-URL: Homepage, https://epam-rail.com
18
18
  Project-URL: Repository, https://github.com/epam/ai-dial-adapter-anthropic/
@@ -1,39 +1,39 @@
1
- aidial_adapter_anthropic/_utils/json.py,sha256=IknyukIXEzQkZ81zo5ukUexvtvnlfRrlxvvYCekoAkc,2941
1
+ aidial_adapter_anthropic/__init__.py,sha256=gWNLIVwPavaMsawwgowSX3-3hM6dxctGgQ3HSw2a7l8,166
2
+ aidial_adapter_anthropic/_utils/json.py,sha256=suurdZDIu3tWbaWVscL4WU-mF-1yJPZ0bNOIKN_IBxs,2947
2
3
  aidial_adapter_anthropic/_utils/list.py,sha256=SHC9KD2ckQCvtzVq31KuvQfl-RMfAfyVkPqDviOLuIY,1996
3
- aidial_adapter_anthropic/_utils/pydantic.py,sha256=7h0cThzw0nHOL5H05yMzbkwilZ8tegCjb0iRdjOrtl4,111
4
4
  aidial_adapter_anthropic/_utils/resource.py,sha256=69i3zt7OP4_uceM4_5xqhBCW_pQOfrgEFhZoAdAS9yk,1563
5
5
  aidial_adapter_anthropic/_utils/text.py,sha256=d9LJdO9jom3B7_dS3SaqjlNvngOFrgzsZHdQye92Kyo,106
6
6
  aidial_adapter_anthropic/adapter/__init__.py,sha256=jgJHHpSRJqObAPx3TBcsxZqNL3jw7g9bDnbcfqml8HY,222
7
- aidial_adapter_anthropic/adapter/_base.py,sha256=MEpRlxgZsaZRedMtQyqJaeFDHhUz5Af9CGJnnqu3pDA,2818
8
- aidial_adapter_anthropic/adapter/_claude/adapter.py,sha256=oT0BIXX7rbLGphe9RdiFZrNywk5hopn1fNquJTpfDVw,20484
7
+ aidial_adapter_anthropic/adapter/_base.py,sha256=ETtsTm3RhU0clGEt7bhNmOc5N80RH_-oKM6eKd-UJ3k,2749
8
+ aidial_adapter_anthropic/adapter/_claude/adapter.py,sha256=Kr4TPBqPooMazfy7AmJ4H2NMVSaI7-eQRoUeL0gCAWA,20495
9
9
  aidial_adapter_anthropic/adapter/_claude/blocks.py,sha256=wXP7UDILn5Az06BAptLo24VFZkXf16ofhlCJsJCclgU,3907
10
10
  aidial_adapter_anthropic/adapter/_claude/citations.py,sha256=NBxl_LxP8dUaG0_Xej0Eo5Yst5X8farQdomggdJm9iE,2175
11
- aidial_adapter_anthropic/adapter/_claude/config.py,sha256=pKfoUUoUOEyYbYR0KL88F3lvCTPNxzGF6L6dgLCn2fI,1314
11
+ aidial_adapter_anthropic/adapter/_claude/config.py,sha256=Z7pu73LT08TLcv7gbn1iQFRNTUvoh-WHeQ5Ya-u_cvk,1349
12
12
  aidial_adapter_anthropic/adapter/_claude/converters.py,sha256=IanS08NHoT1jgcV6_Hf5MzuiE5HdKP9K29A70fVXNr4,10353
13
13
  aidial_adapter_anthropic/adapter/_claude/params.py,sha256=ymFwYEjgq1EjuOM2GsojlcQpUkDvy_DLR8euCHrqsTI,981
14
- aidial_adapter_anthropic/adapter/_claude/state.py,sha256=hhixpQKGn-F-eDPMLwsFCV1wWcxOOz3uXIM3pgvJmDU,1499
14
+ aidial_adapter_anthropic/adapter/_claude/state.py,sha256=eC-Qeby0f68pFu10lwfRJT6ANowFTiIUM1QEE68D60o,1510
15
15
  aidial_adapter_anthropic/adapter/_claude/tokenizer/__init__.py,sha256=zA6TwQu4qMH5XVBOKetsQuPs4Prx1-uNsFopiTBpx_A,256
16
16
  aidial_adapter_anthropic/adapter/_claude/tokenizer/anthropic.py,sha256=EGw7N-6V-huH3FOwrIeHUb5jYpWfinYhZEvGd1la_fs,1738
17
17
  aidial_adapter_anthropic/adapter/_claude/tokenizer/approximate.py,sha256=MfuH2E-I7vqJgYkp0hKbmDSNC1HopWr592n7gKUa8p4,10427
18
18
  aidial_adapter_anthropic/adapter/_claude/tokenizer/base.py,sha256=2wivuP51FXaieqdRgMpBoPARIgkcB9ypyOlTYVykcng,816
19
19
  aidial_adapter_anthropic/adapter/_claude/tools.py,sha256=TrsM7fTbm22FhSKLi2aeCE7VGFWgVoKDA_pnZihiKp8,3053
20
- aidial_adapter_anthropic/adapter/_decorator/base.py,sha256=fRIFpWaYKpo-nEbLapjWX3owu2Uu3MOLTmRZZD73Xv8,1730
21
- aidial_adapter_anthropic/adapter/_decorator/preprocess.py,sha256=VmyS6rG5sJwCVBzLmwy5qU0_9T2o-vbylFevGxnSsro,2204
22
- aidial_adapter_anthropic/adapter/_decorator/replicator.py,sha256=JgQ5qAJJJzOJG-byKHRedqyzC9ETn3CkpkfHUCBJ-rI,959
20
+ aidial_adapter_anthropic/adapter/_decorator/base.py,sha256=rBl_BorYjejRR6Q2syyjq3zDEEpb_DNfdvYWY525C_c,1775
21
+ aidial_adapter_anthropic/adapter/_decorator/preprocess.py,sha256=ALKfa5g9pb5i8YJxkJULCiNt_kBdaCbYJTmfhuyhJ2Q,2249
22
+ aidial_adapter_anthropic/adapter/_decorator/replicator.py,sha256=jQSDEe-uwFbZ6NF6CSdYXP4SvHEOci6UCOOTcHzK0v8,965
23
23
  aidial_adapter_anthropic/adapter/_errors.py,sha256=pBm-JmTv4Y9m24vKqGm-Kh2vm6fv-jbM_I2_lMZkpCk,3058
24
24
  aidial_adapter_anthropic/adapter/_tokenize.py,sha256=jv43xTtTbo1mZahXVpL24Bf9ON8xNluctH9na2_9PJg,566
25
- aidial_adapter_anthropic/adapter/_truncate_prompt.py,sha256=TBUpbGiaRmquzwiqLJnR8oah4AJjrMAib26SksCd9C0,4738
25
+ aidial_adapter_anthropic/adapter/_truncate_prompt.py,sha256=TxtHSVpSQNqxn1gGLfQgycF2arnn__l5C5D9BNNbwMo,4763
26
26
  aidial_adapter_anthropic/adapter/claude.py,sha256=eF3K4yV1tODNF_CPYmhnZQIs6LD4e6MqE-aJz8onD90,500
27
- aidial_adapter_anthropic/dial/_attachments.py,sha256=-laRPBBDGxA3u1lso19zL-sPNbelPrA0Lc5OWhd3GTM,8218
27
+ aidial_adapter_anthropic/dial/_attachments.py,sha256=dDngPo0BJhYclPl3942uSgtXay2hf8WTeUkUBoZqVJw,8175
28
28
  aidial_adapter_anthropic/dial/_lazy_stage.py,sha256=I6ADVm9HonhUr9TIrbO8yV3nkK1Pwb662gzUFZW_6Ys,1000
29
29
  aidial_adapter_anthropic/dial/_message.py,sha256=X1t6M56rGPhhnlI7qutNZc-qlwtzdAZ2OwNo6yP9JAk,9895
30
- aidial_adapter_anthropic/dial/consumer.py,sha256=XWKzt-srYJbWm2S4smBTupIFlZfYdkhGckE8sLCRkCY,7304
31
- aidial_adapter_anthropic/dial/request.py,sha256=Xi_pROYLDuYyTbKpDwwPqszaFWvpNmgsCpFiuP9CIh0,5015
32
- aidial_adapter_anthropic/dial/resource.py,sha256=Zvb-j28LFi_HIozGHbADdGbOVqgPZVyF5StQ4vqdcWo,5927
33
- aidial_adapter_anthropic/dial/storage.py,sha256=i0TezPbfbvWIPu3RoxxDSq9aOuQXXJY33J6yARYiuUU,4365
30
+ aidial_adapter_anthropic/dial/consumer.py,sha256=y3zvrWUShS_j8D7mUR9d-biMCCbZMlZA9tGNl1jG-3Y,7310
31
+ aidial_adapter_anthropic/dial/request.py,sha256=PmJ10rVHGghRFMd8RowY0hNsMe_L2ofP_a8F6bMX0Mk,4881
32
+ aidial_adapter_anthropic/dial/resource.py,sha256=ekYrSOaPExHbJc7CokqqUNBWFw3rx4ydsK-6ZUoTL6k,5936
33
+ aidial_adapter_anthropic/dial/storage.py,sha256=ptSn-p5D7iuC19xHRPNacUOp5TmjtHNnA_hditiqlLY,4394
34
34
  aidial_adapter_anthropic/dial/token_usage.py,sha256=YICZT1HyukT3eLGamhqnDm5ygWqJ-_ZT8jUo27jW5iE,638
35
35
  aidial_adapter_anthropic/dial/tools.py,sha256=QUiMl4dQwv_rdxBUIMqh0Oh65YOsu4KsSufmbj75W8w,5610
36
- aidial_adapter_anthropic-0.1.0.dist-info/LICENSE,sha256=6Uw_PHOiAbebWhTu8GKOQaW-26oTFnPuBZXQptqkqbw,11348
37
- aidial_adapter_anthropic-0.1.0.dist-info/METADATA,sha256=WE90tZqlL0ZQ7kEDaiFFgBuUjYugJ2sFhJ7RdbvTI6A,2586
38
- aidial_adapter_anthropic-0.1.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
39
- aidial_adapter_anthropic-0.1.0.dist-info/RECORD,,
36
+ aidial_adapter_anthropic-0.2.0rc0.dist-info/LICENSE,sha256=6Uw_PHOiAbebWhTu8GKOQaW-26oTFnPuBZXQptqkqbw,11348
37
+ aidial_adapter_anthropic-0.2.0rc0.dist-info/METADATA,sha256=wNIEHK43H19eGguXnDefqG2pLgzzpVsefMtnpN0PWK4,2587
38
+ aidial_adapter_anthropic-0.2.0rc0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
39
+ aidial_adapter_anthropic-0.2.0rc0.dist-info/RECORD,,
@@ -1,6 +0,0 @@
1
- from pydantic import BaseModel
2
-
3
-
4
- class ExtraForbidModel(BaseModel):
5
- class Config:
6
- extra = "forbid"