fix(provider): 修正媒体请求转换与上游错误透传

按上游协议能力延迟处理媒体资源:OpenAI 兼容平台默认使用 multipart,显式配置后才发送 JSON URL;Gemini 官方协议使用 Files API,兼容协议使用内嵌 Base64,并同步覆盖相关媒体客户端。\n\n安全的上游 400/422 原始错误会作为下游 message 返回,同时保留结构化诊断信息和历史任务兼容。\n\n验证:API 全量无缓存测试、go vet、pnpm lint、pnpm test、pnpm build、pnpm openapi、git diff --check。
This commit is contained in:
2026-08-05 00:40:42 +08:00
parent c79c2a7b44
commit ebdb96e7d7
19 changed files with 1163 additions and 93 deletions
+18 -6
View File
@@ -33,7 +33,7 @@ func (c GeminiClient) runVeo(ctx context.Context, request Request, apiKey string
var wire *WireResponse
if operationName == "" {
body, err := geminiVeoBody(request)
body, err := geminiVeoBody(ctx, request)
if err != nil {
return Response{}, err
}
@@ -107,7 +107,7 @@ func (c GeminiClient) runVeo(ctx context.Context, request Request, apiKey string
}
}
func geminiVeoBody(request Request) (map[string]any, error) {
func geminiVeoBody(ctx context.Context, request Request) (map[string]any, error) {
prompt := firstNonEmptyPrompt(request.Body, "")
if prompt == "" {
return nil, &ClientError{Code: "invalid_parameter", Message: "gemini Veo prompt is required", Param: "prompt", StatusCode: http.StatusBadRequest, Retryable: false}
@@ -116,14 +116,14 @@ func geminiVeoBody(request Request) (map[string]any, error) {
firstFrame, lastFrame, references := geminiVeoImageInputs(request.Body)
instance := map[string]any{"prompt": prompt}
if firstFrame.URI != "" {
image, err := geminiVeoInlineImage(firstFrame)
image, err := geminiVeoInlineImage(ctx, firstFrame)
if err != nil {
return nil, err
}
instance["image"] = image
}
if lastFrame.URI != "" {
image, err := geminiVeoInlineImage(lastFrame)
image, err := geminiVeoInlineImage(ctx, lastFrame)
if err != nil {
return nil, err
}
@@ -135,7 +135,7 @@ func geminiVeoBody(request Request) (map[string]any, error) {
if len(references) > 0 {
items := make([]any, 0, len(references))
for _, reference := range references {
image, err := geminiVeoInlineImage(reference)
image, err := geminiVeoInlineImage(ctx, reference)
if err != nil {
return nil, err
}
@@ -340,13 +340,20 @@ func deduplicateGeminiVeoImages(images []geminiVeoImage, exclusions ...string) [
return out
}
func geminiVeoInlineImage(image geminiVeoImage) (map[string]any, error) {
func geminiVeoInlineImage(ctx context.Context, image geminiVeoImage) (map[string]any, error) {
parsed := geminiDataURL(image.URI)
mimeType := strings.TrimSpace(image.MimeType)
data := ""
if parsed != nil {
data = parsed.data
mimeType = firstNonEmptyString(mimeType, parsed.mimeType)
} else if requestAssetStringIsHTTPURL(image.URI) {
fetchedMimeType, payload, err := fetchRemoteMediaInputPayload(ctx, image.URI, 256<<20)
if err != nil {
return nil, err
}
data = base64.StdEncoding.EncodeToString(payload)
mimeType = firstNonEmptyString(mimeType, fetchedMimeType)
} else if !requestLikeURL(image.URI) {
data = strings.TrimSpace(image.URI)
}
@@ -368,6 +375,11 @@ func geminiVeoInlineImage(image geminiVeoImage) (map[string]any, error) {
return map[string]any{"bytesBase64Encoded": data, "mimeType": geminiMediaMime(mimeType, "image")}, nil
}
func requestAssetStringIsHTTPURL(value string) bool {
parsed, err := url.Parse(strings.TrimSpace(value))
return err == nil && parsed.Host != "" && (strings.EqualFold(parsed.Scheme, "http") || strings.EqualFold(parsed.Scheme, "https"))
}
func requestLikeURL(value string) bool {
normalized := strings.ToLower(strings.TrimSpace(value))
return strings.HasPrefix(normalized, "http://") || strings.HasPrefix(normalized, "https://") || strings.HasPrefix(normalized, "/static/")