Skip to content

Commit 6a49b0b

Browse files
[fern-generated] Update SDK (#768)
Generated by Fern CLI Version: unknown Generators: - fernapi/fern-python-sdk: 5.14.6 Co-authored-by: fern-api <115122769+fern-api[bot]@users.noreply.github.com>
1 parent eaf7d15 commit 6a49b0b

4 files changed

Lines changed: 86 additions & 15 deletions

File tree

.fern/metadata.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"cliVersion": "5.37.7",
33
"generatorName": "fernapi/fern-python-sdk",
4-
"generatorVersion": "5.14.4",
4+
"generatorVersion": "5.14.6",
55
"generatorConfig": {
66
"inline_request_params": false,
77
"extras": {
@@ -94,10 +94,10 @@
9494
}
9595
]
9696
},
97-
"originGitCommit": "6b200e3df80ccea85f9d69dc3c5572410454d8b7",
97+
"originGitCommit": "2fca5765e1ee013584ac0e3caafee618cec00d7f",
9898
"originGitCommitIsDirty": true,
9999
"invokedBy": "ci",
100-
"requestedVersion": "7.0.1",
100+
"requestedVersion": "7.0.2",
101101
"ciProvider": "github",
102-
"sdkVersion": "7.0.1"
102+
"sdkVersion": "7.0.2"
103103
}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ dynamic = ["version"]
44

55
[tool.poetry]
66
name = "cohere"
7-
version = "7.0.1"
7+
version = "7.0.2"
88
description = ""
99
readme = "README.md"
1010
authors = []

src/cohere/core/client_wrapper.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ def get_headers(self) -> typing.Dict[str, str]:
3131
import platform
3232

3333
headers: typing.Dict[str, str] = {
34-
"User-Agent": "cohere/7.0.1",
34+
"User-Agent": "cohere/7.0.2",
3535
"X-Fern-Language": "Python",
3636
"X-Fern-Runtime": f"python/{platform.python_version()}",
3737
"X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}",
3838
"X-Fern-SDK-Name": "cohere",
39-
"X-Fern-SDK-Version": "7.0.1",
39+
"X-Fern-SDK-Version": "7.0.2",
4040
**(self.get_custom_headers() or {}),
4141
}
4242
if self._client_name is not None:

src/cohere/core/serialization.py

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,75 @@ def __init__(self, *, alias: str) -> None:
2626
self.alias = alias
2727

2828

