Skip to content

Commit 6f3bce2

Browse files
wanlin31copybara-github
authored andcommitted
feat: Allow custom headers in file upload requests.
PiperOrigin-RevId: 811009698
1 parent aaac8d8 commit 6f3bce2

File tree

4 files changed

+275
-82
lines changed

4 files changed

+275
-82
lines changed

google/genai/_api_client.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
import sys
3535
import threading
3636
import time
37-
from typing import Any, AsyncIterator, Iterator, Optional, Tuple, TYPE_CHECKING, Union
37+
from typing import Any, AsyncIterator, Dict, Set, Iterator, Optional, Tuple, TYPE_CHECKING, Union
3838
from urllib.parse import urlparse
3939
from urllib.parse import urlunparse
4040
import warnings
@@ -160,6 +160,37 @@ def patch_http_options(
160160
return copy_option
161161

162162

163+
def _update_headers_with_append_keys(
164+
original_http_options: Optional[HttpOptions],
165+
patching_http_options: Optional[HttpOptionsOrDict],
166+
) -> Dict[str, Any]:
167+
"""updating headers with append logic for user-agent and x-goog-api-client, and overriding logic for the rest keys."""
168+
original_headers: Dict[str, Any] = {}
169+
if original_http_options:
170+
original_headers = original_http_options.headers or {}
171+
172+
patching_headers: Dict[str, Any] = {}
173+
if patching_http_options:
174+
if isinstance(patching_http_options, HttpOptions):
175+
patching_headers = patching_http_options.headers or {}
176+
else:
177+
patching_headers = patching_http_options.get('headers', {}) or {}
178+
179+
updated_headers = original_headers.copy()
180+
append_keys = {'user-agent', 'x-goog-api-client'}
181+
for key, value in patching_headers.items():
182+
key = key.lower()
183+
if (
184+
key in append_keys
185+
and key in updated_headers
186+
and updated_headers[key]
187+
):
188+
updated_headers[key] = f'{updated_headers[key]}, {value}'
189+
else:
190+
updated_headers[key] = value
191+
return updated_headers
192+
193+
163194
def populate_server_timeout_header(
164195
headers: dict[str, str], timeout_in_seconds: Optional[Union[float, int]]
165196
) -> None:
@@ -1379,6 +1410,8 @@ def upload_file(
13791410
file, upload_url, upload_size, http_options=http_options
13801411
)
13811412

1413+
1414+
13821415
def _upload_fd(
13831416
self,
13841417
file: io.IOBase,
@@ -1424,7 +1457,10 @@ def _upload_fd(
14241457
else self._http_options.timeout
14251458
)
14261459
timeout_in_seconds = get_timeout_in_seconds(timeout)
1460+
1461+
updated_headers = _update_headers_with_append_keys(self._http_options, http_options)
14271462
upload_headers = {
1463+
**updated_headers,
14281464
'X-Goog-Upload-Command': upload_command,
14291465
'X-Goog-Upload-Offset': str(offset),
14301466
'Content-Length': str(chunk_size),
@@ -1439,6 +1475,7 @@ def _upload_fd(
14391475
content=file_chunk,
14401476
timeout=timeout_in_seconds,
14411477
)
1478+
errors.APIError.raise_for_response(response)
14421479
if response.headers.get('x-goog-upload-status'):
14431480
break
14441481
delay_seconds = INITIAL_RETRY_DELAY * (DELAY_MULTIPLIER**retry_count)
@@ -1455,7 +1492,7 @@ def _upload_fd(
14551492
)
14561493

14571494
if response.headers.get('x-goog-upload-status') != 'final':
1458-
raise ValueError('Failed to upload file: Upload status is not finalized.')
1495+
raise ValueError('Failed to upload file, upload status is not finalized.')
14591496
return HttpResponse(response.headers, response_stream=[response.text])
14601497

14611498
def download_file(
@@ -1550,6 +1587,7 @@ async def _async_upload_fd(
15501587
returns:
15511588
The HttpResponse object from the finalized request.
15521589
"""
1590+
updated_headers = _update_headers_with_append_keys(self._http_options, http_options)
15531591
offset = 0
15541592
# Upload the file in chunks
15551593
if self._use_aiohttp(): # pylint: disable=g-import-not-at-top
@@ -1569,8 +1607,8 @@ async def _async_upload_fd(
15691607
http_options = http_options if http_options else self._http_options
15701608
timeout = (
15711609
http_options.get('timeout')
1572-
if isinstance(http_options, dict)
1573-
else http_options.timeout
1610+
if isinstance(http_options, dict)
1611+
else http_options.timeout
15741612
)
15751613
if timeout is None:
15761614
# Per request timeout is not configured. Check the global timeout.
@@ -1580,9 +1618,12 @@ async def _async_upload_fd(
15801618
else self._http_options.timeout
15811619
)
15821620
timeout_in_seconds = get_timeout_in_seconds(timeout)
1621+
1622+
# Define and merge headers, with upload headers taking priority
15831623
upload_headers = {
1624+
**updated_headers,
15841625
'X-Goog-Upload-Command': upload_command,
1585-
'X-Goog-Upload-Offset': str(offset),
1626+
'X-Goog-Upload-Offset': str(offset),
15861627
'Content-Length': str(chunk_size),
15871628
}
15881629
populate_server_timeout_header(upload_headers, timeout_in_seconds)
@@ -1597,7 +1638,7 @@ async def _async_upload_fd(
15971638
headers=upload_headers,
15981639
timeout=aiohttp.ClientTimeout(connect=timeout_in_seconds),
15991640
)
1600-
1641+
await errors.APIError.raise_for_async_response(response)
16011642
if response.headers.get('X-Goog-Upload-Status'):
16021643
break
16031644
delay_seconds = INITIAL_RETRY_DELAY * (DELAY_MULTIPLIER**retry_count)
@@ -1654,7 +1695,10 @@ async def _async_upload_fd(
16541695
else self._http_options.timeout
16551696
)
16561697
timeout_in_seconds = get_timeout_in_seconds(timeout)
1698+
1699+
# Define and merge headers, with upload headers taking priority
16571700
upload_headers = {
1701+
**updated_headers,
16581702
'X-Goog-Upload-Command': upload_command,
16591703
'X-Goog-Upload-Offset': str(offset),
16601704
'Content-Length': str(chunk_size),
@@ -1677,6 +1721,7 @@ async def _async_upload_fd(
16771721
and client_response.headers.get('x-goog-upload-status')
16781722
):
16791723
break
1724+
await errors.APIError.raise_for_async_response(client_response)
16801725
delay_seconds = INITIAL_RETRY_DELAY * (DELAY_MULTIPLIER**retry_count)
16811726
retry_count += 1
16821727
time.sleep(delay_seconds)

google/genai/files.py

Lines changed: 73 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -636,33 +636,31 @@ def upload(
636636
'Unknown mime type: Could not determine the mimetype for your'
637637
' file\n please set the `mime_type` argument'
638638
)
639+
request_specific_headers = (
640+
(config_model.http_options.headers or {})
641+
if config_model.http_options
642+
else {}
643+
)
644+
645+
# Define and merge headers, with upload headers taking priority
646+
create_final_headers = {
647+
**request_specific_headers,
648+
'Content-Type': 'application/json',
649+
'X-Goog-Upload-Protocol': 'resumable',
650+
'X-Goog-Upload-Command': 'start',
651+
'X-Goog-Upload-Header-Content-Length': f'{file_obj.size_bytes}',
652+
'X-Goog-Upload-Header-Content-Type': f'{file_obj.mime_type}',
653+
}
654+
655+
create_http_options = types.HttpOptions(
656+
api_version='',
657+
headers=create_final_headers,
658+
)
639659

640-
http_options: types.HttpOptions
641-
if config_model and config_model.http_options:
642-
http_options = config_model.http_options
643-
http_options.api_version = ''
644-
http_options.headers = {
645-
'Content-Type': 'application/json',
646-
'X-Goog-Upload-Protocol': 'resumable',
647-
'X-Goog-Upload-Command': 'start',
648-
'X-Goog-Upload-Header-Content-Length': f'{file_obj.size_bytes}',
649-
'X-Goog-Upload-Header-Content-Type': f'{file_obj.mime_type}',
650-
}
651-
else:
652-
http_options = types.HttpOptions(
653-
api_version='',
654-
headers={
655-
'Content-Type': 'application/json',
656-
'X-Goog-Upload-Protocol': 'resumable',
657-
'X-Goog-Upload-Command': 'start',
658-
'X-Goog-Upload-Header-Content-Length': f'{file_obj.size_bytes}',
659-
'X-Goog-Upload-Header-Content-Type': f'{file_obj.mime_type}',
660-
},
661-
)
662660
response = self._create(
663661
file=file_obj,
664662
config=types.CreateFileConfig(
665-
http_options=http_options, should_return_http_response=True
663+
http_options=create_http_options, should_return_http_response=True
666664
),
667665
)
668666

@@ -679,11 +677,17 @@ def upload(
679677

680678
if isinstance(file, io.IOBase):
681679
return_file = self._api_client.upload_file(
682-
file, upload_url, file_obj.size_bytes, http_options=http_options
680+
file,
681+
upload_url,
682+
file_obj.size_bytes,
683+
http_options=config_model.http_options,
683684
)
684685
else:
685686
return_file = self._api_client.upload_file(
686-
fs_path, upload_url, file_obj.size_bytes, http_options=http_options
687+
fs_path,
688+
upload_url,
689+
file_obj.size_bytes,
690+
http_options=config_model.http_options,
687691
)
688692

689693
return types.File._from_response(
@@ -1122,58 +1126,67 @@ async def upload(
11221126
' file\n please set the `mime_type` argument'
11231127
)
11241128

1125-
http_options: types.HttpOptions
1126-
if config_model and config_model.http_options:
1127-
http_options = config_model.http_options
1128-
http_options.api_version = ''
1129-
http_options.headers = {
1130-
'Content-Type': 'application/json',
1131-
'X-Goog-Upload-Protocol': 'resumable',
1132-
'X-Goog-Upload-Command': 'start',
1133-
'X-Goog-Upload-Header-Content-Length': f'{file_obj.size_bytes}',
1134-
'X-Goog-Upload-Header-Content-Type': f'{file_obj.mime_type}',
1135-
}
1136-
else:
1137-
http_options = types.HttpOptions(
1138-
api_version='',
1139-
headers={
1140-
'Content-Type': 'application/json',
1141-
'X-Goog-Upload-Protocol': 'resumable',
1142-
'X-Goog-Upload-Command': 'start',
1143-
'X-Goog-Upload-Header-Content-Length': f'{file_obj.size_bytes}',
1144-
'X-Goog-Upload-Header-Content-Type': f'{file_obj.mime_type}',
1145-
},
1146-
)
1129+
request_specific_headers = (
1130+
(config_model.http_options.headers or {})
1131+
if config_model.http_options
1132+
else {}
1133+
)
1134+
1135+
# Define and merge headers, with upload headers taking priority
1136+
create_final_headers = {
1137+
**request_specific_headers,
1138+
'Content-Type': 'application/json',
1139+
'X-Goog-Upload-Protocol': 'resumable',
1140+
'X-Goog-Upload-Command': 'start',
1141+
'X-Goog-Upload-Header-Content-Length': f'{file_obj.size_bytes}',
1142+
'X-Goog-Upload-Header-Content-Type': f'{file_obj.mime_type}',
1143+
}
1144+
1145+
create_http_options = types.HttpOptions(
1146+
api_version='',
1147+
headers=create_final_headers,
1148+
)
1149+
11471150
response = await self._create(
11481151
file=file_obj,
11491152
config=types.CreateFileConfig(
1150-
http_options=http_options, should_return_http_response=True
1153+
http_options=create_http_options, should_return_http_response=True
11511154
),
11521155
)
11531156
if (
11541157
response.sdk_http_response is None
11551158
or response.sdk_http_response.headers is None
1156-
or (
1157-
'x-goog-upload-url' not in response.sdk_http_response.headers
1158-
and 'X-Goog-Upload-URL' not in response.sdk_http_response.headers
1159-
)
11601159
):
11611160
raise KeyError(
1162-
'Failed to create file. Upload URL did not returned from the create'
1163-
' file request.'
1161+
'Failed to create file. The SDK HTTP response or its headers were'
1162+
' missing.'
1163+
)
1164+
1165+
upload_url = None
1166+
for key, value in response.sdk_http_response.headers.items():
1167+
if key.lower() == 'x-goog-upload-url':
1168+
upload_url = value
1169+
break # Stop as soon as we find the first match
1170+
1171+
if upload_url is None:
1172+
raise KeyError(
1173+
'Failed to create file. Upload URL was not returned in the response'
1174+
' headers.'
11641175
)
1165-
elif 'x-goog-upload-url' in response.sdk_http_response.headers:
1166-
upload_url = response.sdk_http_response.headers['x-goog-upload-url']
1167-
else:
1168-
upload_url = response.sdk_http_response.headers['X-Goog-Upload-URL']
11691176

11701177
if isinstance(file, io.IOBase):
11711178
return_file = await self._api_client.async_upload_file(
1172-
file, upload_url, file_obj.size_bytes, http_options=http_options
1179+
file,
1180+
upload_url,
1181+
file_obj.size_bytes,
1182+
http_options=config_model.http_options,
11731183
)
11741184
else:
11751185
return_file = await self._api_client.async_upload_file(
1176-
fs_path, upload_url, file_obj.size_bytes, http_options=http_options
1186+
fs_path,
1187+
upload_url,
1188+
file_obj.size_bytes,
1189+
http_options=config_model.http_options,
11771190
)
11781191

11791192
return types.File._from_response(

0 commit comments

Comments
 (0)