fix(media): 统一图片结果 URL 化并限制同步 Base64
将上游 URL 直接持久化,内联媒体经对象存储后仅保留 URL 与内部定位元数据;异步轮询、任务详情和幂等重放统一使用零对象读取的 URL 投影,并增加 64KiB 响应门禁。 OpenAI 图片接口接受 url 与 b64_json,同步 Base64 限制为 20MiB 和每 Pod 2 并发;新增历史结果迁移清零门禁、结果指标和 API GOMEMLIMIT。 验证:API go test ./...、go vet、聚焦 race、pnpm openapi、pnpm lint/test/build、迁移安全检查与 docker compose config 均通过。
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -243,6 +244,13 @@ func (s *Service) observeObjectStorage(event string, provider string, bytes int,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) observeResultStorage(source string) {
|
||||
observer, ok := s.billingMetrics.(interface{ ObserveResultStorage(string) })
|
||||
if ok {
|
||||
observer.ObserveResultStorage(source)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Execute(ctx context.Context, task store.GatewayTask, user *auth.User) (Result, error) {
|
||||
return s.execute(ctx, task, user, nil)
|
||||
}
|
||||
@@ -982,7 +990,7 @@ candidatesLoop:
|
||||
}
|
||||
walletReservationFinalized = true
|
||||
s.logger.Warn("task succeeded but billing requires manual review", "taskID", task.ID, "error_category", "billing_calculation_failed")
|
||||
output, hydrateErr := s.HydrateTaskResult(ctx, task.ID, response.Result)
|
||||
output, hydrateErr := s.taskResultForSynchronousResponse(ctx, task, response.Result)
|
||||
if hydrateErr != nil {
|
||||
return Result{Task: review}, hydrateErr
|
||||
}
|
||||
@@ -1075,7 +1083,7 @@ candidatesLoop:
|
||||
// time after the task is already durably complete.
|
||||
return Result{Task: finished, Output: response.Result}, nil
|
||||
}
|
||||
output, hydrateErr := s.HydrateTaskResult(ctx, task.ID, response.Result)
|
||||
output, hydrateErr := s.taskResultForSynchronousResponse(ctx, task, response.Result)
|
||||
if hydrateErr != nil {
|
||||
return Result{Task: finished}, hydrateErr
|
||||
}
|
||||
@@ -1272,6 +1280,33 @@ candidatesLoop:
|
||||
return Result{Task: failed, Output: failed.Result}, lastErr
|
||||
}
|
||||
|
||||
func (s *Service) taskResultForSynchronousResponse(ctx context.Context, task store.GatewayTask, result map[string]any) (map[string]any, error) {
|
||||
if taskRequestsInlineMediaResult(task.Request) {
|
||||
if rawBytes := SynchronousInlineResultBytes(result); rawBytes > MaxSynchronousInlineResponseBytes {
|
||||
return nil, &clients.ClientError{
|
||||
Code: "response_format_too_large",
|
||||
Message: "synchronous Base64 response exceeds the 20 MiB limit",
|
||||
StatusCode: http.StatusRequestEntityTooLarge,
|
||||
Retryable: false,
|
||||
Details: map[string]any{
|
||||
"task_id": task.ID,
|
||||
"query_url": "/api/v1/ai/result/" + task.ID,
|
||||
"limit_bytes": MaxSynchronousInlineResponseBytes,
|
||||
"actual_bytes": rawBytes,
|
||||
"response_format": "url",
|
||||
},
|
||||
}
|
||||
}
|
||||
return s.HydrateTaskResult(ctx, task.ID, result)
|
||||
}
|
||||
return s.ProjectTaskResultURLs(ctx, task.ID, result)
|
||||
}
|
||||
|
||||
func taskRequestsInlineMediaResult(request map[string]any) bool {
|
||||
value, _ := request["response_format"].(string)
|
||||
return strings.EqualFold(strings.TrimSpace(value), "b64_json")
|
||||
}
|
||||
|
||||
func pricingCandidateKey(candidate store.RuntimeModelCandidate) string {
|
||||
return firstNonEmptyString(candidate.PlatformModelID, candidate.PlatformID+":"+candidate.ModelName)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user