大文件上传(视觉理解)
POST /v1/uploads · POST /v1/uploads/{id}/parts · POST /v1/uploads/{id}/complete · GET /v1/uploads/{id} · DELETE /v1/uploads/{id}
给 Gemini 多模态模型做大文件视觉理解(视频 / 图片 / 音频 / PDF)时,如果素材超过约 15 MB,既不能内联 base64(整个请求体上限 20 MB),公网 https:// URL 也只会被模型服务读取前 ~15 MB(超出部分被静默丢弃,不会报错)。本接口解决这个问题:把大文件先上传到我们的存储,拿到一个 gs:// 引用,再把它作为 Gemini 原生接口 的 fileData.fileUri 使用,模型即可完整理解整个文件。
流程三步:① 创建上传任务拿直传地址 → ② 把文件直传上去 → ③ 完成并取回 gs:// 引用。
大文件(≥ 约 64 MB)请开分片并行上传:在第 ① 步加上
"multipart": true,第 ② 步就变成多条连接同时传多个分片,速度和成功率都显著更高——见 第 6 节。这是显式开关,不加该字段的请求行为完全不变。
单次使用,用完即删:拿到的
gs://引用只能用一次。第一次在 Gemini 请求里引用并成功返回后,该文件会被自动删除。若同一文件要在多轮对话里反复引用,见 第 4 节 的X-Upload-Single-Use说明。文件直传我们的存储,不经过网关:第 ② 步的字节走预签名直传地址,不占用你的 API 额度、也不受请求体大小限制。
有效期:上传地址(第 ①→② 步)默认 1 小时内有效(分片上传的各分片地址默认 6 小时);完成后的
gs://引用若一直不使用,最长保留约 24 小时后自动清理。
1. 创建上传任务:POST /v1/uploads
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
size_bytes |
int | 是 | 文件的精确字节数(会签进上传地址,直传时大小不符会被拒)。 |
mime_type |
string | 是 | 文件的 MIME 类型。支持 video/*、image/*、audio/*、application/pdf。 |
multipart |
bool | 否 | 传 true 开启分片并行上传(大文件强烈推荐,见 第 6 节)。不传或传其它值一律走本节的单次直传,行为与以往完全一致。 |
curl https://api.aiin1.ai/v1/uploads \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "size_bytes": 33852748, "mime_type": "video/mp4" }'返回
{
"upload_id": "upload_2b9574243b714a13a9fceda4c152f314",
"upload_mode": "single",
"upload_url": "https://<account>.r2.cloudflarestorage.com/...(预签名直传地址)...",
"upload_method": "PUT",
"upload_headers": { "Content-Type": "video/mp4", "Content-Length": "33852748" },
"upload_expires_at": "2026-07-23T13:10:38+00:00",
"single_use": true
}| 字段 | 说明 |
|---|---|
upload_id |
上传任务 ID,后续 complete / 查询 / 删除都用它。 |
upload_mode |
single = 本节的单次直传;multipart = 分片并行上传(仅在请求里显式传 "multipart": true 时出现)。 |
upload_url |
预签名直传地址,用第 2 步的 PUT 把文件上传到这里。 |
upload_headers |
直传时必须原样带上的请求头(它们已签进地址,不一致会被拒)。 |
upload_expires_at |
直传地址的过期时间(默认 1 小时)。 |
2. 上传文件:PUT 到 upload_url
用上一步返回的 upload_url 和 upload_headers,把文件字节 PUT 上去。这一步直连存储,不经过 API 网关:
curl -X PUT "<上一步返回的 upload_url>" \
-H "Content-Type: video/mp4" \
--data-binary @my-video.mp4上传成功返回 200(无响应体)。Content-Type 必须与创建时的 mime_type 一致,上传的字节数必须等于 size_bytes,否则存储会拒绝。
3. 完成并取回引用:POST /v1/uploads/{id}/complete
文件传完后调用 complete。接口会校验文件已上传且大小正确,把它转存为一个可供模型服务读取的 gs:// 引用,同步等待转存完成(200 MB 通常几秒),然后返回:
curl -X POST https://api.aiin1.ai/v1/uploads/upload_2b9574243b714a13a9fceda4c152f314/complete \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'返回
{
"upload_id": "upload_2b9574243b714a13a9fceda4c152f314",
"file_uri": "gs://.../30367d4d.../upload_2b9574243b714a13a9fceda4c152f314",
"uri_type": "gcs",
"size_bytes": 33852748,
"single_use": true
}| 字段 | 说明 |
|---|---|
file_uri |
直接粘进 Gemini 请求 fileData.fileUri 的引用(见第 4 节)。 |
uri_type |
引用类型,当前恒为 gcs。 |
complete 是幂等的:文件已就绪时重复调用返回同一个 file_uri;转存仍在进行时返回 409(可短暂等待后重试)。
4. 在 Gemini 原生接口中使用
把 file_uri 放进 Gemini 原生接口 请求的 fileData(注意 mimeType 要与文件一致):
curl https://api.aiin1.ai/v1beta/models/gemini-3.1-pro-preview:generateContent \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contents": [{
"parts": [
{"text": "详细描述这段视频的内容。"},
{"fileData": {"mimeType": "video/mp4", "fileUri": "gs://.../upload_2b9574243b714a13a9fceda4c152f314"}}
]
}]
}'单次使用:上面的请求成功返回后,
file_uri指向的文件会被自动删除,再次引用会失败。多轮复用:若同一文件要在多轮对话里反复引用,在 Gemini 请求里加请求头
X-Upload-Single-Use: off——这样引用后不会立即删除,文件按有效期(约 24 小时)到期后再清理。
5. 查询与删除(可选)
按 upload_id 查状态:
curl https://api.aiin1.ai/v1/uploads/upload_2b9574243b714a13a9fceda4c152f314 \
-H "Authorization: Bearer YOUR_API_KEY"{ "upload_id": "upload_2b9574243b714a13a9fceda4c152f314", "status": "ready", "file_uri": "gs://.../upload_2b9574243b714a13a9fceda4c152f314", "uri_type": "gcs", "size_bytes": 33852748, "mime_type": "video/mp4", "created_at": "2026-07-23T12:10:38+00:00", "ready_at": "2026-07-23T12:11:02+00:00" }status 取值:pending_upload(待直传)/ copying(转存中)/ ready(可用)/ consumed(已被引用)/ deleted / expired / copy_failed。
主动删除(用完即删的情况下一般无需手动删):
curl -X DELETE https://api.aiin1.ai/v1/uploads/upload_2b9574243b714a13a9fceda4c152f314 \
-H "Authorization: Bearer YOUR_API_KEY"{ "upload_id": "upload_2b9574243b714a13a9fceda4c152f314", "deleted": true }非本组织的 upload_id 一律返回 404。
6. 分片并行上传(大文件推荐,需显式开启)
单次直传只用一条连接把整个文件推上去,跨境链路上一条连接的带宽很有限:实测同一条线路,90 MB 以内约 1 MB/s,再大就掉到 0.1 MB/s 左右——几百 MB 的视频往往传不完就断了。分片上传把文件切成若干块,多条连接同时传,每块独立重试,断了只重传那一块。
什么时候用:文件 ≥ 约 64 MB(尤其是视频)一律建议开分片;更小的文件用单次直传更简单。
必须显式开启:只有创建请求里带
"multipart": true才走分片。不带这个字段的请求行为与以往完全一致,不会因为文件变大就自动切换。
6.1 创建分片上传任务
curl https://api.aiin1.ai/v1/uploads \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"size_bytes": 734003200,
"mime_type": "video/mp4",
"multipart": true
}'| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
size_bytes |
int | 是 | 文件的精确字节数。 |
mime_type |
string | 是 | 同单次直传。 |
multipart |
bool | 是 | 必须是布尔 true。 |
part_size_bytes |
int | 否 | 期望的分片大小。会被规整到 5 MiB ~ 256 MiB 区间;若按该大小切出的片数超过 10000,会自动向上取整到能满足上限的大小。不传则用默认值(16 MiB)。 |
返回
{
"upload_id": "upload_2b9574243b714a13a9fceda4c152f314",
"upload_mode": "multipart",
"multipart_upload_id": "ABPnzm...(存储侧的分片会话 ID)",
"part_size_bytes": 16777216,
"part_count": 44,
"parts": [
{ "part_number": 1, "upload_url": "https://...", "upload_method": "PUT", "content_length": 16777216 },
{ "part_number": 2, "upload_url": "https://...", "upload_method": "PUT", "content_length": 16777216 },
{ "part_number": 44, "upload_url": "https://...", "upload_method": "PUT", "content_length": 3145728 }
],
"parallelism_hint": 8,
"upload_expires_at": "2026-07-23T18:10:38+00:00",
"single_use": true
}| 字段 | 说明 |
|---|---|
multipart_upload_id |
分片会话 ID,排查问题时提供给我们即可;客户端本身不需要拼接它。 |
part_size_bytes |
实际采用的分片大小(可能与你请求的不同,见上表)。 |
part_count |
分片总数。分片编号从 1 开始连续到 part_count。 |
parts[].upload_url |
该分片的预签名直传地址。 |
parts[].content_length |
该分片必须上传的精确字节数。除最后一片外都等于 part_size_bytes,最后一片是余数。 |
parallelism_hint |
建议的并发数(默认 8)。只是建议,你可以自行调整。 |
upload_expires_at |
全部分片地址的过期时间(默认 6 小时,给跨境大文件留足时间)。 |
6.2 并行上传各分片
对第 n 片,取文件的 [(n-1) * part_size_bytes, (n-1) * part_size_bytes + content_length) 区间字节,PUT 到对应的 upload_url:
curl -X PUT "<parts[0].upload_url>" \
--data-binary @chunk-1.bin两条硬性要求:
- 每片的字节数必须与返回的
content_length完全一致(它已经签进了地址)。少一字节多一字节都会被存储拒绝。 - 必须记下每片
PUT响应头里的ETag,第 6.4 步complete时要按分片编号回传。ETag带不带双引号都可以,我们会自动规整。
这一步同样直连存储、不经过 API 网关,不占用你的 API 额度。
6.3 断点续传 / 重新签发地址:POST /v1/uploads/{id}/parts
上传中途地址过期、进程重启、或某几片一直失败时,调这个接口:它会重新签发分片地址,并告诉你存储侧已经收到了哪些片,这样你只补传缺的那几片。
curl -X POST https://api.aiin1.ai/v1/uploads/upload_2b95.../parts \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "part_numbers": [17, 23] }' # 不传 part_numbers 则重签全部分片{
"upload_id": "upload_2b9574243b714a13a9fceda4c152f314",
"upload_mode": "multipart",
"part_size_bytes": 16777216,
"part_count": 44,
"parts": [
{ "part_number": 17, "upload_url": "https://...", "upload_method": "PUT", "content_length": 16777216 },
{ "part_number": 23, "upload_url": "https://...", "upload_method": "PUT", "content_length": 16777216 }
],
"received": [
{ "part_number": 1, "size": 16777216, "etag": "9f2b...c1" },
{ "part_number": 2, "size": 16777216, "etag": "77ad...3e" }
],
"parallelism_hint": 8,
"upload_expires_at": "2026-07-23T21:10:38+00:00"
}received 里已有的分片可以直接跳过——它的 etag 就是 complete 时要回传的值。该接口仅对尚未完成的分片上传任务有效,其它情况返回 404。
6.4 完成:POST /v1/uploads/{id}/complete
分片上传的 complete 必须带上全部分片的 part_number + etag:
curl -X POST https://api.aiin1.ai/v1/uploads/upload_2b95.../complete \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"parts": [
{ "part_number": 1, "etag": "9f2b...c1" },
{ "part_number": 2, "etag": "77ad...3e" },
{ "part_number": 44, "etag": "0c51...9a" }
]
}'parts 必须不重不漏地覆盖 1 ~ part_count。缺片或多片会返回 400,并在响应里直接列出问题分片:
{
"error": { "type": "invalid_request_error", "message": "parts must cover exactly 1..44" },
"missing_part_numbers": [17],
"unexpected_part_numbers": [45]
}拿到这个响应后,补传缺的分片(可先用第 6.3 步重签地址)再重新调用 complete 即可——任务不会被作废,状态仍是 pending_upload。
成功后的返回与单次直传完全一致(file_uri / uri_type / size_bytes / single_use),后续用法见 第 4 节。region 字段同样可用。
不再需要的分片任务请调 DELETE /v1/uploads/{id}:它会同时终止分片会话,释放已上传的分片。
6.5 完整示例(Python,8 线程并发 + 单片重试)
import os, requests
from concurrent.futures import ThreadPoolExecutor
API = "https://api.aiin1.ai"
KEY = os.environ["AIONE_API_KEY"]
PATH = "my-video.mp4"
SIZE = os.path.getsize(PATH)
# 1) 创建分片上传任务
task = requests.post(
f"{API}/v1/uploads",
headers={"Authorization": f"Bearer {KEY}"},
json={"size_bytes": SIZE, "mime_type": "video/mp4", "multipart": True},
timeout=30,
).json()
part_size = task["part_size_bytes"]
def put_part(part):
"""上传一片,返回 (part_number, etag);失败重试 3 次。"""
offset = (part["part_number"] - 1) * part_size
with open(PATH, "rb") as f:
f.seek(offset)
chunk = f.read(part["content_length"]) # 必须正好这么多字节
for attempt in range(3):
try:
r = requests.put(part["upload_url"], data=chunk, timeout=600)
r.raise_for_status()
return part["part_number"], r.headers["ETag"] # 带不带引号都行
except Exception:
if attempt == 2:
raise
# 2) 并发上传(并发数用服务端给的建议值)
with ThreadPoolExecutor(max_workers=task["parallelism_hint"]) as pool:
done = list(pool.map(put_part, task["parts"]))
# 3) 完成,拿到 gs:// 引用
res = requests.post(
f"{API}/v1/uploads/{task['upload_id']}/complete",
headers={"Authorization": f"Bearer {KEY}"},
json={"parts": [{"part_number": n, "etag": e} for n, e in done]},
timeout=180,
).json()
print(res["file_uri"]) # gs://... 直接放进 Gemini 请求的 fileData.fileUri如果某几片反复失败,用
POST /v1/uploads/{id}/parts重签地址、对照received只补缺失的片,然后再调complete,不必从头重传。
7. 常见错误
| HTTP | 场景 | 说明 |
|---|---|---|
| 400 | size_bytes / mime_type 非法 |
size_bytes 必须为正整数且不超过上限;mime_type 仅支持 video/* image/* audio/* application/pdf。 |
| 400 | complete 时文件未上传 |
先完成第 2 步的 PUT 再调 complete。 |
| 400 | 大小不符 | 直传的字节数与创建时的 size_bytes 不一致。 |
| 404 | 上传任务不存在 / 非本组织 | upload_id 错误或不属于你的组织。 |
| 404 | 功能未开通 | 你的组织所在线路未开通大文件上传,请联系客服。 |
| 409 | 转存进行中 | complete 正在转存,Retry-After 秒后重试即可。 |
| 410 | 引用已失效 | 文件已被引用(单次使用)、已删除或已过期。 |
| 429 | 超出每日配额 | 单组织每日创建的上传任务数达到上限。 |
| 400 | 分片功能未开启 | 该线路暂未开启分片上传(multipart uploads are disabled),请改用单次直传或联系客服。 |
| 400 | 分片数超限 | 文件按最大分片(256 MiB)切分仍超过 10000 片。当前单文件上限内不会发生,若遇到请联系客服。 |
| 400 | parts 不完整 |
complete 的 parts 没有不重不漏地覆盖 1..part_count;响应里的 missing_part_numbers / unexpected_part_numbers 列出了问题分片。 |
| 400 | 分片未收到 | 存储侧没有收到 parts 里列出的某些分片(同样在 missing_part_numbers 中),补传后重试 complete。 |
| 400 | 分片校验失败 | 某片的 etag 与存储侧记录不符,或该片大小不等于签发时的 content_length。重传该片后重试 complete。 |
| 404 | /parts 任务不可用 |
该 upload_id 不是分片任务、不属于你的组织,或已经完成/删除。 |