Skip to content

Commit 8496604

Browse files
fix(client): always respect content-type multipart/form-data if provided (#1519)
1 parent 59e2b8b commit 8496604

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
@@ -110,11 +110,10 @@ def create(
110110
}
111111
)
112112
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
113-
if files:
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 {})}
113+
# It should be noted that the actual Content-Type header that will be
114+
# sent to the server will contain a `boundary` parameter, e.g.
115+
# multipart/form-data; boundary=---abc--
116+
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
118117
return self._post(
119118
"/files",
120119
body=maybe_transform(body, file_create_params.FileCreateParams),
@@ -370,11 +369,10 @@ async def create(
370369
}
371370
)
372371
files = extract_files(cast(Mapping[str, object], body), paths=[["file"]])
373-
if files:
374-
# It should be noted that the actual Content-Type header that will be
375-
# sent to the server will contain a `boundary` parameter, e.g.
376-
# multipart/form-data; boundary=---abc--
377-
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
372+
# It should be noted that the actual Content-Type header that will be
373+
# sent to the server will contain a `boundary` parameter, e.g.
374+
# multipart/form-data; boundary=---abc--
375+
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
378376
return await self._post(
379377
"/files",
380378
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)