Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ modelRoles:

四套生成接口均可按各自协议字段启用 Search、Image Search、URL Context、Code Execution 和 Maps。Files、Transcribe、Live、Robotics 的请求与事件格式见 [Google AI Studio 协议规范](docs/protocol.md)。

生成请求中的内联附件会优先上传到生成账户的 Drive,之后带相同附件的请求优先发往已上传该附件的账户并直接引用已上传的文件,闲置 1 小时后删除;账户未授予 Drive 权限时保持内联数据发送。图片、音频、视频、PDF 等输入仍需所选模型支持。重复使用的附件可通过 Files 接口上传一次并复用文件 ID。
生成请求中的内联附件会优先上传到生成账户的 Drive,之后带相同附件的请求优先发往已上传该附件的账户并直接引用已上传的文件,闲置 1 小时后删除;账户未授予 Drive 权限或存储配额不足时保持内联数据发送。图片、音频、视频、PDF 等输入仍需所选模型支持。重复使用的附件可通过 Files 接口上传一次并复用文件 ID。

Gemini 附件与视频图片输入支持 `inlineData` / `inline_data`、`fileData` / `file_data`、`mimeType` / `mime_type` 和 `fileUri` / `file_uri`。媒体 Base64 数据支持标准与 URL-safe 字母表、带填充与无填充形式,以及 `data:<MIME>;base64,` 前缀。OpenAI 助手历史中的 Markdown 图片同样支持 URL-safe Base64 和 CR/LF 换行。内联 GIF 和视频表单上传的 GIF 按首帧静态图片转换为 PNG,保留逻辑画布、帧位置与透明背景;经 Files API 上传的 GIF 按首帧转换为白底 JPEG。

Expand Down
2 changes: 1 addition & 1 deletion README_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ Main endpoints:

All four generation APIs can enable Search, Image Search, URL Context, Code Execution, and Maps through their protocol fields. Request and event formats for Files, Transcribe, Live, and Robotics are documented in the [Google AI Studio protocol specification](docs/protocol.md).

Inline attachments in generation requests are preferentially uploaded to the generating account's Drive; later requests carrying the same attachment go first to the account that already holds it and reference the uploaded file, and files idle for 1 hour are deleted. If the account has not granted Drive access, the original inline data is sent instead. Images, audio, video, PDFs, and other inputs must be supported by the selected model. Upload reusable attachments once through the Files API and reuse their file IDs.
Inline attachments in generation requests are preferentially uploaded to the generating account's Drive; later requests carrying the same attachment go first to the account that already holds it and reference the uploaded file, and files idle for 1 hour are deleted. If the account has not granted Drive access or has exceeded its storage quota, the original inline data is sent instead. Images, audio, video, PDFs, and other inputs must be supported by the selected model. Upload reusable attachments once through the Files API and reuse their file IDs.

Gemini attachments and video image inputs accept `inlineData` / `inline_data`, `fileData` / `file_data`, `mimeType` / `mime_type`, and `fileUri` / `file_uri`. Base64 media data supports standard and URL-safe alphabets, padded and unpadded forms, and the `data:<MIME>;base64,` prefix. Markdown images in OpenAI assistant history also support URL-safe Base64 and CR/LF line breaks. Inline GIFs and GIFs uploaded through video multipart requests are converted to PNG using the first frame, preserving the logical canvas, frame position, and transparency; GIFs uploaded through the Files API are converted to JPEG on a white background using the first frame.

