Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
82 lines
2.4 KiB
Go
82 lines
2.4 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const (
|
|
grokMissingUsageErrorCode = "grok_missing_usage"
|
|
grokMissingUsageMessage = "xAI upstream returned a successful chat completion without billable usage"
|
|
)
|
|
|
|
// hasBillableGrokChatUsage stays aligned with the aggregate token buckets used
|
|
// to account for chat completions. Detail fields alone do not prove that the
|
|
// successful response can be settled safely.
|
|
func hasBillableGrokChatUsage(usage OpenAIUsage) bool {
|
|
return usage.InputTokens > 0 ||
|
|
usage.OutputTokens > 0 ||
|
|
usage.CacheCreationInputTokens > 0 ||
|
|
usage.CacheReadInputTokens > 0
|
|
}
|
|
|
|
// requiresBillableGrokChatUsage identifies Grok traffic by both account
|
|
// platform and model identity. Grok models may be served through generic
|
|
// OpenAI-compatible accounts, so account.Platform alone is not a safe billing
|
|
// boundary.
|
|
func requiresBillableGrokChatUsage(account *Account, models ...string) bool {
|
|
if account != nil && account.Platform == PlatformGrok {
|
|
return true
|
|
}
|
|
for _, model := range models {
|
|
normalized := strings.ToLower(strings.TrimSpace(model))
|
|
if separator := strings.LastIndex(normalized, "/"); separator >= 0 {
|
|
normalized = strings.TrimSpace(normalized[separator+1:])
|
|
}
|
|
if normalized == "grok" || strings.HasPrefix(normalized, "grok-") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func newGrokMissingUsageFailoverError(c *gin.Context, account *Account, upstreamRequestID string) *UpstreamFailoverError {
|
|
accountID := int64(0)
|
|
accountName := ""
|
|
if account != nil {
|
|
accountID = account.ID
|
|
accountName = account.Name
|
|
}
|
|
|
|
setOpsUpstreamError(c, http.StatusBadGateway, grokMissingUsageMessage, "")
|
|
appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
|
|
Platform: PlatformGrok,
|
|
AccountID: accountID,
|
|
AccountName: accountName,
|
|
UpstreamStatusCode: http.StatusBadGateway,
|
|
UpstreamRequestID: strings.TrimSpace(upstreamRequestID),
|
|
Kind: "failover",
|
|
Message: grokMissingUsageMessage,
|
|
})
|
|
|
|
body, _ := json.Marshal(gin.H{
|
|
"error": gin.H{
|
|
"type": "upstream_error",
|
|
"code": grokMissingUsageErrorCode,
|
|
"message": grokMissingUsageMessage,
|
|
},
|
|
})
|
|
headers := http.Header{}
|
|
if requestID := strings.TrimSpace(upstreamRequestID); requestID != "" {
|
|
headers.Set("x-request-id", requestID)
|
|
}
|
|
return &UpstreamFailoverError{
|
|
StatusCode: http.StatusBadGateway,
|
|
ResponseBody: body,
|
|
ResponseHeaders: headers,
|
|
}
|
|
}
|