Skip to content

Commit 1c59013

Browse files
fix(client): always respect content-type multipart/form-data if provided (openai#1519)
1 parent fee32a0 commit 1c59013

File tree

5 files changed

+58
-52
lines changed

5 files changed

+58
-52
lines changed

src/openai/_base_client.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
HttpxSendArgs,
5959
AsyncTransport,
6060
RequestOptions,
61+
HttpxRequestFiles,
6162
ModelBuilderProtocol,
6263
)
6364
from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping
@@ -460,6 +461,7 @@ def _build_request(
460461
headers = self._build_headers(options)
461462
params = _merge_mappings(self.default_query, options.params)
462463
content_type = headers.get("Content-Type")
464+
files = options.files
463465

464466
# If the given Content-Type header is multipart/form-data then it
465467
# has to be removed so that httpx can generate the header with
@@ -473,14 +475,23 @@ def _build_request(
473475
headers.pop("Content-Type")
474476

475477
# As we are now sending multipart/form-data instead of application/json
476-
# we need to tell httpx to use it, https://www.python-httpx.org/advanced/#multipart-file-encoding
478+
# we need to tell httpx to use it, https://www.python-httpx.org/advanced/clients/#multipart-file-encoding
477479
if json_data:
478480
if not is_dict(json_data):
479481
raise TypeError(
480482
f"Expected query input to be a dictionary for multipart requests but got {type(json_data)} instead."
481483
)
482484
kwargs["data"] = self._serialize_multipartform(json_data)
483485

486+
# httpx determines whether or not to send a "multipart/form-data"
487+
# request based on the truthiness of the "files" argument.
488+
# This gets around that issue by generating a dict value that
489+
# evaluates to true.
490+
#
491+
# https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186
492+
if not files:
493+
files = cast(HttpxRequestFiles, ForceMultipartDict())
494+
484495
# TODO: report this error to httpx
485496
return self._client.build_request( # pyright: ignore[reportUnknownMemberType]
486497
headers=headers,
@@ -493,7 +504,7 @@ def _build_request(
493504
# https://github.com/microsoft/pyright/issues/3526#event-6715453066
494505
params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None,
495506
json=json_data,
496-
files=options.files,
507+
files=files,
497508
**kwargs,
498509
)
499510

@@ -1891,6 +1902,11 @@ def make_request_options(
18911902
return options
18921903

18931904

1905+
class ForceMultipartDict(Dict[str, None]):
1906+
def __bool__(self) -> bool:
1907+
return True
1908+
1909+
18941910
class OtherPlatform:
18951911
def __init__(self, name: str) -> None:
18961912
self.name = name

src/openai/resources/audio/transcriptions.py

+8-10
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,10 @@ def create(
108108
}
109109
)
110110
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
111-
if files:
112-
# It should be noted that the actual Content-Type header that will be
113-
# sent to the server will contain a `boundary` parameter, e.g.
114-
# multipart/form-data; boundary=---abc--
115-
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
111+
# It should be noted that the actual Content-Type header that will be
112+
# sent to the server will contain a `boundary` parameter, e.g.
113+
# multipart/form-data; boundary=---abc--
114+
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
116115
return self._post(
117116
"/audio/transcriptions",
118117
body=maybe_transform(body, transcription_create_params.TranscriptionCreateParams),
@@ -205,11 +204,10 @@ async def create(
205204
}
206205
)
207206
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
208-
if files:
209-
# It should be noted that the actual Content-Type header that will be
210-
# sent to the server will contain a `boundary` parameter, e.g.
211-
# multipart/form-data; boundary=---abc--
212-
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
207+
# It should be noted that the actual Content-Type header that will be
208+
# sent to the server will contain a `boundary` parameter, e.g.
209+
# multipart/form-data; boundary=---abc--
210+
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
213211
return await self._post(
214212
"/audio/transcriptions",
215213
body=await async_maybe_transform(body, transcription_create_params.TranscriptionCreateParams),

src/openai/resources/audio/translations.py

+8-10
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,10 @@ def create(
9393
}
9494
)
9595
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
96-
if files:
97-
# It should be noted that the actual Content-Type header that will be
98-
# sent to the server will contain a `boundary` parameter, e.g.
99-
# multipart/form-data; boundary=---abc--
100-
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
96+
# It should be noted that the actual Content-Type header that will be
97+
# sent to the server will contain a `boundary` parameter, e.g.
98+
# multipart/form-data; boundary=---abc--
99+
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
101100
return self._post(
102101
"/audio/translations",
103102
body=maybe_transform(body, translation_create_params.TranslationCreateParams),
@@ -175,11 +174,10 @@ async def create(
175174
}
176175
)
177176
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
178-
if files:
179-
# It should be noted that the actual Content-Type header that will be
180-
# sent to the server will contain a `boundary` parameter, e.g.
181-
# multipart/form-data; boundary=---abc--
182-
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
177+
# It should be noted that the actual Content-Type header that will be
178+
# sent to the server will contain a `boundary` parameter, e.g.
179+
# multipart/form-data; boundary=---abc--
180+
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
183181
return await self._post(
184182
"/audio/translations",
185183
body=await async_maybe_transform(body, translation_create_params.TranslationCreateParams),

src/openai/resources/files.py

+8-10
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,10 @@ def create(
111111
}
112112
)
113113
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
114-
if files:
115-
# It should be noted that the actual Content-Type header that will be
116-
# sent to the server will contain a `boundary` parameter, e.g.
117-
# multipart/form-data; boundary=---abc--
118-
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
114+
# It should be noted that the actual Content-Type header that will be
115+
# sent to the server will contain a `boundary` parameter, e.g.
116+
# multipart/form-data; boundary=---abc--
117+
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
119118
return self._post(
120119
"/files",
121120
body=maybe_transform(body, file_create_params.FileCreateParams),
@@ -394,11 +393,10 @@ async def create(
394393
}
395394
)
396395
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
397-
if files:
398-
# It should be noted that the actual Content-Type header that will be
399-
# sent to the server will contain a `boundary` parameter, e.g.
400-
# multipart/form-data; boundary=---abc--
401-
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
396+
# It should be noted that the actual Content-Type header that will be
397+
# sent to the server will contain a `boundary` parameter, e.g.
398+
# multipart/form-data; boundary=---abc--
399+
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
402400
return await self._post(
403401
"/files",
404402
body=await async_maybe_transform(body, file_create_params.FileCreateParams),

src/openai/resources/images.py

+16-20
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,10 @@ def create_variation(
9595
}
9696
)
9797
files = extract_files(cast(Mapping[str, object], body), paths=[["image"]])
98-
if files:
99-
# It should be noted that the actual Content-Type header that will be
100-
# sent to the server will contain a `boundary` parameter, e.g.
101-
# multipart/form-data; boundary=---abc--
102-
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
98+
# It should be noted that the actual Content-Type header that will be
99+
# sent to the server will contain a `boundary` parameter, e.g.
100+
# multipart/form-data; boundary=---abc--
101+
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
103102
return self._post(
104103
"/images/variations",
105104
body=maybe_transform(body, image_create_variation_params.ImageCreateVariationParams),
@@ -179,11 +178,10 @@ def edit(
179178
}
180179
)
181180
files = extract_files(cast(Mapping[str, object], body), paths=[["image"], ["mask"]])
182-
if files:
183-
# It should be noted that the actual Content-Type header that will be
184-
# sent to the server will contain a `boundary` parameter, e.g.
185-
# multipart/form-data; boundary=---abc--
186-
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
181+
# It should be noted that the actual Content-Type header that will be
182+
# sent to the server will contain a `boundary` parameter, e.g.
183+
# multipart/form-data; boundary=---abc--
184+
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
187185
return self._post(
188186
"/images/edits",
189187
body=maybe_transform(body, image_edit_params.ImageEditParams),
@@ -343,11 +341,10 @@ async def create_variation(
343341
}
344342
)
345343
files = extract_files(cast(Mapping[str, object], body), paths=[["image"]])
346-
if files:
347-
# It should be noted that the actual Content-Type header that will be
348-
# sent to the server will contain a `boundary` parameter, e.g.
349-
# multipart/form-data; boundary=---abc--
350-
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
344+
# It should be noted that the actual Content-Type header that will be
345+
# sent to the server will contain a `boundary` parameter, e.g.
346+
# multipart/form-data; boundary=---abc--
347+
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
351348
return await self._post(
352349
"/images/variations",
353350
body=await async_maybe_transform(body, image_create_variation_params.ImageCreateVariationParams),
@@ -427,11 +424,10 @@ async def edit(
427424
}
428425
)
429426
files = extract_files(cast(Mapping[str, object], body), paths=[["image"], ["mask"]])
430-
if files:
431-
# It should be noted that the actual Content-Type header that will be
432-
# sent to the server will contain a `boundary` parameter, e.g.
433-
# multipart/form-data; boundary=---abc--
434-
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
427+
# It should be noted that the actual Content-Type header that will be
428+
# sent to the server will contain a `boundary` parameter, e.g.
429+
# multipart/form-data; boundary=---abc--
430+
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
435431
return await self._post(
436432
"/images/edits",
437433
body=await async_maybe_transform(body, image_edit_params.ImageEditParams),

0 commit comments

Comments
 (0)