feat: enrich task record details

This commit is contained in:
2026-05-10 22:33:58 +08:00
parent 205a4b625e
commit 53f8edfb67
19 changed files with 1781 additions and 165 deletions
+5
View File
@@ -10,6 +10,11 @@ func stringFromMap(values map[string]any, key string) string {
return strings.TrimSpace(value)
}
func stringFromAny(value any) string {
text, _ := value.(string)
return strings.TrimSpace(text)
}
func boolFromMap(values map[string]any, key string) bool {
value, _ := values[key].(bool)
return value
+67 -6
View File
@@ -16,7 +16,7 @@ type EstimateResult struct {
}
func (s *Service) Estimate(ctx context.Context, kind string, model string, body map[string]any, user *auth.User) (EstimateResult, error) {
candidates, err := s.store.ListModelCandidates(ctx, model, modelTypeFromKind(kind))
candidates, err := s.store.ListModelCandidates(ctx, model, modelTypeFromKind(kind), user)
if err != nil {
return EstimateResult{}, err
}
@@ -40,15 +40,15 @@ func (s *Service) Estimate(ctx context.Context, kind string, model string, body
func (s *Service) billings(ctx context.Context, user *auth.User, kind string, body map[string]any, candidate store.RuntimeModelCandidate, response clients.Response, simulated bool) []any {
config := effectiveBillingConfig(candidate)
discount := effectiveDiscount(ctx, s.store, user, candidate)
if modelTypeFromKind(kind) == "chat" {
if isTextGenerationKind(kind) {
inputTokens := response.Usage.InputTokens
outputTokens := response.Usage.OutputTokens
if inputTokens == 0 && outputTokens == 0 {
inputTokens = estimateRequestTokens(body)
outputTokens = 1
}
inputAmount := roundPrice(float64(inputTokens) / 1000 * price(config, "textInputPer1k") * discount)
outputAmount := roundPrice(float64(outputTokens) / 1000 * price(config, "textOutputPer1k") * discount)
inputAmount := roundPrice(float64(inputTokens) / 1000 * resourcePrice(config, "text", "textInputPer1k", "inputTokenPrice", "basePrice") * discount)
outputAmount := roundPrice(float64(outputTokens) / 1000 * resourcePrice(config, "text", "textOutputPer1k", "outputTokenPrice", "basePrice") * discount)
return []any{
billingLine(candidate, "text_input", "1k_tokens", inputTokens, inputAmount, discount, simulated),
billingLine(candidate, "text_output", "1k_tokens", outputTokens, outputAmount, discount, simulated),
@@ -59,13 +59,19 @@ func (s *Service) billings(ctx context.Context, user *auth.User, kind string, bo
count = 1
}
resource := "image"
unit := "image"
baseKey := "imageBase"
if kind == "images.edits" {
resource = "image_edit"
baseKey = "editBase"
}
amount := float64(count) * price(config, baseKey) * weighted(config, "qualityWeights", stringFromMap(body, "quality")) * weighted(config, "sizeWeights", stringFromMap(body, "size")) * discount
return []any{billingLine(candidate, resource, "image", count, roundPrice(amount), discount, simulated)}
if kind == "videos.generations" {
resource = "video"
unit = "video"
baseKey = "videoBase"
}
amount := float64(count) * resourcePrice(config, resource, baseKey, "basePrice") * resourceWeight(config, resource, "qualityWeights", stringFromMap(body, "quality")) * resourceWeight(config, resource, "sizeWeights", stringFromMap(body, "size")) * resourceWeight(config, resource, "resolutionWeights", firstNonEmptyString(stringFromMap(body, "resolution"), stringFromMap(body, "size"))) * discount
return []any{billingLine(candidate, resource, unit, count, roundPrice(amount), discount, simulated)}
}
func effectiveBillingConfig(candidate store.RuntimeModelCandidate) map[string]any {
@@ -121,6 +127,28 @@ func price(config map[string]any, key string) float64 {
return 0
}
func resourcePrice(config map[string]any, resource string, keys ...string) float64 {
for _, key := range keys {
if value := price(config, key); value > 0 {
return value
}
}
if resourceConfig, ok := config[resource].(map[string]any); ok {
for _, key := range keys {
if value := floatFromAny(resourceConfig[key]); value > 0 {
return value
}
}
if value := floatFromAny(resourceConfig["basePrice"]); value > 0 {
return value
}
}
if resource == "image_edit" {
return resourcePrice(config, "image", keys...)
}
return 0
}
func weighted(config map[string]any, key string, name string) float64 {
if strings.TrimSpace(name) == "" {
return 1
@@ -132,6 +160,39 @@ func weighted(config map[string]any, key string, name string) float64 {
return 1
}
func resourceWeight(config map[string]any, resource string, key string, name string) float64 {
if value := weighted(config, key, name); value != 1 {
return value
}
if strings.TrimSpace(name) == "" {
return 1
}
resourceConfig, _ := config[resource].(map[string]any)
if len(resourceConfig) == 0 && resource == "image_edit" {
resourceConfig, _ = config["image"].(map[string]any)
}
if weights, ok := resourceConfig["dynamicWeight"].(map[string]any); ok {
if value := floatFromAny(weights[name]); value > 0 {
return value
}
}
if weights, ok := resourceConfig[key].(map[string]any); ok {
if value := floatFromAny(weights[name]); value > 0 {
return value
}
}
return 1
}
func firstNonEmptyString(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func roundPrice(value float64) float64 {
return math.Round(value*1000000) / 1000000
}
+221
View File
@@ -0,0 +1,221 @@
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
}
}
+142 -35
View File
@@ -41,18 +41,26 @@ func New(cfg config.Config, db *store.Store, logger *slog.Logger) *Service {
}
func (s *Service) Execute(ctx context.Context, task store.GatewayTask, user *auth.User) (Result, error) {
return s.execute(ctx, task, user, nil)
}
func (s *Service) ExecuteStream(ctx context.Context, task store.GatewayTask, user *auth.User, onDelta clients.StreamDelta) (Result, error) {
return s.execute(ctx, task, user, onDelta)
}
func (s *Service) execute(ctx context.Context, task store.GatewayTask, user *auth.User, onDelta clients.StreamDelta) (Result, error) {
modelType := modelTypeFromKind(task.Kind)
body := normalizeRequest(task.Kind, task.Request)
if err := validateRequest(task.Kind, body); err != nil {
failed, finishErr := s.failTask(ctx, task.ID, "bad_request", err.Error(), task.RunMode == "simulation")
failed, finishErr := s.failTask(ctx, task.ID, "bad_request", err.Error(), task.RunMode == "simulation", err)
if finishErr != nil {
return Result{}, finishErr
}
return Result{Task: failed, Output: failed.Result}, err
}
candidates, err := s.store.ListModelCandidates(ctx, task.Model, modelType)
candidates, err := s.store.ListModelCandidates(ctx, task.Model, modelType, user)
if err != nil {
failed, finishErr := s.failTask(ctx, task.ID, "no_model_candidate", err.Error(), task.RunMode == "simulation")
failed, finishErr := s.failTask(ctx, task.ID, "no_model_candidate", err.Error(), task.RunMode == "simulation", err)
if finishErr != nil {
return Result{}, finishErr
}
@@ -72,14 +80,35 @@ func (s *Service) Execute(ctx context.Context, task store.GatewayTask, user *aut
break
}
attemptNo := index + 1
response, err := s.runCandidate(ctx, task, user, body, candidate, attemptNo)
response, err := s.runCandidate(ctx, task, user, body, candidate, attemptNo, onDelta)
if err == nil {
billings := s.billings(ctx, user, task.Kind, body, candidate, response, isSimulation(task, candidate))
finished, finishErr := s.store.FinishTaskSuccess(ctx, task.ID, response.Result, billings)
record := buildSuccessRecord(task, user, body, candidate, response, billings, isSimulation(task, candidate))
finished, finishErr := s.store.FinishTaskSuccess(ctx, store.FinishTaskSuccessInput{
TaskID: task.ID,
Result: response.Result,
Billings: billings,
RequestID: record.RequestID,
ResolvedModel: record.ResolvedModel,
Usage: record.Usage,
Metrics: record.Metrics,
BillingSummary: record.BillingSummary,
FinalChargeAmount: record.FinalChargeAmount,
ResponseStartedAt: record.ResponseStartedAt,
ResponseFinishedAt: record.ResponseFinishedAt,
ResponseDurationMS: record.ResponseDurationMS,
})
if finishErr != nil {
return Result{}, finishErr
}
if err := s.emit(ctx, task.ID, "task.completed", "succeeded", "completed", 1, "task completed", map[string]any{"result": response.Result, "billings": billings}, isSimulation(task, candidate)); err != nil {
if err := s.emit(ctx, task.ID, "task.completed", "succeeded", "completed", 1, "task completed", map[string]any{
"result": response.Result,
"billings": billings,
"usage": record.Usage,
"metrics": record.Metrics,
"billingSummary": record.BillingSummary,
"requestId": record.RequestID,
}, isSimulation(task, candidate)); err != nil {
return Result{}, err
}
return Result{Task: finished, Output: response.Result}, nil
@@ -98,14 +127,14 @@ func (s *Service) Execute(ctx context.Context, task store.GatewayTask, user *aut
if lastErr != nil {
message = lastErr.Error()
}
failed, err := s.failTask(ctx, task.ID, code, message, task.RunMode == "simulation")
failed, err := s.failTask(ctx, task.ID, code, message, task.RunMode == "simulation", lastErr)
if err != nil {
return Result{}, err
}
return Result{Task: failed, Output: failed.Result}, lastErr
}
func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user *auth.User, body map[string]any, candidate store.RuntimeModelCandidate, attemptNo int) (clients.Response, error) {
func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user *auth.User, body map[string]any, candidate store.RuntimeModelCandidate, attemptNo int, onDelta clients.StreamDelta) (clients.Response, error) {
simulated := isSimulation(task, candidate)
if err := s.emit(ctx, task.ID, "task.attempt.started", "running", "submitting", 0.25, "client attempt started", map[string]any{"attempt": attemptNo, "clientId": candidate.ClientID}, simulated); err != nil {
return clients.Response{}, err
@@ -127,7 +156,14 @@ func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user
reservations := s.rateLimitReservations(ctx, user, candidate, body)
limitResult, err := s.store.ReserveRateLimits(ctx, task.ID, reservations)
if err != nil {
_ = s.store.FinishTaskAttempt(ctx, store.FinishTaskAttemptInput{AttemptID: attemptID, Status: "failed", Retryable: false, ErrorCode: "rate_limit", ErrorMessage: err.Error()})
_ = s.store.FinishTaskAttempt(ctx, store.FinishTaskAttemptInput{
AttemptID: attemptID,
Status: "failed",
Retryable: false,
Metrics: map[string]any{"error": err.Error(), "candidateModel": candidate.ModelName, "clientId": candidate.ClientID},
ErrorCode: "rate_limit",
ErrorMessage: err.Error(),
})
return clients.Response{}, &clients.ClientError{Code: "rate_limit", Message: err.Error(), Retryable: false}
}
defer s.store.ReleaseConcurrencyLeases(context.WithoutCancel(ctx), limitResult.LeaseIDs)
@@ -138,34 +174,77 @@ func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user
defer s.store.RecordClientRelease(context.WithoutCancel(ctx), candidate.ClientID, "")
client := s.clientFor(candidate, simulated)
callStartedAt := time.Now()
response, err := client.Run(ctx, clients.Request{
Kind: task.Kind,
ModelType: candidate.ModelType,
Model: task.Model,
Body: body,
Candidate: candidate,
Stream: boolFromMap(body, "stream"),
Kind: task.Kind,
ModelType: candidate.ModelType,
Model: task.Model,
Body: body,
Candidate: candidate,
Stream: boolFromMap(body, "stream"),
StreamDelta: onDelta,
})
callFinishedAt := time.Now()
if response.ResponseStartedAt.IsZero() {
response.ResponseStartedAt = callStartedAt
}
if response.ResponseFinishedAt.IsZero() {
response.ResponseFinishedAt = callFinishedAt
}
if response.ResponseDurationMS == 0 {
response.ResponseDurationMS = response.ResponseFinishedAt.Sub(response.ResponseStartedAt).Milliseconds()
if response.ResponseDurationMS < 0 {
response.ResponseDurationMS = 0
}
}
if err != nil {
retryable := clients.IsRetryable(err)
requestID, metrics, responseStartedAt, responseFinishedAt, responseDurationMS := failureMetrics(err, simulated)
if responseStartedAt.IsZero() {
responseStartedAt = callStartedAt
}
if responseFinishedAt.IsZero() {
responseFinishedAt = callFinishedAt
}
if responseDurationMS == 0 {
responseDurationMS = responseFinishedAt.Sub(responseStartedAt).Milliseconds()
if responseDurationMS < 0 {
responseDurationMS = 0
}
}
_ = s.store.FinishTaskAttempt(ctx, store.FinishTaskAttemptInput{
AttemptID: attemptID,
Status: "failed",
Retryable: retryable,
ErrorCode: clients.ErrorCode(err),
ErrorMessage: err.Error(),
AttemptID: attemptID,
Status: "failed",
Retryable: retryable,
RequestID: requestID,
Metrics: metrics,
ResponseStartedAt: responseStartedAt,
ResponseFinishedAt: responseFinishedAt,
ResponseDurationMS: responseDurationMS,
ErrorCode: clients.ErrorCode(err),
ErrorMessage: err.Error(),
})
_ = s.emit(ctx, task.ID, "task.attempt.failed", "running", "attempt_failed", 0.45, err.Error(), map[string]any{"attempt": attemptNo, "retryable": retryable}, simulated)
_ = s.emit(ctx, task.ID, "task.attempt.failed", "running", "attempt_failed", 0.45, err.Error(), map[string]any{"attempt": attemptNo, "retryable": retryable, "requestId": requestID, "metrics": metrics}, simulated)
return clients.Response{}, err
}
uploadedResult, err := s.uploadGeneratedAssets(ctx, response.Result)
if err != nil {
metrics := taskMetrics(task, user, body, candidate, response, simulated)
metrics["error"] = err.Error()
metrics["retryable"] = clients.IsRetryable(err)
_ = s.store.FinishTaskAttempt(ctx, store.FinishTaskAttemptInput{
AttemptID: attemptID,
Status: "failed",
Retryable: clients.IsRetryable(err),
ErrorCode: clients.ErrorCode(err),
ErrorMessage: err.Error(),
AttemptID: attemptID,
Status: "failed",
Retryable: clients.IsRetryable(err),
RequestID: response.RequestID,
Usage: usageToMap(response.Usage),
Metrics: metrics,
ResponseSnapshot: response.Result,
ResponseStartedAt: response.ResponseStartedAt,
ResponseFinishedAt: response.ResponseFinishedAt,
ResponseDurationMS: response.ResponseDurationMS,
ErrorCode: clients.ErrorCode(err),
ErrorMessage: err.Error(),
})
return clients.Response{}, err
}
@@ -176,9 +255,15 @@ func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user
}
}
if err := s.store.FinishTaskAttempt(ctx, store.FinishTaskAttemptInput{
AttemptID: attemptID,
Status: "succeeded",
ResponseSnapshot: response.Result,
AttemptID: attemptID,
Status: "succeeded",
RequestID: response.RequestID,
Usage: usageToMap(response.Usage),
Metrics: taskMetrics(task, user, body, candidate, response, simulated),
ResponseSnapshot: response.Result,
ResponseStartedAt: response.ResponseStartedAt,
ResponseFinishedAt: response.ResponseFinishedAt,
ResponseDurationMS: response.ResponseDurationMS,
}); err != nil {
return clients.Response{}, err
}
@@ -189,19 +274,32 @@ func (s *Service) clientFor(candidate store.RuntimeModelCandidate, simulated boo
if simulated {
return s.clients["simulation"]
}
key := strings.ToLower(candidate.Provider)
key := strings.ToLower(strings.TrimSpace(candidate.SpecType))
if key == "" {
key = strings.ToLower(strings.TrimSpace(candidate.Provider))
}
if client, ok := s.clients[key]; ok {
return client
}
return s.clients["openai"]
}
func (s *Service) failTask(ctx context.Context, taskID string, code string, message string, simulated bool) (store.GatewayTask, error) {
failed, err := s.store.FinishTaskFailure(ctx, taskID, code, message)
func (s *Service) failTask(ctx context.Context, taskID string, code string, message string, simulated bool, cause error) (store.GatewayTask, error) {
requestID, metrics, responseStartedAt, responseFinishedAt, responseDurationMS := failureMetrics(cause, simulated)
failed, err := s.store.FinishTaskFailure(ctx, store.FinishTaskFailureInput{
TaskID: taskID,
Code: code,
Message: message,
RequestID: requestID,
Metrics: metrics,
ResponseStartedAt: responseStartedAt,
ResponseFinishedAt: responseFinishedAt,
ResponseDurationMS: responseDurationMS,
})
if err != nil {
return store.GatewayTask{}, err
}
if eventErr := s.emit(ctx, taskID, "task.failed", "failed", "failed", 1, message, map[string]any{"code": code}, simulated); eventErr != nil {
if eventErr := s.emit(ctx, taskID, "task.failed", "failed", "failed", 1, message, map[string]any{"code": code, "requestId": requestID, "metrics": metrics}, simulated); eventErr != nil {
return store.GatewayTask{}, eventErr
}
return failed, nil
@@ -221,14 +319,23 @@ func (s *Service) emit(ctx context.Context, taskID string, eventType string, sta
func modelTypeFromKind(kind string) string {
switch kind {
case "chat.completions", "responses":
return "chat"
return "text_generate"
case "images.generations", "images.edits":
return "image"
if kind == "images.edits" {
return "image_edit"
}
return "image_generate"
case "videos.generations":
return "video_generate"
default:
return "task"
}
}
func isTextGenerationKind(kind string) bool {
return kind == "chat.completions" || kind == "responses"
}
func isSimulation(task store.GatewayTask, candidate store.RuntimeModelCandidate) bool {
if task.RunMode == "simulation" {
return true