| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670 |
- package service
- import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "log/slog"
- "net/http"
- "strings"
- "time"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- "github.com/gin-gonic/gin"
- )
- const (
- grokCredentialFailoverDeadlineKey = "grok_credential_failover_deadline"
- grokCredentialFailoverBudget = 15 * time.Second
- grokCredentialMutationTimeout = 5 * time.Second
- grokCredentialMutationConfirmWait = 250 * time.Millisecond
- grokCredentialCacheCleanupTimeout = 500 * time.Millisecond
- GrokCredentialUnavailableClientMessage = "No healthy Grok OAuth account is currently available"
- GrokCredentialReasonRevoked GatewayFailureReason = "grok_oauth_credential_revoked"
- GrokCredentialReasonMissing GatewayFailureReason = "grok_oauth_credentials_missing"
- GrokCredentialReasonEntitlement GatewayFailureReason = "grok_oauth_entitlement_action_required"
- GrokCredentialReasonProxyInvalid GatewayFailureReason = "grok_oauth_proxy_invalid"
- GrokCredentialReasonRefreshTransient GatewayFailureReason = "grok_oauth_refresh_transient"
- GrokCredentialReasonProviderConfig GatewayFailureReason = "grok_oauth_provider_config"
- GrokCredentialReasonProviderDown GatewayFailureReason = "grok_oauth_provider_unavailable"
- GrokCredentialReasonAccountChanged GatewayFailureReason = "grok_oauth_account_state_changed"
- GrokCredentialReasonStateUpdate GatewayFailureReason = "grok_oauth_account_state_update_failed"
- GrokCredentialReasonFailoverTimeout GatewayFailureReason = "grok_oauth_failover_timeout"
- )
- var errGrokCredentialStateUpdateFailed = errors.New("grok oauth account state update failed")
- type grokCredentialFailureClass struct {
- scope GatewayFailureScope
- reason GatewayFailureReason
- action NextAccountAction
- permanent bool
- transient bool
- message string
- snapshot *GrokCredentialMutationSnapshot
- }
- // GrokCredentialMutationSnapshot is the credential identity observed when the
- // request selected an account. Repository mutations compare all fields before
- // quarantining that account so a concurrent refresh cannot be overwritten.
- type GrokCredentialMutationSnapshot struct {
- CredentialsJSON string
- AccessToken string
- RefreshToken string
- TokenVersion int64
- ProxyID *int64
- }
- type grokCredentialFailureSnapshotError struct {
- cause error
- snapshot GrokCredentialMutationSnapshot
- }
- func (e *grokCredentialFailureSnapshotError) Error() string { return e.cause.Error() }
- func (e *grokCredentialFailureSnapshotError) Unwrap() error { return e.cause }
- func withGrokCredentialFailureSnapshot(err error, account *Account) error {
- if err == nil || account == nil || !account.IsGrokOAuth() {
- return err
- }
- var existing *grokCredentialFailureSnapshotError
- if errors.As(err, &existing) {
- return err
- }
- return &grokCredentialFailureSnapshotError{cause: err, snapshot: grokCredentialMutationSnapshot(account)}
- }
- func grokCredentialFailureSnapshot(err error) (GrokCredentialMutationSnapshot, bool) {
- var snapshotErr *grokCredentialFailureSnapshotError
- if !errors.As(err, &snapshotErr) || snapshotErr == nil {
- return GrokCredentialMutationSnapshot{}, false
- }
- return snapshotErr.snapshot, true
- }
- type grokCredentialConditionalStateRepository interface {
- SetGrokCredentialErrorIfMatch(context.Context, int64, GrokCredentialMutationSnapshot, string) (bool, error)
- SetGrokCredentialTempUnschedulableIfMatch(context.Context, int64, GrokCredentialMutationSnapshot, time.Time, string) (bool, error)
- }
- // GetRequestCredential applies the request-path credential and failover contract
- // before any upstream transport is opened.
- func (s *OpenAIGatewayService) GetRequestCredential(ctx context.Context, c *gin.Context, account *Account) (string, string, error) {
- return s.getRequestCredential(ctx, c, account)
- }
- func (s *OpenAIGatewayService) getRequestCredential(ctx context.Context, c *gin.Context, account *Account) (string, string, error) {
- if ctx == nil {
- ctx = context.Background()
- }
- if account == nil {
- return "", "", errors.New("account is nil")
- }
- if !account.IsGrokOAuth() {
- return s.GetAccessToken(ctx, account)
- }
- if err := ctx.Err(); err != nil {
- return "", "", err
- }
- if s == nil || s.grokTokenProvider == nil {
- return "", "", s.newGrokCredentialFailover(c, account, grokCredentialFailureClass{
- scope: GatewayFailureScopeProvider,
- reason: GrokCredentialReasonProviderConfig,
- action: NextAccountStop,
- message: "Grok OAuth credential provider is unavailable",
- })
- }
- if s.isOpenAIAccountRuntimeBlocked(account) {
- return "", "", s.newGrokCredentialFailover(c, account, grokCredentialFailureClass{
- scope: GatewayFailureScopeAccount,
- reason: GrokCredentialReasonAccountChanged,
- action: NextAccountRetry,
- message: "Grok OAuth account is not currently schedulable",
- })
- }
- credentialCtx, cancel, budgetExpired := grokCredentialAcquisitionContext(ctx, c)
- if cancel != nil {
- defer cancel()
- }
- if budgetExpired {
- return "", "", s.newGrokCredentialFailover(c, account, grokCredentialFailureClass{
- scope: GatewayFailureScopeRequest,
- reason: GrokCredentialReasonFailoverTimeout,
- action: NextAccountStop,
- message: "Grok OAuth credential failover budget exhausted",
- })
- }
- token, kind, err := s.GetAccessToken(credentialCtx, account)
- if err == nil {
- if s.isOpenAIAccountRuntimeBlocked(account) {
- return "", "", s.newGrokCredentialFailover(c, account, grokCredentialFailureClass{
- scope: GatewayFailureScopeAccount,
- reason: GrokCredentialReasonAccountChanged,
- action: NextAccountRetry,
- message: "Grok OAuth account is not currently schedulable",
- })
- }
- return token, kind, nil
- }
- if parentErr := ctx.Err(); parentErr != nil {
- return "", "", parentErr
- }
- if credentialCtx.Err() != nil {
- return "", "", s.newGrokCredentialFailover(c, account, grokCredentialFailureClass{
- scope: GatewayFailureScopeRequest,
- reason: GrokCredentialReasonFailoverTimeout,
- action: NextAccountStop,
- message: "Grok OAuth credential failover budget exhausted",
- })
- }
- class := classifyGrokCredentialFailure(account, err)
- if snapshot, ok := grokCredentialFailureSnapshot(err); ok {
- class.snapshot = &snapshot
- }
- if ctx.Err() != nil {
- return "", "", ctx.Err()
- }
- if class.permanent || class.transient {
- freshToken, mutationErr := s.applyGrokCredentialAccountFailure(credentialCtx, account, class)
- if freshToken != "" {
- return freshToken, "oauth", nil
- }
- if mutationErr != nil {
- if ctx.Err() != nil {
- return "", "", ctx.Err()
- }
- if credentialCtx.Err() != nil {
- return "", "", s.newGrokCredentialFailover(c, account, grokCredentialFailureClass{
- scope: GatewayFailureScopeRequest,
- reason: GrokCredentialReasonFailoverTimeout,
- action: NextAccountStop,
- message: "Grok OAuth credential failover budget exhausted",
- })
- }
- if errors.Is(mutationErr, errOAuthRefreshAccountStateChanged) {
- class = grokCredentialFailureClass{
- scope: GatewayFailureScopeAccount,
- reason: GrokCredentialReasonAccountChanged,
- action: NextAccountRetry,
- message: "Grok OAuth account eligibility changed",
- }
- } else if errors.Is(mutationErr, errOAuthRefreshAccountRereadFailed) {
- class = grokCredentialFailureClass{
- scope: GatewayFailureScopeProvider,
- reason: GrokCredentialReasonProviderDown,
- action: NextAccountStop,
- message: "Grok OAuth account state is temporarily unavailable",
- }
- } else {
- class = grokCredentialFailureClass{
- scope: GatewayFailureScopeProvider,
- reason: GrokCredentialReasonStateUpdate,
- action: NextAccountStop,
- message: "Grok OAuth account state could not be updated safely",
- }
- }
- }
- }
- return "", "", s.newGrokCredentialFailover(c, account, class)
- }
- func grokCredentialAcquisitionContext(ctx context.Context, c *gin.Context) (context.Context, context.CancelFunc, bool) {
- if c == nil {
- return ctx, nil, false
- }
- deadline := time.Time{}
- if raw, ok := c.Get(grokCredentialFailoverDeadlineKey); ok {
- deadline, _ = raw.(time.Time)
- }
- if deadline.IsZero() {
- deadline = time.Now().Add(grokCredentialFailoverBudget)
- c.Set(grokCredentialFailoverDeadlineKey, deadline)
- }
- if !time.Now().Before(deadline) {
- return ctx, nil, true
- }
- acquireCtx, cancel := context.WithDeadline(ctx, deadline)
- return acquireCtx, cancel, false
- }
- func classifyGrokCredentialFailure(account *Account, err error) grokCredentialFailureClass {
- stableReason := strings.ToLower(strings.TrimSpace(infraerrors.Reason(err)))
- message := ""
- if err != nil {
- message = strings.ToLower(err.Error())
- }
- contains := func(values ...string) bool {
- for _, value := range values {
- if strings.Contains(stableReason, value) || strings.Contains(message, value) {
- return true
- }
- }
- return false
- }
- var providerConfigErr *providerConfigurationRefreshError
- var containmentErr *providerCycleContainmentRefreshError
- switch {
- case errors.Is(err, errGrokOAuthRefreshTokenMissing), errors.Is(err, errGrokOAuthAccessTokenMissing), errors.Is(err, errGrokOAuthAccessTokenExpired):
- return grokCredentialFailureClass{scope: GatewayFailureScopeAccount, reason: GrokCredentialReasonMissing, action: NextAccountRetry, permanent: true, message: "Grok OAuth credentials are missing or expired"}
- case contains("invalid_grant", "invalid_refresh_token", "token_expired", "refresh_token_reused", "refresh_token_invalidated", "app_session_terminated"):
- return grokCredentialFailureClass{scope: GatewayFailureScopeAccount, reason: GrokCredentialReasonRevoked, action: NextAccountRetry, permanent: true, message: "Grok OAuth credentials require account action"}
- case contains("spending limit", "run out of credits", "out of credits", "credits exhausted", "included free usage"):
- // Billing and rolling free-usage exhaustion recover without replacing the
- // OAuth credential. Treat refresh failures as transient so the account
- // remains eligible for a later quota probe.
- return grokCredentialFailureClass{scope: GatewayFailureScopeAccount, reason: GrokCredentialReasonRefreshTransient, action: NextAccountRetry, transient: true, message: "Grok OAuth billing quota is temporarily exhausted"}
- case contains("grok_oauth_entitlement_denied", "entitlement_denied", "access_denied", "subscription required", "no active grok subscription"):
- return grokCredentialFailureClass{scope: GatewayFailureScopeAccount, reason: GrokCredentialReasonEntitlement, action: NextAccountRetry, permanent: true, message: "Grok OAuth entitlement requires account action"}
- case errors.Is(err, errGrokOAuthConfiguredProxyMiss), contains("grok_oauth_proxy_not_found"):
- return grokCredentialFailureClass{scope: GatewayFailureScopeAccount, reason: GrokCredentialReasonProxyInvalid, action: NextAccountRetry, permanent: true, message: "Grok OAuth account proxy configuration is invalid"}
- case errors.Is(err, errOAuthRefreshAccountRereadFailed):
- return grokCredentialFailureClass{scope: GatewayFailureScopeProvider, reason: GrokCredentialReasonProviderDown, action: NextAccountStop, message: "Grok OAuth account state is temporarily unavailable"}
- case errors.Is(err, errOAuthRefreshCredentialPersist):
- return grokCredentialFailureClass{scope: GatewayFailureScopeProvider, reason: GrokCredentialReasonProviderDown, action: NextAccountStop, message: "Grok OAuth shared credential state is temporarily unavailable"}
- case errors.As(err, &containmentErr):
- return grokCredentialFailureClass{scope: GatewayFailureScopeProvider, reason: GrokCredentialReasonProviderDown, action: NextAccountStop, message: "Grok OAuth provider state is temporarily unavailable"}
- case errors.Is(err, errOAuthRefreshAccountStateChanged):
- return grokCredentialFailureClass{scope: GatewayFailureScopeAccount, reason: GrokCredentialReasonAccountChanged, action: NextAccountRetry, message: "Grok OAuth account eligibility changed"}
- case errors.As(err, &providerConfigErr):
- return grokCredentialFailureClass{scope: GatewayFailureScopeProvider, reason: GrokCredentialReasonProviderConfig, action: NextAccountStop, message: "Grok OAuth provider configuration is unavailable"}
- case errors.Is(err, errGrokOAuthRefreshNotConfigured), contains("invalid_client", "unauthorized_client", "invalid_scope", "unknown scope", "grok oauth service is not configured", "grok_oauth_proxy_not_available"):
- return grokCredentialFailureClass{scope: GatewayFailureScopeProvider, reason: GrokCredentialReasonProviderConfig, action: NextAccountStop, message: "Grok OAuth provider configuration is unavailable"}
- case contains("grok_oauth_proxy_lookup_failed"),
- contains("grok_oauth_token_refresh_failed") && contains("status 403") && (account == nil || account.ProxyID == nil):
- return grokCredentialFailureClass{scope: GatewayFailureScopeProvider, reason: GrokCredentialReasonProviderDown, action: NextAccountStop, message: "Grok OAuth provider is temporarily unavailable"}
- case contains("grok_oauth_client_init_failed") && (account == nil || account.ProxyID == nil):
- return grokCredentialFailureClass{scope: GatewayFailureScopeProvider, reason: GrokCredentialReasonProviderConfig, action: NextAccountStop, message: "Grok OAuth provider configuration is unavailable"}
- case contains("grok_oauth_request_failed") && (account == nil || account.ProxyID == nil):
- return grokCredentialFailureClass{scope: GatewayFailureScopeProvider, reason: GrokCredentialReasonProviderDown, action: NextAccountStop, message: "Grok OAuth provider is temporarily unavailable"}
- case contains("status 429", "status 500", "status 502", "status 503", "status 504") && (account == nil || account.ProxyID == nil):
- return grokCredentialFailureClass{scope: GatewayFailureScopeProvider, reason: GrokCredentialReasonProviderDown, action: NextAccountStop, message: "Grok OAuth provider is temporarily unavailable"}
- default:
- return grokCredentialFailureClass{scope: GatewayFailureScopeAccount, reason: GrokCredentialReasonRefreshTransient, action: NextAccountRetry, transient: true, message: "Grok OAuth credential refresh is temporarily unavailable"}
- }
- }
- func (s *OpenAIGatewayService) applyGrokCredentialAccountFailure(ctx context.Context, account *Account, class grokCredentialFailureClass) (string, error) {
- if s == nil || account == nil || ctx == nil || ctx.Err() != nil {
- if ctx != nil {
- return "", ctx.Err()
- }
- return "", context.Canceled
- }
- mutationMu := s.grokCredentialMutationLock(account.ID)
- if err := mutationMu.Lock(ctx); err != nil {
- return "", err
- }
- defer mutationMu.Unlock()
- stateRepo, hasConditionalStateRepo := s.accountRepo.(grokCredentialConditionalStateRepository)
- snapshot := grokCredentialMutationSnapshot(account)
- if class.snapshot != nil {
- snapshot = *class.snapshot
- }
- if token, err := s.validateCurrentGrokCredentialFailure(ctx, account.ID, snapshot, class); err != nil || token != "" {
- return token, err
- }
- if class.permanent {
- if token, ok := s.grokCredentialConcurrentlyRefreshedToken(ctx, account.ID, snapshot); ok {
- return token, nil
- }
- if ctx.Err() != nil {
- return "", ctx.Err()
- }
- rollbackRuntime := s.blockGrokCredentialRuntime(account, time.Time{}, string(class.reason))
- keepRuntimeBlock := false
- runtimeRollbackDone := false
- defer func() {
- if !keepRuntimeBlock && !runtimeRollbackDone {
- rollbackRuntime()
- }
- }()
- if s.accountRepo == nil {
- keepRuntimeBlock = true
- return "", fmt.Errorf("%w: account repository is not configured", errGrokCredentialStateUpdateFailed)
- }
- if !hasConditionalStateRepo {
- keepRuntimeBlock = true
- return "", fmt.Errorf("%w: conditional account repository is not configured", errGrokCredentialStateUpdateFailed)
- }
- stateCtx, cancel := context.WithTimeout(ctx, grokCredentialMutationTimeout)
- if err := ctx.Err(); err != nil {
- cancel()
- return "", err
- }
- updated, err := stateRepo.SetGrokCredentialErrorIfMatch(stateCtx, account.ID, snapshot, string(class.reason))
- requestErr := ctx.Err()
- cancel()
- if err != nil {
- slog.Warn("grok_credential_failure.set_error_failed", "account_id", account.ID, "reason", class.reason, "error", err)
- if requestErr != nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
- if s.grokCredentialMutationCommitted(account.ID, class, time.Time{}) {
- updated = true
- } else if requestErr != nil {
- return "", requestErr
- } else {
- keepRuntimeBlock = true
- return "", fmt.Errorf("%w: permanent state commit could not be confirmed: %v", errGrokCredentialStateUpdateFailed, err)
- }
- } else {
- keepRuntimeBlock = true
- return "", fmt.Errorf("%w: persist permanent state: %v", errGrokCredentialStateUpdateFailed, err)
- }
- }
- if !updated {
- rollbackRuntime()
- runtimeRollbackDone = true
- return s.resolveGrokCredentialCASMiss(ctx, account.ID, snapshot)
- }
- // SetError is the linearization point: the durable quarantine is now
- // committed and this node's runtime block must not be rolled back.
- keepRuntimeBlock = true
- if s.grokTokenProvider == nil {
- if ctx.Err() != nil {
- return "", ctx.Err()
- }
- return "", fmt.Errorf("%w: token provider is not configured", errGrokCredentialStateUpdateFailed)
- }
- invalidateCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), grokCredentialCacheCleanupTimeout)
- err = s.grokTokenProvider.InvalidateToken(invalidateCtx, account)
- cancel()
- if err != nil {
- slog.Warn("grok_credential_failure.invalidate_token_failed", "account_id", account.ID, "reason", class.reason, "error", err)
- if ctx.Err() != nil {
- return "", ctx.Err()
- }
- return "", fmt.Errorf("%w: invalidate cached credential: %v", errGrokCredentialStateUpdateFailed, err)
- }
- if ctx.Err() != nil {
- return "", ctx.Err()
- }
- return "", nil
- }
- if class.transient {
- until := time.Now().Add(tokenRefreshTempUnschedDuration)
- if ctx.Err() != nil {
- return "", ctx.Err()
- }
- rollbackRuntime := s.blockGrokCredentialRuntime(account, until, string(class.reason))
- keepRuntimeBlock := false
- runtimeRollbackDone := false
- defer func() {
- if !keepRuntimeBlock && !runtimeRollbackDone {
- rollbackRuntime()
- }
- }()
- stateCtx, cancel := context.WithTimeout(ctx, grokCredentialMutationTimeout)
- if s.accountRepo == nil {
- cancel()
- keepRuntimeBlock = true
- return "", fmt.Errorf("%w: account repository is not configured", errGrokCredentialStateUpdateFailed)
- }
- if !hasConditionalStateRepo {
- cancel()
- keepRuntimeBlock = true
- return "", fmt.Errorf("%w: conditional account repository is not configured", errGrokCredentialStateUpdateFailed)
- }
- if err := ctx.Err(); err != nil {
- cancel()
- return "", err
- }
- updated, err := stateRepo.SetGrokCredentialTempUnschedulableIfMatch(stateCtx, account.ID, snapshot, until, string(class.reason))
- requestErr := ctx.Err()
- cancel()
- if err != nil {
- slog.Warn("grok_credential_failure.set_temp_unschedulable_failed", "account_id", account.ID, "reason", class.reason, "error", err)
- if requestErr != nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
- if s.grokCredentialMutationCommitted(account.ID, class, until) {
- updated = true
- } else if requestErr != nil {
- return "", requestErr
- } else {
- keepRuntimeBlock = true
- return "", fmt.Errorf("%w: transient state commit could not be confirmed: %v", errGrokCredentialStateUpdateFailed, err)
- }
- } else {
- keepRuntimeBlock = true
- return "", fmt.Errorf("%w: persist transient state: %v", errGrokCredentialStateUpdateFailed, err)
- }
- }
- if !updated {
- rollbackRuntime()
- runtimeRollbackDone = true
- return s.resolveGrokCredentialCASMiss(ctx, account.ID, snapshot)
- }
- // The temporary quarantine is durable after SetTempUnschedulable succeeds.
- keepRuntimeBlock = true
- if ctx.Err() != nil {
- return "", ctx.Err()
- }
- return "", nil
- }
- return "", nil
- }
- func (s *OpenAIGatewayService) validateCurrentGrokCredentialFailure(
- ctx context.Context,
- accountID int64,
- snapshot GrokCredentialMutationSnapshot,
- class grokCredentialFailureClass,
- ) (string, error) {
- if s == nil || s.accountRepo == nil || accountID <= 0 || ctx == nil || ctx.Err() != nil {
- if ctx != nil {
- return "", ctx.Err()
- }
- return "", context.Canceled
- }
- checkCtx, cancel := context.WithTimeout(ctx, time.Second)
- defer cancel()
- latest, err := s.accountRepo.GetByID(checkCtx, accountID)
- if err != nil {
- if errors.Is(err, ErrAccountNotFound) {
- return "", errOAuthRefreshAccountStateChanged
- }
- return "", fmt.Errorf("%w: %v", errOAuthRefreshAccountRereadFailed, err)
- }
- if latest == nil || !latest.IsGrokOAuth() || !latest.IsSchedulable() || s.isOpenAIAccountRuntimeBlocked(latest) {
- return "", errOAuthRefreshAccountStateChanged
- }
- latestSnapshot := grokCredentialMutationSnapshot(latest)
- if latestSnapshot.CredentialsJSON != snapshot.CredentialsJSON ||
- !grokCredentialProxyIDsEqual(latestSnapshot.ProxyID, snapshot.ProxyID) {
- if token, ok := s.grokCredentialConcurrentlyRefreshedToken(ctx, accountID, snapshot); ok {
- return token, nil
- }
- return "", errOAuthRefreshAccountStateChanged
- }
- // A configured proxy is external to the account-row CAS identity. Recheck
- // the hydrated proxy object so restoring a deleted row under the same ID
- // wins over a stale proxy-invalid failure.
- if class.reason == GrokCredentialReasonProxyInvalid {
- if latest.ProxyID == nil || latest.Proxy != nil {
- return "", errOAuthRefreshAccountStateChanged
- }
- } else if latest.ProxyID != nil && latest.Proxy == nil {
- return "", errOAuthRefreshAccountStateChanged
- }
- if class.reason == GrokCredentialReasonMissing {
- expiresAt := latest.GetCredentialAsTime("expires_at")
- credentialsStillMissing := strings.TrimSpace(latest.GetGrokAccessToken()) == "" ||
- strings.TrimSpace(latest.GetGrokRefreshToken()) == "" || expiresAt == nil || !time.Now().Before(*expiresAt)
- if !credentialsStillMissing {
- return "", errOAuthRefreshAccountStateChanged
- }
- }
- return "", nil
- }
- func (s *OpenAIGatewayService) grokCredentialMutationLock(accountID int64) *oauthRefreshLocalLock {
- actual, _ := s.grokCredentialMutationLocks.LoadOrStore(accountID, newOAuthRefreshLocalLock())
- mu, ok := actual.(*oauthRefreshLocalLock)
- if !ok {
- mu = newOAuthRefreshLocalLock()
- s.grokCredentialMutationLocks.Store(accountID, mu)
- }
- return mu
- }
- func (s *OpenAIGatewayService) grokCredentialMutationCommitted(accountID int64, class grokCredentialFailureClass, until time.Time) bool {
- if s == nil || s.accountRepo == nil || accountID <= 0 {
- return false
- }
- confirmCtx, cancel := context.WithTimeout(context.Background(), grokCredentialMutationConfirmWait)
- defer cancel()
- latest, err := s.accountRepo.GetByID(confirmCtx, accountID)
- if err != nil || latest == nil {
- return false
- }
- if class.permanent {
- return latest.Status == StatusError && !latest.Schedulable && latest.ErrorMessage == string(class.reason)
- }
- if class.transient {
- return latest.TempUnschedulableUntil != nil && !latest.TempUnschedulableUntil.Before(until) &&
- latest.TempUnschedulableReason == string(class.reason)
- }
- return false
- }
- func grokCredentialMutationSnapshot(account *Account) GrokCredentialMutationSnapshot {
- if account == nil {
- return GrokCredentialMutationSnapshot{}
- }
- credentialsJSON := "null"
- if encoded, err := json.Marshal(account.Credentials); err == nil {
- credentialsJSON = string(encoded)
- }
- snapshot := GrokCredentialMutationSnapshot{
- CredentialsJSON: credentialsJSON,
- AccessToken: strings.TrimSpace(account.GetGrokAccessToken()),
- RefreshToken: strings.TrimSpace(account.GetGrokRefreshToken()),
- TokenVersion: account.GetCredentialAsInt64("_token_version"),
- }
- if account.ProxyID != nil {
- proxyID := *account.ProxyID
- snapshot.ProxyID = &proxyID
- }
- return snapshot
- }
- func (s *OpenAIGatewayService) resolveGrokCredentialCASMiss(ctx context.Context, accountID int64, snapshot GrokCredentialMutationSnapshot) (string, error) {
- if ctx.Err() != nil {
- return "", ctx.Err()
- }
- if token, ok := s.grokCredentialConcurrentlyRefreshedToken(ctx, accountID, snapshot); ok {
- return token, nil
- }
- return "", errOAuthRefreshAccountStateChanged
- }
- func (s *OpenAIGatewayService) blockGrokCredentialRuntime(account *Account, until time.Time, reason string) func() {
- if s == nil || account == nil {
- return func() {}
- }
- mu := s.openAIAccountRuntimeBlockLock(account.ID)
- mu.Lock()
- before, hadBefore := s.openaiAccountRuntimeBlockUntil.Load(account.ID)
- installedGeneration, changed := s.blockAccountSchedulingLocked(account, until, reason)
- installed, installedOK := s.openaiAccountRuntimeBlockUntil.Load(account.ID)
- installedUntil, isTime := installed.(time.Time)
- mu.Unlock()
- if !changed || !installedOK || !isTime {
- return func() {}
- }
- if hadBefore {
- if beforeUntil, ok := before.(time.Time); ok && beforeUntil.Equal(installedUntil) {
- return func() {}
- }
- }
- return func() {
- mu.Lock()
- defer mu.Unlock()
- generation, ok := s.openaiAccountRuntimeBlockGeneration.Load(account.ID)
- if !ok || generation != installedGeneration {
- return
- }
- current, ok := s.openaiAccountRuntimeBlockUntil.Load(account.ID)
- currentUntil, isTime := current.(time.Time)
- if !ok || !isTime || !currentUntil.Equal(installedUntil) {
- return
- }
- if hadBefore {
- s.openaiAccountRuntimeBlockUntil.Store(account.ID, before)
- s.openaiAccountRuntimeBlockGeneration.Store(account.ID, s.openaiAccountRuntimeBlockSequence.Add(1))
- return
- }
- s.openaiAccountRuntimeBlockUntil.Delete(account.ID)
- s.openaiAccountRuntimeBlockGeneration.Store(account.ID, s.openaiAccountRuntimeBlockSequence.Add(1))
- }
- }
- func (s *OpenAIGatewayService) grokCredentialConcurrentlyRefreshedToken(ctx context.Context, accountID int64, baseline GrokCredentialMutationSnapshot) (string, bool) {
- if s == nil || s.accountRepo == nil || accountID <= 0 || ctx == nil || ctx.Err() != nil {
- return "", false
- }
- checkCtx, cancel := context.WithTimeout(ctx, time.Second)
- defer cancel()
- latest, err := s.accountRepo.GetByID(checkCtx, accountID)
- if err != nil || latest == nil {
- return "", false
- }
- latestSnapshot := grokCredentialMutationSnapshot(latest)
- if !grokCredentialProxyIDsEqual(latestSnapshot.ProxyID, baseline.ProxyID) ||
- latestSnapshot.CredentialsJSON == baseline.CredentialsJSON || !latest.IsSchedulable() ||
- (latest.ProxyID != nil && latest.Proxy == nil) || s.isOpenAIAccountRuntimeBlocked(latest) {
- return "", false
- }
- latestToken := strings.TrimSpace(latest.GetGrokAccessToken())
- if latestToken == "" || strings.TrimSpace(latest.GetGrokRefreshToken()) == "" {
- return "", false
- }
- expiresAt := latest.GetCredentialAsTime("expires_at")
- if expiresAt == nil || !time.Now().Before(*expiresAt) {
- return "", false
- }
- return latestToken, true
- }
- func grokCredentialProxyIDsEqual(left, right *int64) bool {
- if left == nil || right == nil {
- return left == nil && right == nil
- }
- return *left == *right
- }
- func (s *OpenAIGatewayService) newGrokCredentialFailover(c *gin.Context, account *Account, class grokCredentialFailureClass) error {
- if strings.TrimSpace(class.message) == "" {
- class.message = "Grok OAuth credentials are unavailable"
- }
- appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
- // Credential acquisition happens before the inference transport opens,
- // so the account binding is not evidence of an inference proxy route.
- ProxyID: nil,
- ProxyName: opsProxyNameUnknown,
- Platform: PlatformGrok,
- AccountID: account.ID,
- Stage: string(GatewayFailureStageAccountAuth),
- Scope: string(class.scope),
- Reason: string(class.reason),
- Kind: "credential_failover",
- Message: class.message,
- })
- return &UpstreamFailoverError{
- Stage: GatewayFailureStageAccountAuth,
- Scope: class.scope,
- Reason: class.reason,
- NextAccountAction: class.action,
- ClientStatusCode: http.StatusServiceUnavailable,
- ClientMessage: GrokCredentialUnavailableClientMessage,
- }
- }
|