Expand Down
2 changes: 1 addition & 1 deletion docs/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -1002,7 +1002,7 @@ GET clients6.google.com Drive v3 discovery(每个账户第一次上传前一

Drive token、上传和下载使用文件所属账户的固定出口。文件 ID 与账户绑定写入 `runtime-state.json`;生成请求可以组合不同账户的文件,其他账户的文件会临时复制到本次生成账户;失败尝试的副本随即回收,成功尝试的副本在请求结束后回收。副本的绑定带自复制起 48 小时的 `expires_at`,服务在请求进行中退出时由到期资源回收删除。

生成请求的内联附件优先上传到本次生成账户,正文使用 Drive file Part。上传的文件以请求给出的文件名命名,没有文件名时为 `inline-media-<序号>`,模型以这个名称引用附件。上传的文件以 `inline-media` 绑定写入该账户的 `runtime-state.json`,记录 MIME、内容的 SHA-256、请求给出的文件名与最近使用时间,不出现在公开文件接口中;同一账户之后的请求中 MIME、内容与文件名都相同、闲置不到 1 小时的附件直接引用该文件,同一请求中相同的附件只上传一次;闲置 1 小时且没有进行中的请求引用的文件,在该账户下一次带内联附件的请求结束时删除,没有后续请求时由到期资源回收删除(见“账户持久状态”节)。`GenerateContent` 返回 404 且请求引用了已上传的内联附件时,服务以 `GET https://www.googleapis.com/drive/v3/files/<ID>` 查询这些文件,解除已不存在的文件的绑定,在同一账户重新上传后重试一次。`GenerateAccessToken` 明确返回 `401`、Code 16 和 `OAuth error: unauthorized_client` 时,正文保留原始 inline data Part,账户继续参与普通生成调度;其他令牌错误保持失败语义。上传使用公开适配器输出的 MIME 和字节内容。内联附件、Files API 上传与下载的外部媒体的 MIME 缺失、不含子类型或为 `application/octet-stream` 时按内容与文件名识别;内容为文本而 MIME 不是 `text/*`、`application/json` 或 `application/x-ipynb+json` 时以 `text/plain` 发送。图片、音频、视频、PDF 等附件的支持范围由所选模型决定。普通文本和 YouTube 外部媒体保持各自的 Part 编码。
生成请求的内联附件优先上传到本次生成账户,正文使用 Drive file Part。上传的文件以请求给出的文件名命名,没有文件名时为 `inline-media-<序号>`,模型以这个名称引用附件。上传的文件以 `inline-media` 绑定写入该账户的 `runtime-state.json`,记录 MIME、内容的 SHA-256、请求给出的文件名与最近使用时间,不出现在公开文件接口中;同一账户之后的请求中 MIME、内容与文件名都相同、闲置不到 1 小时的附件直接引用该文件,同一请求中相同的附件只上传一次;闲置 1 小时且没有进行中的请求引用的文件,在该账户下一次带内联附件的请求结束时删除,没有后续请求时由到期资源回收删除(见“账户持久状态”节)。`GenerateContent` 返回 404 且请求引用了已上传的内联附件时,服务以 `GET https://www.googleapis.com/drive/v3/files/<ID>` 查询这些文件,解除已不存在的文件的绑定,在同一账户重新上传后重试一次。`GenerateAccessToken` 明确返回 `401`、Code 16 和 `OAuth error: unauthorized_client`,或 Drive 上传遇到存储配额不足(`storageQuotaExceeded`)时,正文保留原始 inline data Part,账户继续参与普通生成调度;其他令牌与上传错误保持失败语义。上传使用公开适配器输出的 MIME 和字节内容。内联附件、Files API 上传与下载的外部媒体的 MIME 缺失、不含子类型或为 `application/octet-stream` 时按内容与文件名识别;内容为文本而 MIME 不是 `text/*`、`application/json` 或 `application/x-ipynb+json` 时以 `text/plain` 发送。图片、音频、视频、PDF 等附件的支持范围由所选模型决定。普通文本和 YouTube 外部媒体保持各自的 Part 编码。

`POST /v1/files` 的未知长度上传使用 Drive resumable upload 和 8 MiB 分块,`GET /v1/files/{id}` 从资源绑定读取文件名、大小、purpose 与创建时间;客户端取消会终止上传并释放账户租约。公开文件字段见“Files、Transcribe 与媒体”节。

Expand Down
21 changes: 21 additions & 0 deletions internal/aistudio/compatibility_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,27 @@ func TestRPCErrorCompatibility(t *testing.T) {
}
}
})

t.Run("drive storage quota exceeded", func(t *testing.T) {
for _, err := range []*RPCError{
{Method: "DriveUpload", StatusCode: http.StatusForbidden, Message: `{"error":{"code":403,"message":"The user's Drive storage quota has been exceeded.","errors":[{"message":"The user's Drive storage quota has been exceeded.","domain":"usageLimits","reason":"storageQuotaExceeded"}]}}`},
{Method: "DriveUpload", StatusCode: http.StatusForbidden, Message: `{"error":{"code":403,"message":"Service disabled","errors":[{"reason":"serviceDisabled"}]}}`},
} {
if !driveStorageQuotaExceeded(err) {
t.Fatalf("expected storage quota error classified: %+v", err)
}
}

for _, err := range []*RPCError{
{Method: "DriveUpload", StatusCode: http.StatusBadRequest, Message: `storageQuotaExceeded`},
{Method: "DriveUpload", StatusCode: http.StatusUnauthorized, Message: `storageQuotaExceeded`},
{Method: "DriveUpload", StatusCode: http.StatusForbidden, Message: `rateLimitExceeded`},
} {
if driveStorageQuotaExceeded(err) {
t.Fatalf("unrelated error classified as drive storage quota: %+v", err)
}
}
})
}

// TestEncodeContentsEmptyPartsFilter 验证多轮历史中空 parts 内容被自动忽略而不触发编码校验失败
Expand Down
18 changes: 18 additions & 0 deletions internal/aistudio/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,20 @@ func driveAuthorizationMissing(err error) bool {
strings.Contains(strings.ToLower(rpcError.Message), "unauthorized_client")
}

// driveStorageQuotaExceeded 判断账户 Google Drive 空间不足或服务未启用
func driveStorageQuotaExceeded(err error) bool {
var rpcErr *RPCError
if !errors.As(err, &rpcErr) || rpcErr == nil || rpcErr.StatusCode != http.StatusForbidden {
return false
}

msg := strings.ToLower(rpcErr.Message)

return strings.Contains(msg, "storagequotaexceeded") ||
strings.Contains(msg, "quota has been exceeded") ||
strings.Contains(msg, "servicedisabled")
}

// CopyFileReferencesToLease 将文件引用复制到目标账户并返回改写内容
func (s *PooledService) CopyFileReferencesToLease(
ctx context.Context,
Expand Down Expand Up @@ -1181,6 +1195,10 @@ func (s *PooledService) UploadInlineMediaToLease(
}
temporary.release = s.useInlineMedia(used)
if uploadErr != nil {
if driveStorageQuotaExceeded(uploadErr) {
_ = temporary.Cleanup()
return rewritten, temporary, nil
}
if DefinitiveAuthenticationFailure(uploadErr) {
uploadErr = errors.Join(uploadErr, target.ConfirmAuthenticationFailure(targetCtx, uploadErr))
}
Expand Down