openai_gateway_usage_integrity.go 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. package service
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "strings"
  6. "github.com/gin-gonic/gin"
  7. )
  8. const (
  9. grokMissingUsageErrorCode = "grok_missing_usage"
  10. grokMissingUsageMessage = "xAI upstream returned a successful chat completion without billable usage"
  11. )
  12. // hasBillableGrokChatUsage stays aligned with the aggregate token buckets used
  13. // to account for chat completions. Detail fields alone do not prove that the
  14. // successful response can be settled safely.
  15. func hasBillableGrokChatUsage(usage OpenAIUsage) bool {
  16. return usage.InputTokens > 0 ||
  17. usage.OutputTokens > 0 ||
  18. usage.CacheCreationInputTokens > 0 ||
  19. usage.CacheReadInputTokens > 0
  20. }
  21. // requiresBillableGrokChatUsage identifies Grok traffic by both account
  22. // platform and model identity. Grok models may be served through generic
  23. // OpenAI-compatible accounts, so account.Platform alone is not a safe billing
  24. // boundary.
  25. func requiresBillableGrokChatUsage(account *Account, models ...string) bool {
  26. if account != nil && account.Platform == PlatformGrok {
  27. return true
  28. }
  29. for _, model := range models {
  30. normalized := strings.ToLower(strings.TrimSpace(model))
  31. if separator := strings.LastIndex(normalized, "/"); separator >= 0 {
  32. normalized = strings.TrimSpace(normalized[separator+1:])
  33. }
  34. if normalized == "grok" || strings.HasPrefix(normalized, "grok-") {
  35. return true
  36. }
  37. }
  38. return false
  39. }
  40. func newGrokMissingUsageFailoverError(c *gin.Context, account *Account, upstreamRequestID string) *UpstreamFailoverError {
  41. accountID := int64(0)
  42. accountName := ""
  43. if account != nil {
  44. accountID = account.ID
  45. accountName = account.Name
  46. }
  47. setOpsUpstreamError(c, http.StatusBadGateway, grokMissingUsageMessage, "")
  48. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  49. ProxyID: opsUpstreamProxyID(account),
  50. ProxyName: opsUpstreamProxyName(account),
  51. Platform: PlatformGrok,
  52. AccountID: accountID,
  53. AccountName: accountName,
  54. UpstreamStatusCode: http.StatusBadGateway,
  55. UpstreamRequestID: strings.TrimSpace(upstreamRequestID),
  56. Kind: "failover",
  57. Message: grokMissingUsageMessage,
  58. })
  59. body, _ := json.Marshal(gin.H{
  60. "error": gin.H{
  61. "type": "upstream_error",
  62. "code": grokMissingUsageErrorCode,
  63. "message": grokMissingUsageMessage,
  64. },
  65. })
  66. headers := http.Header{}
  67. if requestID := strings.TrimSpace(upstreamRequestID); requestID != "" {
  68. headers.Set("x-request-id", requestID)
  69. }
  70. return &UpstreamFailoverError{
  71. StatusCode: http.StatusBadGateway,
  72. ResponseBody: body,
  73. ResponseHeaders: headers,
  74. }
  75. }