feat(gemini): 接入 Veo 官方异步生成协议

将 Gemini Veo 视频请求改为 predictLongRunning 提交与 Operation 轮询,支持任务恢复、首尾帧和参考图的官方 Base64 字段映射。

完成后由网关携带 API Key 从受信任的 Gemini 文件域下载视频,再进入现有对象存储转存链路,避免暴露鉴权地址。

验证:env -u AI_GATEWAY_TEST_DATABASE_URL go test ./... -count=1;go vet ./...;真实 Veo 3.1 任务生成并下载 1633544 字节视频。
This commit is contained in:
2026-08-04 16:27:24 +08:00
parent 5c7d6ac9aa
commit 44d5cf2b9d
7 changed files with 951 additions and 1 deletions
+9 -1
View File
@@ -164,7 +164,7 @@ func (s *Service) hydrateProviderRequestAssetString(ctx context.Context, value s
return value, nil
}
style, ok := requestAssetHydrateUnsupported, false
if openAIImageEditRequiresMultipartBytes(candidate) {
if geminiVeoRequiresInlineImage(candidate) || openAIImageEditRequiresMultipartBytes(candidate) {
style, ok = requestAssetHydrateDataURL, true
} else {
style, ok = requestAssetCapabilityHydrationForMedia("image", candidate, raw, "")
@@ -395,6 +395,9 @@ func requestAssetHydrationForField(path []string, asset store.RequestAsset, cand
return requestAssetHydrateDataURL
}
if requestAssetMediaKindForHydration(path, asset) == "image" {
if geminiVeoRequiresInlineImage(candidate) {
return requestAssetHydrateDataURL
}
if openAIImageEditRequiresMultipartBytes(candidate) {
return requestAssetHydrateDataURL
}
@@ -418,6 +421,11 @@ func openAIImageEditRequiresMultipartBytes(candidate store.RuntimeModelCandidate
strings.TrimSpace(candidate.ModelType) == "image_edit"
}
func geminiVeoRequiresInlineImage(candidate store.RuntimeModelCandidate) bool {
return normalizeProviderKey(candidate.Provider) == "gemini" &&
strings.Contains(strings.ToLower(strings.TrimSpace(firstNonEmptyString(candidate.ProviderModelName, candidate.ModelName))), "veo")
}
func requestAssetMediaKindForHydration(path []string, asset store.RequestAsset) string {
if mediaURLFieldNeedsHydration(path) {
return requestAssetMediaURLKind(path)