29+
# Resolving type hints (typing.get_type_hints) is expensive because it eval/compiles
30+
# forward-reference annotations. The result is constant for a given type, so we cache it.
31+
# This is critical for hot paths like SSE event parsing, where the same (often large
32+
# discriminated-union) type is converted on every single event.
33+
_type_hints_cache: typing.Dict[typing.Any, typing.Dict[str, typing.Any]] = {}
34+
35+
36+
def _get_cached_type_hints(expected_type: typing.Any) -> typing.Dict[str, typing.Any]:
37+
try:
38+
cached = _type_hints_cache.get(expected_type)
39+
except TypeError:
40+
# Unhashable type; resolve without caching.
41+
return _resolve_type_hints(expected_type)
42+
if cached is None:
43+
cached = _resolve_type_hints(expected_type)
44+
_type_hints_cache[expected_type] = cached
45+
return cached
46+
47+
48+
def _resolve_type_hints(expected_type: typing.Any) -> typing.Dict[str, typing.Any]:
49+
try:
50+
return typing_extensions.get_type_hints(expected_type, include_extras=True)
51+
except NameError:
52+
# The type contains a circular reference, so we use the __annotations__ attribute directly.
53+
return getattr(expected_type, "__annotations__", {})
54+
55+
56+
# Whether convert_and_respect_annotation_metadata can possibly rewrite anything for a given
57+
# annotation, i.e. whether any reachable model/TypedDict field carries a FieldMetadata alias.
58+
# This is constant per type, so we cache it and use it to short-circuit the recursive walk.
59+
_requires_conversion_cache: typing.Dict[typing.Any, bool] = {}
60+
61+
62+
def _requires_conversion(type_: typing.Any) -> bool:
63+
try:
64+
cached = _requires_conversion_cache.get(type_)
65+
except TypeError:
66+
# Unhashable annotation; compute without caching.
67+
return _compute_requires_conversion(type_, set())
68+
if cached is None:
69+
cached = _compute_requires_conversion(type_, set())
70+
_requires_conversion_cache[type_] = cached
71+
return cached
72+
73+
74+
def _compute_requires_conversion(type_: typing.Any, seen: typing.Set[typing.Any]) -> bool:
75+
clean_type = _remove_annotations(type_)
76+
77+
try:
78+
if clean_type in seen:
79+
return False
80+
seen = seen | {clean_type}
81+
except TypeError:
82+
# Unhashable type; skip cycle tracking (the type graph is finite in practice).
83+
pass
84+
85+
# Models / TypedDicts: a field alias here means we must dealias; otherwise recurse into fields.
86+
if (inspect.isclass(clean_type) and issubclass(clean_type, pydantic.BaseModel)) or typing_extensions.is_typeddict(
87+
clean_type
88+
):
89+
annotations = _get_cached_type_hints(clean_type)
90+
if _get_alias_to_field_name(annotations):
91+
return True
92+
return any(_compute_requires_conversion(hint, seen) for hint in annotations.values())
93+
94+
# Containers / unions: recurse into the type arguments (List/Set/Sequence/Dict/Union/etc.).
95+
return any(_compute_requires_conversion(arg, seen) for arg in typing_extensions.get_args(clean_type))
96+
97+
2998
def convert_and_respect_annotation_metadata(
3099
*,
31100
object_: typing.Any,
@@ -57,6 +126,13 @@ def convert_and_respect_annotation_metadata(
57126
return None
58127
if inner_type is None:
59128
inner_type = annotation
129+
# The only thing this function ever rewrites is keys that carry a FieldMetadata
130+
# alias. If nothing in the (cached) type graph has such an alias, the conversion is
131+
# a content-identity transform, so we can skip the entire recursive walk. This is
132+
# the hot path for SSE streaming, where a large discriminated union would otherwise
133+
# be traversed on every single event.
134+
if not _requires_conversion(annotation):
135+
return object_
60136

61137
clean_type = _remove_annotations(inner_type)
62138
# Pydantic models
@@ -160,12 +236,7 @@ def _convert_mapping(
160236
direction: typing.Literal["read", "write"],
161237
) -> typing.Mapping[str, object]:
162238
converted_object: typing.Dict[str, object] = {}
163-
try:
164-
annotations = typing_extensions.get_type_hints(expected_type, include_extras=True)
165-
except NameError:
166-
# The TypedDict contains a circular reference, so
167-
# we use the __annotations__ attribute directly.
168-
annotations = getattr(expected_type, "__annotations__", {})
239+
annotations = _get_cached_type_hints(expected_type)
169240
aliases_to_field_names = _get_alias_to_field_name(annotations)
170241
for key, value in object_.items():
171242
if direction == "read" and key in aliases_to_field_names:
@@ -221,12 +292,12 @@ def _remove_annotations(type_: typing.Any) -> typing.Any:
221292

222293

223294
def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]:
224-
annotations = typing_extensions.get_type_hints(type_, include_extras=True)
295+
annotations = _get_cached_type_hints(type_)
225296
return _get_alias_to_field_name(annotations)
226297

227298

228299
def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]:
229-
annotations = typing_extensions.get_type_hints(type_, include_extras=True)
300+
annotations = _get_cached_type_hints(type_)
230301
return _get_field_to_alias_name(annotations)
231302

232303

0 commit comments

Comments
 (0)