package runner import ( "strings" "time" "github.com/easyai/easyai-ai-gateway/apps/api/internal/auth" "github.com/easyai/easyai-ai-gateway/apps/api/internal/clients" "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" ) type taskRecordDetails struct { RequestID string ResolvedModel string Usage map[string]any Metrics map[string]any BillingSummary map[string]any FinalChargeAmount float64 ResponseStartedAt time.Time ResponseFinishedAt time.Time ResponseDurationMS int64 } func buildSuccessRecord(task store.GatewayTask, user *auth.User, body map[string]any, candidate store.RuntimeModelCandidate, response clients.Response, billings []any, simulated bool) taskRecordDetails { usage := usageToMap(response.Usage) metrics := taskMetrics(task, user, body, candidate, response, simulated) summary := summarizeBillings(billings, simulated) finalAmount := floatFromAny(summary["totalAmount"]) return taskRecordDetails{ RequestID: response.RequestID, ResolvedModel: candidate.ModelName, Usage: usage, Metrics: metrics, BillingSummary: summary, FinalChargeAmount: finalAmount, ResponseStartedAt: response.ResponseStartedAt, ResponseFinishedAt: response.ResponseFinishedAt, ResponseDurationMS: response.ResponseDurationMS, } } func taskMetrics(task store.GatewayTask, user *auth.User, body map[string]any, candidate store.RuntimeModelCandidate, response clients.Response, simulated bool) map[string]any { metrics := attemptMetrics(candidate, 0, simulated) for key, value := range map[string]any{ "kind": task.Kind, "runMode": task.RunMode, "requestedModel": task.Model, "resolvedModel": candidate.ModelName, "modelAlias": candidate.ModelAlias, "providerModel": candidate.ProviderModelName, "canonicalModel": candidate.CanonicalModelKey, "modelType": candidate.ModelType, "provider": candidate.Provider, "platformId": candidate.PlatformID, "platformName": candidate.PlatformName, "platformModelId": candidate.PlatformModelID, "clientId": candidate.ClientID, "queueKey": candidate.QueueKey, "requestId": response.RequestID, "simulated": simulated, } { metrics[key] = value } if user != nil { metrics["apiKeyId"] = user.APIKeyID metrics["apiKeyName"] = user.APIKeyName metrics["apiKeyPrefix"] = user.APIKeyPrefix } if response.ResponseDurationMS > 0 { metrics["responseDurationMs"] = response.ResponseDurationMS } switch task.Kind { case "chat.completions", "responses": metrics["stream"] = boolFromMap(body, "stream") metrics["messageCount"] = messageCount(body) copyIfPresent(metrics, body, "temperature") copyIfPresent(metrics, body, "max_tokens") copyIfPresent(metrics, body, "max_output_tokens") case "images.generations", "images.edits": metrics["imageCount"] = requestedCount(body) metrics["outputImageCount"] = outputDataCount(response.Result) metrics["inputImageCount"] = imageInputCount(body) metrics["hasMask"] = stringFromMap(body, "mask") != "" copyIfPresent(metrics, body, "size") copyIfPresent(metrics, body, "quality") copyIfPresent(metrics, body, "style") case "videos.generations": metrics["hasReferenceImage"] = imageInputCount(body) > 0 metrics["hasReferenceVideo"] = hasAnyString(body, "video", "video_url", "videoUrl", "reference_video", "referenceVideo") copyIfPresent(metrics, body, "duration") copyIfPresent(metrics, body, "resolution") copyIfPresent(metrics, body, "size") copyIfPresent(metrics, body, "aspect_ratio") copyIfPresent(metrics, body, "aspectRatio") copyIfPresent(metrics, body, "fps") } return metrics } func attemptMetrics(candidate store.RuntimeModelCandidate, attemptNo int, simulated bool) map[string]any { metrics := map[string]any{ "resolvedModel": candidate.ModelName, "modelName": candidate.ModelName, "modelAlias": candidate.ModelAlias, "providerModel": candidate.ProviderModelName, "canonicalModel": candidate.CanonicalModelKey, "modelType": candidate.ModelType, "provider": candidate.Provider, "platformId": candidate.PlatformID, "platformKey": candidate.PlatformKey, "platformName": candidate.PlatformName, "platformModelId": candidate.PlatformModelID, "clientId": candidate.ClientID, "queueKey": candidate.QueueKey, "simulated": simulated, } if attemptNo > 0 { metrics["attempt"] = attemptNo } return metrics } func usageToMap(usage clients.Usage) map[string]any { out := map[string]any{} if usage.InputTokens > 0 { out["inputTokens"] = usage.InputTokens out["promptTokens"] = usage.InputTokens } if usage.OutputTokens > 0 { out["outputTokens"] = usage.OutputTokens out["completionTokens"] = usage.OutputTokens } if usage.TotalTokens > 0 { out["totalTokens"] = usage.TotalTokens } return out } func summarizeBillings(billings []any, simulated bool) map[string]any { amountByCurrency := map[string]float64{} for _, raw := range billings { line, _ := raw.(map[string]any) if line == nil { continue } currency := strings.TrimSpace(stringFromAny(line["currency"])) if currency == "" { currency = "resource" } amountByCurrency[currency] = roundPrice(amountByCurrency[currency] + floatFromAny(line["amount"])) } currency := "" totalAmount := 0.0 for key, amount := range amountByCurrency { if currency == "" { currency = key } else if currency != key { currency = "mixed" } totalAmount += amount } if currency == "" { currency = "resource" } totalAmount = roundPrice(totalAmount) return map[string]any{ "lineCount": len(billings), "totalAmount": totalAmount, "amountByCurrency": amountByCurrency, "currency": currency, "simulated": simulated, "finalCharge": map[string]any{ "amount": totalAmount, "currency": currency, "simulated": simulated, }, } } func failureMetrics(err error, simulated bool) (string, map[string]any, time.Time, time.Time, int64) { meta := clients.ErrorResponseMetadata(err) metrics := map[string]any{ "simulated": simulated, } if err != nil { metrics["error"] = err.Error() metrics["retryable"] = clients.IsRetryable(err) } if meta.StatusCode > 0 { metrics["statusCode"] = meta.StatusCode } if meta.RequestID != "" { metrics["requestId"] = meta.RequestID } if meta.ResponseDurationMS > 0 { metrics["responseDurationMs"] = meta.ResponseDurationMS } return meta.RequestID, metrics, meta.ResponseStartedAt, meta.ResponseFinishedAt, meta.ResponseDurationMS } func mergeMetrics(values ...map[string]any) map[string]any { out := map[string]any{} for _, value := range values { for key, item := range value { out[key] = item } } return out } func summarizeAttempts(attempts []store.TaskAttempt) []map[string]any { items := make([]map[string]any, 0, len(attempts)) for _, attempt := range attempts { item := map[string]any{ "attempt": attempt.AttemptNo, "status": attempt.Status, "platformId": attempt.PlatformID, "platformName": attempt.PlatformName, "provider": attempt.Provider, "platformModelId": attempt.PlatformModelID, "modelName": attempt.ModelName, "providerModelName": attempt.ProviderModelName, "modelAlias": attempt.ModelAlias, "modelType": attempt.ModelType, "clientId": attempt.ClientID, "queueKey": attempt.QueueKey, "requestId": attempt.RequestID, "retryable": attempt.Retryable, "simulated": attempt.Simulated, "errorCode": attempt.ErrorCode, "errorMessage": attempt.ErrorMessage, "responseDurationMs": attempt.ResponseDurationMS, "startedAt": attempt.StartedAt, "finishedAt": attempt.FinishedAt, } items = append(items, item) } return items } func messageCount(body map[string]any) int { messages, _ := body["messages"].([]any) return len(messages) } func requestedCount(body map[string]any) int { count := int(floatFromAny(body["n"])) if count <= 0 { return 1 } return count } func outputDataCount(result map[string]any) int { data, _ := result["data"].([]any) return len(data) } func imageInputCount(body map[string]any) int { count := 0 for _, key := range []string{"image", "image_url", "imageUrl"} { if stringFromMap(body, key) != "" { count++ } } for _, key := range []string{"image_urls", "imageUrls", "images"} { if values, ok := body[key].([]any); ok { count += len(values) } } return count } func hasAnyString(body map[string]any, keys ...string) bool { for _, key := range keys { if stringFromMap(body, key) != "" { return true } } return false } func copyIfPresent(target map[string]any, body map[string]any, key string) { if value, ok := body[key]; ok && value != nil { target[key] = value } }