fix(gemini): 移除空返图占位并暴露失败原因
Gemini 图片响应未包含可提取资源时,不再返回伪造占位图,改为非重试失败并保留安全拦截、候选结束状态和上游错误等结构化诊断。\n\n同步将诊断写入 HTTP 错误、任务结果、attempt 指标和日志,确保任务失败时不结算,并补充单元及 PostgreSQL 验收测试。\n\n验证:gofmt、go test ./... -count=1、go vet ./...、迁移安全检查、pnpm openapi。
This commit is contained in:
@@ -258,6 +258,9 @@ func failureMetrics(err error, simulated bool) (string, map[string]any, time.Tim
|
||||
metrics["error"] = err.Error()
|
||||
metrics["errorCategory"] = info.Category
|
||||
metrics["retryable"] = retryable
|
||||
if details := clients.ErrorDetails(err); len(details) > 0 {
|
||||
metrics["errorDetails"] = details
|
||||
}
|
||||
if detail := rateLimitFailureDetail(err); len(detail) > 0 {
|
||||
metrics["rateLimit"] = detail
|
||||
}
|
||||
@@ -288,6 +291,9 @@ func buildFailureResult(code string, message string, requestID string, err error
|
||||
if requestID != "" {
|
||||
errorPayload["requestId"] = requestID
|
||||
}
|
||||
if details := clients.ErrorDetails(err); len(details) > 0 {
|
||||
errorPayload["details"] = details
|
||||
}
|
||||
if detail := rateLimitFailureDetail(err); len(detail) > 0 {
|
||||
errorPayload["rateLimit"] = detail
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package runner
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
@@ -45,3 +46,27 @@ func TestAttemptMetricsIncludesCacheAffinityCoefficient(t *testing.T) {
|
||||
t.Fatalf("expected cache affinity routing diagnostics in attempt metrics, got %+v", metrics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailureRecordIncludesStructuredClientErrorDetails(t *testing.T) {
|
||||
cause := &clients.ClientError{
|
||||
Code: "gemini_image_output_missing",
|
||||
Message: "Gemini image response contains no extractable image: prompt blocked",
|
||||
Details: map[string]any{
|
||||
"provider": "gemini",
|
||||
"reason": "prompt_blocked",
|
||||
"promptBlockReason": "SAFETY",
|
||||
},
|
||||
}
|
||||
|
||||
_, metrics, _, _, _ := failureMetrics(cause, false)
|
||||
details, _ := metrics["errorDetails"].(map[string]any)
|
||||
if details["reason"] != "prompt_blocked" || details["promptBlockReason"] != "SAFETY" {
|
||||
t.Fatalf("failure metrics lost client error details: %+v", metrics)
|
||||
}
|
||||
result := buildFailureResult(cause.Code, cause.Message, "", cause)
|
||||
errorPayload, _ := result["error"].(map[string]any)
|
||||
resultDetails, _ := errorPayload["details"].(map[string]any)
|
||||
if resultDetails["provider"] != "gemini" || resultDetails["reason"] != "prompt_blocked" {
|
||||
t.Fatalf("failure result lost client error details: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1412,6 +1412,20 @@ func (s *Service) runCandidate(
|
||||
response.ResponseDurationMS = 0
|
||||
}
|
||||
}
|
||||
if err != nil && clients.ErrorCode(err) == "gemini_image_output_missing" && s.logger != nil {
|
||||
meta := clients.ErrorResponseMetadata(err)
|
||||
s.logger.Warn("gemini image response contains no extractable resource",
|
||||
"taskID", task.ID,
|
||||
"attempt", attemptNo,
|
||||
"platformID", candidate.PlatformID,
|
||||
"platformModelID", candidate.PlatformModelID,
|
||||
"clientID", candidate.ClientID,
|
||||
"requestID", meta.RequestID,
|
||||
"error_category", clients.ErrorCode(err),
|
||||
"reason", err.Error(),
|
||||
"details", clients.ErrorDetails(err),
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
if clients.ErrorResponseMetadata(err).StatusCode > 0 && submissionStatus != "response_received" {
|
||||
if markErr := setSubmissionStatus("response_received"); markErr != nil {
|
||||
|
||||
Reference in New Issue
Block a user