大文件上传(视觉理解)

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. 上传文件:PUTupload_url

用上一步返回的 upload_urlupload_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

两条硬性要求:

  1. 每片的字节数必须与返回的 content_length 完全一致(它已经签进了地址)。少一字节多一字节都会被存储拒绝。
  2. 必须记下每片 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 不完整 completeparts 没有不重不漏地覆盖 1..part_count;响应里的 missing_part_numbers / unexpected_part_numbers 列出了问题分片。
400 分片未收到 存储侧没有收到 parts 里列出的某些分片(同样在 missing_part_numbers 中),补传后重试 complete
400 分片校验失败 某片的 etag 与存储侧记录不符,或该片大小不等于签发时的 content_length。重传该片后重试 complete
404 /parts 任务不可用 upload_id 不是分片任务、不属于你的组织,或已经完成/删除。