easyai-ai-gateway/apps/api/internal/runner/recording.go

222 lines
6.6 KiB
Go

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 := 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,
}
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 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 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
}
}