package httpapi import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "net/http" "strings" "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" ) var errInvalidTaskIdempotencyKey = errors.New("invalid Idempotency-Key header") func optionalTaskIdempotencyKey(r *http.Request) (string, bool, error) { values := r.Header.Values("Idempotency-Key") if len(values) == 0 { return "", false, nil } if len(values) != 1 { return "", false, errInvalidTaskIdempotencyKey } value := strings.TrimSpace(values[0]) if value == "" || len(value) > 255 || strings.Contains(value, ",") { return "", false, errInvalidTaskIdempotencyKey } return value, true, nil } func taskIdempotencyKeyHash(key string) string { digest := sha256.Sum256([]byte(key)) return hex.EncodeToString(digest[:]) } func taskIdempotencyRequestHash(kind string, async bool, stream bool, body map[string]any) string { payload, _ := json.Marshal(map[string]any{ "kind": kind, "async": async, "stream": stream, "request": body, }) digest := sha256.Sum256(payload) return hex.EncodeToString(digest[:]) } func writeIdempotentTaskReplay(w http.ResponseWriter, task store.GatewayTask, compatible bool) { w.Header().Set("Idempotent-Replayed", "true") w.Header().Set("X-Gateway-Task-Id", task.ID) if !compatible || task.AsyncMode || (task.Status != "succeeded" && task.Status != "failed" && task.Status != "cancelled") { writeTaskAccepted(w, task) return } if task.Status == "succeeded" { writeJSON(w, http.StatusOK, task.Result) return } status := storedTaskErrorStatus(task.ErrorCode) message := strings.TrimSpace(task.ErrorMessage) if message == "" { message = strings.TrimSpace(task.Error) } if message == "" { message = "task failed" } code := strings.TrimSpace(task.ErrorCode) if code == "" { code = "task_failed" } writeError(w, status, message, code) } func storedTaskErrorStatus(code string) int { switch strings.TrimSpace(code) { case "pricing_unavailable", "response_chain_unavailable", "billing_hold": return http.StatusServiceUnavailable case "insufficient_balance": return http.StatusPaymentRequired case "bad_request", "invalid_parameter", "invalid_previous_response_id", "unsupported_operation": return http.StatusBadRequest case "no_model_candidate", "cloned_voice_not_found": return http.StatusNotFound case "rate_limit", "platform_cooling_down", "model_cooling_down": return http.StatusTooManyRequests default: return http.StatusBadGateway } }