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:
@@ -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)
|
||||
|
||||
@@ -172,6 +172,24 @@ func TestHydrateProviderRequestAssetsConvertsGeminiInlineDataAssetToRawBase64(t
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeminiVeoForcesOfficialInlineImageFormat(t *testing.T) {
|
||||
candidate := store.RuntimeModelCandidate{
|
||||
Provider: "gemini",
|
||||
ProviderModelName: "veo-3.1-generate-preview",
|
||||
ModelType: "image_to_video",
|
||||
Capabilities: map[string]any{
|
||||
"image_to_video": map[string]any{
|
||||
"support_url_input": true,
|
||||
"support_base64_input": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
asset := store.RequestAsset{URL: "https://cdn.example.com/input.png", StorageProvider: "remote", ContentType: "image/png"}
|
||||
if got := requestAssetHydrationForField([]string{"image"}, asset, candidate); got != requestAssetHydrateDataURL {
|
||||
t.Fatalf("Gemini Veo must hydrate public image URLs as data URLs, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHydrateProviderRequestAssetsConvertsVolcesImageURLAssetToDataURL(t *testing.T) {
|
||||
storageDir := t.TempDir()
|
||||
fileName := "gateway-request-asset-chat-image.png"
|
||||
|
||||
@@ -1919,6 +1919,9 @@ func compatibilitySourceProtocol(kind string, candidate store.RuntimeModelCandid
|
||||
return clients.ProtocolKlingV1Omni
|
||||
}
|
||||
case "gemini":
|
||||
if kind == "videos.generations" && strings.Contains(strings.ToLower(strings.TrimSpace(firstNonEmptyString(candidate.ProviderModelName, candidate.ModelName))), "veo") {
|
||||
return clients.ProtocolGeminiVeo
|
||||
}
|
||||
return clients.ProtocolGeminiGenerateContent
|
||||
case "openai":
|
||||
switch kind {
|
||||
|
||||
Reference in New Issue
Block a user