vibe-proxy/backend/sdk/cliproxy/auth/conductor_home.go
2026-08-24 00:10:41 +02:00

1420 lines
45 KiB
Go

package auth
import (
"context"
"encoding/json"
"errors"
"net/http"
"sort"
"strings"
"sync"
"time"
internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/home"
"github.com/router-for-me/CLIProxyAPI/v7/internal/logging"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executionregistry"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
log "github.com/sirupsen/logrus"
)
const (
homeAuthCountMetadataKey = "__cliproxy_home_auth_count"
homeRetryRoundMetadataKey = "request_retry_round"
// ExcludedAuthIDsMetadataKey stores credential IDs already attempted in the
// current request retry round.
ExcludedAuthIDsMetadataKey = "excluded_auth_ids"
// CloseAllExecutionSessionsID asks an executor to release all active execution sessions.
// Executors that do not support this marker may ignore it.
CloseAllExecutionSessionsID = "__all_execution_sessions__"
)
// HomeDispatchBundle is the immutable client and registry pair for one Home lifetime.
type HomeDispatchBundle struct {
client homeAuthDispatcher
registry *executionregistry.Registry
generation uint64
}
// PublishHomeDispatch publishes the selectable Home lifetime as one atomic bundle.
func (m *Manager) PublishHomeDispatch(client homeAuthDispatcher, registry *executionregistry.Registry, generation uint64) *HomeDispatchBundle {
if m == nil || client == nil || registry == nil {
return nil
}
bundle := &HomeDispatchBundle{client: client, registry: registry, generation: generation}
m.homeDispatchBundle.Store(bundle)
return bundle
}
// ClearHomeDispatchBundle removes bundle only when it still belongs to the active lifetime.
func (m *Manager) ClearHomeDispatchBundle(bundle *HomeDispatchBundle) bool {
if m == nil || bundle == nil {
return false
}
return m.homeDispatchBundle.CompareAndSwap(bundle, nil)
}
// HomeDispatchBundle returns the active Home lifetime bundle.
func (m *Manager) HomeDispatchBundle() *HomeDispatchBundle {
if m == nil {
return nil
}
return m.homeDispatchBundle.Load()
}
// SetHomeExecutionRegistry preserves the legacy registry API for callers that also install the current dispatcher.
func (m *Manager) SetHomeExecutionRegistry(registry *executionregistry.Registry) {
if m == nil {
return
}
m.PublishHomeDispatch(currentHomeDispatcher(), registry, 0)
}
// ClearHomeExecutionRegistry removes a matching legacy registry bundle.
func (m *Manager) ClearHomeExecutionRegistry(registry *executionregistry.Registry) bool {
bundle := m.HomeDispatchBundle()
if bundle == nil || bundle.registry != registry {
return false
}
return m.ClearHomeDispatchBundle(bundle)
}
// HomeExecutionRegistry returns the registry from the active Home lifetime bundle.
func (m *Manager) HomeExecutionRegistry() *executionregistry.Registry {
bundle := m.HomeDispatchBundle()
if bundle == nil {
return nil
}
return bundle.registry
}
// HomeEnabled reports whether the home control plane integration is enabled in the runtime config.
func (m *Manager) HomeEnabled() bool {
if m == nil {
return false
}
cfg, _ := m.runtimeConfig.Load().(*internalconfig.Config)
return cfg != nil && cfg.Home.Enabled
}
func (m *Manager) localExecutionAllowed() bool {
return m != nil && !m.HomeEnabled()
}
func (m *Manager) localFallbackAuth(authID string) *Auth {
if !m.localExecutionAllowed() {
return nil
}
m.mu.RLock()
auth := m.auths[strings.TrimSpace(authID)]
m.mu.RUnlock()
if auth == nil {
return nil
}
return auth.Clone()
}
type homeErrorEnvelope struct {
Error *homeErrorDetail `json:"error"`
}
type homeErrorDetail struct {
Type string `json:"type"`
Message string `json:"message"`
Code string `json:"code,omitempty"`
Retryable bool `json:"retryable,omitempty"`
RetryAfterMS int64 `json:"retry_after_ms,omitempty"`
RequestRetry *int `json:"request_retry,omitempty"`
}
type homeDispatchRetryAfterError struct {
cause *Error
retryAfter time.Duration
requestRetry int
hasRequestRetry bool
}
// homeRetryRoundExhaustedError marks a terminal error produced after the
// current Home credential round has been exhausted. The wrapped error retains
// its status and retry-after metadata for the outer request retry policy.
type homeRetryRoundExhaustedError struct {
cause error
retryAfter time.Duration
hasRetryAfter bool
retryNow bool
}
func (e *homeRetryRoundExhaustedError) Error() string {
if e == nil || e.cause == nil {
return ""
}
return e.cause.Error()
}
func (e *homeRetryRoundExhaustedError) Unwrap() error {
if e == nil {
return nil
}
return e.cause
}
func (e *homeRetryRoundExhaustedError) RetryAfter() *time.Duration {
if e == nil || !e.hasRetryAfter {
return nil
}
value := e.retryAfter
return &value
}
func markHomeRetryRoundExhausted(err error, retryAfter *time.Duration, retryNow bool) error {
if err == nil {
return nil
}
marked := &homeRetryRoundExhaustedError{cause: err, retryNow: retryNow}
if retryAfter != nil {
marked.retryAfter = *retryAfter
marked.hasRetryAfter = true
}
return marked
}
func isHomeRetryRoundExhausted(err error) bool {
if err == nil {
return false
}
var marker *homeRetryRoundExhaustedError
return errors.As(err, &marker) && marker != nil
}
type homeRetryRoundTiming struct {
retryAfter time.Duration
immediate bool
invalid bool
}
func (t *homeRetryRoundTiming) Observe(err error) {
if t == nil || err == nil || t.immediate || t.invalid {
return
}
retryAfter := retryAfterFromError(err)
if retryAfter == nil {
return
}
if *retryAfter == 0 {
t.retryAfter = 0
t.immediate = true
return
}
if *retryAfter < 0 {
t.retryAfter = *retryAfter
t.invalid = true
return
}
if t.retryAfter <= 0 || *retryAfter < t.retryAfter {
t.retryAfter = *retryAfter
}
}
func (t *homeRetryRoundTiming) RetryAfter() *time.Duration {
if t == nil || t.immediate || (!t.invalid && t.retryAfter <= 0) {
return nil
}
value := t.retryAfter
return &value
}
func (e *homeDispatchRetryAfterError) Error() string {
if e == nil || e.cause == nil {
return ""
}
return e.cause.Error()
}
func (e *homeDispatchRetryAfterError) Unwrap() error {
if e == nil {
return nil
}
return e.cause
}
func (e *homeDispatchRetryAfterError) StatusCode() int {
if e == nil || e.cause == nil {
return 0
}
return e.cause.HTTPStatus
}
func (e *homeDispatchRetryAfterError) RetryAfter() *time.Duration {
if e == nil || e.retryAfter <= 0 {
return nil
}
value := e.retryAfter
return &value
}
func (e *homeDispatchRetryAfterError) RequestRetryLimit() (int, bool) {
if e == nil || !e.hasRequestRetry {
return 0, false
}
return e.requestRetry, true
}
const (
homeUpstreamModelAttributeKey = "home_upstream_model"
homeForceMappingAttributeKey = "home_force_mapping"
homeOriginalAliasAttributeKey = "home_original_alias"
homeRequestRetryExceededErrorCode = "request_retry_exceeded"
)
func isHomeRequestRetryExceededError(err error) bool {
var authErr *Error
if !errors.As(err, &authErr) || authErr == nil {
return false
}
return strings.EqualFold(strings.TrimSpace(authErr.Code), homeRequestRetryExceededErrorCode)
}
func shouldReturnLastErrorOnPickFailure(homeMode bool, lastErr error, errPick error) bool {
if lastErr == nil {
return false
}
if !homeMode {
return true
}
if isHomeRequestRetryExceededError(errPick) {
return true
}
var authErr *Error
if !errors.As(errPick, &authErr) || authErr == nil {
return false
}
switch strings.ToLower(strings.TrimSpace(authErr.Code)) {
case "auth_not_found", "auth_unavailable":
return true
default:
return false
}
}
func isHomeNextRoundImmediatelyAvailable(err error) bool {
var authErr *Error
if !errors.As(err, &authErr) || authErr == nil {
return false
}
return strings.EqualFold(strings.TrimSpace(authErr.Code), "auth_unavailable")
}
func pendingHomeRetryRoundDelay(err error, maxWait time.Duration, retryLimit *int, acceptRemoteRetryLimit bool) (time.Duration, bool) {
if err == nil || isHomeRetryRoundExhausted(err) {
return 0, false
}
var homeCooldown *homeDispatchRetryAfterError
if !errors.As(err, &homeCooldown) || homeCooldown == nil {
return 0, false
}
observeHomeCooldownRetryLimit(homeCooldown, retryLimit, acceptRemoteRetryLimit)
retryAfter := homeCooldown.RetryAfter()
if retryAfter == nil || *retryAfter <= 0 || maxWait <= 0 || *retryAfter > maxWait {
return 0, false
}
return *retryAfter, true
}
func homeAuthAlreadyTried(tried map[string]struct{}, authID string) bool {
authID = strings.TrimSpace(authID)
if authID == "" || len(tried) == 0 {
return false
}
_, ok := tried[authID]
return ok
}
func repeatedHomeAuthError() *Error {
return &Error{
Code: homeRequestRetryExceededErrorCode,
Message: "home returned a previously tried auth",
HTTPStatus: http.StatusServiceUnavailable,
}
}
type homeAuthDispatchResponse struct {
Model string `json:"model"`
Provider string `json:"provider"`
AuthIndex string `json:"auth_index"`
UserAPIKey string `json:"user_api_key"`
RequestRetry *int `json:"request_retry,omitempty"`
ForceMapping bool `json:"force_mapping"`
OriginalAlias string `json:"original_alias"`
Auth Auth `json:"auth"`
}
type homeAuthDispatcher interface {
HeartbeatOK() bool
RPopAuth(ctx context.Context, requestedModel string, sessionID string, headers http.Header, count int) ([]byte, error)
AbortAmbiguousDispatch()
}
type homeDispatchConstraintsDispatcher interface {
RPopAuthWithConstraints(ctx context.Context, requestedModel string, sessionID string, headers http.Header, count int, excludedAuthIDs []string, pinnedAuthID string) ([]byte, error)
}
type homeDispatchRetryRoundConstraintsDispatcher interface {
RPopAuthWithRetryRoundConstraints(ctx context.Context, requestedModel string, sessionID string, headers http.Header, count int, retryRound int, excludedAuthIDs []string, pinnedAuthID string) ([]byte, error)
}
type homeCredentialPolicyDispatcher interface {
RPopAuthWithPolicy(ctx context.Context, requestedModel string, sessionID string, headers http.Header, count int, credentialPolicy string) ([]byte, error)
}
type homeCredentialPolicyConstraintsDispatcher interface {
RPopAuthWithPolicyAndConstraints(ctx context.Context, requestedModel string, sessionID string, headers http.Header, count int, credentialPolicy string, excludedAuthIDs []string, pinnedAuthID string) ([]byte, error)
}
type homeCredentialPolicyRetryRoundConstraintsDispatcher interface {
RPopAuthWithPolicyAndRetryRoundConstraints(ctx context.Context, requestedModel string, sessionID string, headers http.Header, count int, credentialPolicy string, retryRound int, excludedAuthIDs []string, pinnedAuthID string) ([]byte, error)
}
var currentHomeDispatcher = func() homeAuthDispatcher {
return home.Current()
}
func setHomeUserAPIKeyOnGinContext(ctx context.Context, apiKey string) {
apiKey = strings.TrimSpace(apiKey)
if apiKey == "" || ctx == nil {
return
}
ginCtx, ok := ctx.Value("gin").(interface{ Set(string, any) })
if !ok || ginCtx == nil {
return
}
ginCtx.Set("userApiKey", apiKey)
}
func homeDispatchHeaders(ctx context.Context, headers http.Header) http.Header {
apiKey, ok := homeQueryCredentialFromContext(ctx)
if !ok {
return headers
}
out := headers.Clone()
if out == nil {
out = http.Header{}
}
if out.Get("Authorization") != "" || out.Get("X-Goog-Api-Key") != "" || out.Get("X-Api-Key") != "" {
return out
}
out.Set("X-Goog-Api-Key", apiKey)
return out
}
func homeQueryCredentialFromContext(ctx context.Context) (string, bool) {
if ctx == nil {
return "", false
}
if queryCtx, ok := ctx.Value("gin").(interface{ Query(string) string }); ok && queryCtx != nil {
if apiKey := strings.TrimSpace(queryCtx.Query("key")); apiKey != "" {
return apiKey, true
}
if apiKey := strings.TrimSpace(queryCtx.Query("auth_token")); apiKey != "" {
return apiKey, true
}
}
ginCtx, ok := ctx.Value("gin").(interface{ Get(string) (any, bool) })
if !ok || ginCtx == nil {
return "", false
}
rawMetadata, ok := ginCtx.Get("accessMetadata")
if !ok {
return "", false
}
source := accessMetadataSource(rawMetadata)
if source != "query-key" && source != "query-auth-token" {
return "", false
}
rawAPIKey, ok := ginCtx.Get("userApiKey")
if !ok {
return "", false
}
apiKey := contextStringValue(rawAPIKey)
if apiKey == "" {
return "", false
}
return apiKey, true
}
func accessMetadataSource(raw any) string {
switch v := raw.(type) {
case map[string]string:
return strings.TrimSpace(v["source"])
case map[string]any:
return contextStringValue(v["source"])
default:
return ""
}
}
func contextStringValue(raw any) string {
switch v := raw.(type) {
case string:
return strings.TrimSpace(v)
case []byte:
return strings.TrimSpace(string(v))
default:
return ""
}
}
func homeExecutionSessionIDFromMetadata(meta map[string]any) string {
if len(meta) == 0 {
return ""
}
raw, ok := meta[cliproxyexecutor.ExecutionSessionMetadataKey]
if !ok || raw == nil {
return ""
}
switch value := raw.(type) {
case string:
return strings.TrimSpace(value)
case []byte:
return strings.TrimSpace(string(value))
default:
return ""
}
}
type homeSessionSelectionKey struct {
credentialID string
routeModel string
}
func (m *Manager) lockHomeWebsocketSession(ctx context.Context, opts cliproxyexecutor.Options) func() {
if m == nil || !cliproxyexecutor.DownstreamWebsocket(ctx) {
return nil
}
sessionID := homeExecutionSessionIDFromMetadata(opts.Metadata)
if sessionID == "" {
return nil
}
lock, _ := m.homeSessionLocks.LoadOrStore(sessionID, &sync.Mutex{})
mutex, ok := lock.(*sync.Mutex)
if !ok || mutex == nil {
return nil
}
mutex.Lock()
return mutex.Unlock
}
func (m *Manager) retainedHomeSessionSelection(ctx context.Context, opts cliproxyexecutor.Options, model string, excludedAuthIDs map[string]struct{}) (*HomeDispatchSelection, bool, error) {
if m == nil || !cliproxyexecutor.DownstreamWebsocket(ctx) {
return nil, false, nil
}
sessionID := homeExecutionSessionIDFromMetadata(opts.Metadata)
credentialID := pinnedAuthIDFromMetadata(opts.Metadata)
if sessionID == "" {
return nil, false, nil
}
routeModel, validRouteModel := validCanonicalHomeConcurrencyModelKey(model)
var retained *HomeDispatchSelection
var ended []*HomeDispatchSelection
fallbackAttempt := homeAuthCountFromMetadata(opts.Metadata) > 1 || homeRetryRoundFromMetadata(opts.Metadata) > 0
m.mu.Lock()
selections := m.homeSessionSelections[sessionID]
for key, selection := range selections {
if selection == nil {
delete(selections, key)
continue
}
matchesCredential := credentialID == "" || key.credentialID == credentialID
matchesRoute := validRouteModel && key.routeModel == routeModel
_, excluded := excludedAuthIDs[strings.TrimSpace(key.credentialID)]
if !fallbackAttempt && !excluded && matchesCredential && selection.Active() && matchesRoute && retained == nil {
retained = selection
continue
}
delete(selections, key)
ended = append(ended, selection)
}
if len(selections) == 0 {
delete(m.homeSessionSelections, sessionID)
}
m.mu.Unlock()
for _, selection := range ended {
if errWait := m.endHomeSelectionBeforeRedispatch(ctx, selection, "target_changed"); errWait != nil {
return nil, false, errWait
}
}
return retained, retained != nil, nil
}
func (m *Manager) predictedHomeConcurrencyModel(auth *Auth, routeModel string) (string, bool) {
requestedModel := rewriteModelForAuth(routeModel, auth)
aliasResult := m.resolveExecutionAliasResultForRequested(auth, requestedModel)
upstreamModel := executionAliasPoolModel(auth, requestedModel, aliasResult)
if pool := m.resolveOpenAICompatUpstreamModelPool(auth, upstreamModel); len(pool) != 0 {
if len(pool) != 1 {
return "", false
}
upstreamModel = pool[0]
} else {
upstreamModel = m.applyAPIKeyModelAlias(auth, upstreamModel)
}
return validCanonicalHomeConcurrencyModelKey(upstreamModel)
}
func (m *Manager) endMismatchedHomeSessionSelections(ctx context.Context, sessionID, credentialID, model string, waitForAck bool) error {
if m == nil || sessionID == "" {
return nil
}
routeModel, validRouteModel := validCanonicalHomeConcurrencyModelKey(model)
var ended []*HomeDispatchSelection
m.mu.Lock()
selections := m.homeSessionSelections[sessionID]
for key, selection := range selections {
if selection == nil {
delete(selections, key)
continue
}
matchesRoute := validRouteModel && key.routeModel == routeModel
if key.credentialID == credentialID && matchesRoute {
continue
}
delete(selections, key)
ended = append(ended, selection)
}
if len(selections) == 0 {
delete(m.homeSessionSelections, sessionID)
}
m.mu.Unlock()
for _, selection := range ended {
if !waitForAck {
selection.End("target_changed")
continue
}
if errWait := m.endHomeSelectionBeforeRedispatch(ctx, selection, "target_changed"); errWait != nil {
return errWait
}
}
return nil
}
func (m *Manager) endHomeSelectionBeforeRedispatch(ctx context.Context, selection *HomeDispatchSelection, reason string) error {
if selection == nil {
return nil
}
ticket := selection.EndWithRelease(reason)
if ticket == nil {
return nil
}
bound := internalconfig.CredentialConcurrencyConfig{}.WithDefaults().CPACancelBound
if m != nil {
if cfg, ok := m.runtimeConfig.Load().(*internalconfig.Config); ok && cfg != nil {
bound = cfg.CredentialConcurrency.WithDefaults().CPACancelBound
}
}
waitCtx := ctx
if waitCtx == nil {
waitCtx = context.Background()
}
waitCtx, cancelWait := context.WithTimeout(waitCtx, bound)
defer cancelWait()
if errWait := ticket.Wait(waitCtx); errWait != nil {
return &Error{Code: "home_unavailable", Message: "Home did not acknowledge credential release: " + errWait.Error(), Retryable: true, HTTPStatus: http.StatusServiceUnavailable}
}
return nil
}
func (m *Manager) retainHomeWebsocketSelection(ctx context.Context, opts cliproxyexecutor.Options, model string, selection *HomeDispatchSelection) bool {
if m == nil || selection == nil || !selection.Retained() || !cliproxyexecutor.DownstreamWebsocket(ctx) {
return false
}
selectionAuth := selection.CloneAuth()
if selectionAuth == nil {
return false
}
sessionID := homeExecutionSessionIDFromMetadata(opts.Metadata)
credentialID := strings.TrimSpace(selectionAuth.ID)
routeModel, validRouteModel := validCanonicalHomeConcurrencyModelKey(model)
if selection.accountedModel == "" {
selection.accountedModel, _ = m.predictedHomeConcurrencyModel(selectionAuth, model)
}
if sessionID == "" || credentialID == "" || !validRouteModel || selection.accountedModel == "" {
return false
}
_ = m.endMismatchedHomeSessionSelections(ctx, sessionID, credentialID, routeModel, false)
key := homeSessionSelectionKey{credentialID: credentialID, routeModel: routeModel}
m.mu.Lock()
if m.homeSessionSelections == nil {
m.homeSessionSelections = make(map[string]map[homeSessionSelectionKey]*HomeDispatchSelection)
}
selections := m.homeSessionSelections[sessionID]
if selections == nil {
selections = make(map[homeSessionSelectionKey]*HomeDispatchSelection)
m.homeSessionSelections[sessionID] = selections
}
previous := selections[key]
selections[key] = selection
m.mu.Unlock()
m.rememberHomeRuntimeAuth(sessionID, selectionAuth)
if previous != nil && previous != selection {
previous.End("target_replaced")
}
return true
}
func (m *Manager) clearHomeSessionLocks() {
if m == nil {
return
}
m.homeSessionLocks.Range(func(key, _ any) bool {
m.homeSessionLocks.Delete(key)
return true
})
}
func (m *Manager) takeHomeSessionSelectionsLocked(sessionID string) []*HomeDispatchSelection {
if m == nil {
return nil
}
selections := m.homeSessionSelections[sessionID]
delete(m.homeSessionSelections, sessionID)
result := make([]*HomeDispatchSelection, 0, len(selections))
for _, selection := range selections {
result = append(result, selection)
}
return result
}
func (m *Manager) takeAllHomeSessionSelectionsLocked() []*HomeDispatchSelection {
if m == nil {
return nil
}
result := make([]*HomeDispatchSelection, 0)
for sessionID, selections := range m.homeSessionSelections {
delete(m.homeSessionSelections, sessionID)
for _, selection := range selections {
result = append(result, selection)
}
}
return result
}
func (m *Manager) clearHomeRuntimeAuths() {
if m == nil {
return
}
m.mu.Lock()
m.clearHomeRuntimeAuthsLocked()
selections := m.takeAllHomeSessionSelectionsLocked()
m.mu.Unlock()
m.homeSessionAliases.clear()
for _, selection := range selections {
selection.End("home_disabled")
}
}
func (m *Manager) clearHomeRuntimeAuthsLocked() {
if m == nil {
return
}
m.homeRuntimeAuths = make(map[string]map[string]*Auth)
m.homeRuntimeAuthOwners = make(map[string]map[string]*HomeDispatchSelection)
}
func (m *Manager) clearHomeRuntimeAuthsForSessionLocked(sessionID string) {
sessionID = strings.TrimSpace(sessionID)
if m == nil || sessionID == "" {
return
}
delete(m.homeRuntimeAuths, sessionID)
delete(m.homeRuntimeAuthOwners, sessionID)
}
func (m *Manager) bindHomeSelectionRuntimeAuth(ctx context.Context, opts cliproxyexecutor.Options, selection *HomeDispatchSelection) error {
if m == nil || selection == nil || !cliproxyexecutor.DownstreamWebsocket(ctx) {
return nil
}
selectionAuth := selection.CloneAuth()
if selectionAuth == nil || !authWebsocketsEnabled(selectionAuth) {
return nil
}
sessionID := homeExecutionSessionIDFromMetadata(opts.Metadata)
authID := strings.TrimSpace(selectionAuth.ID)
if sessionID == "" || authID == "" || !selection.runtimeAuthBound.CompareAndSwap(false, true) {
return nil
}
m.rememberHomeSelectionRuntimeAuth(sessionID, selection)
if errBind := selection.Bind(func() error {
m.forgetHomeRuntimeAuth(sessionID, authID, selection)
return nil
}); errBind != nil {
selection.runtimeAuthBound.Store(false)
m.forgetHomeRuntimeAuth(sessionID, authID, selection)
return errBind
}
return nil
}
func (m *Manager) rememberHomeSelectionRuntimeAuth(sessionID string, selection *HomeDispatchSelection) {
if m == nil || selection == nil {
return
}
selectionAuth := selection.CloneAuth()
if selectionAuth == nil {
return
}
sessionID = strings.TrimSpace(sessionID)
authID := strings.TrimSpace(selectionAuth.ID)
if sessionID == "" || authID == "" {
return
}
m.mu.Lock()
if m.homeRuntimeAuths == nil {
m.homeRuntimeAuths = make(map[string]map[string]*Auth)
}
if m.homeRuntimeAuthOwners == nil {
m.homeRuntimeAuthOwners = make(map[string]map[string]*HomeDispatchSelection)
}
if m.homeRuntimeAuths[sessionID] == nil {
m.homeRuntimeAuths[sessionID] = make(map[string]*Auth)
}
if m.homeRuntimeAuthOwners[sessionID] == nil {
m.homeRuntimeAuthOwners[sessionID] = make(map[string]*HomeDispatchSelection)
}
m.homeRuntimeAuths[sessionID][authID] = selectionAuth
m.homeRuntimeAuthOwners[sessionID][authID] = selection
m.mu.Unlock()
}
func (m *Manager) replaceHomeSelectionAuth(selection *HomeDispatchSelection, auth *Auth) {
if m == nil || selection == nil || auth == nil {
return
}
m.mu.Lock()
selection.ReplaceAuth(auth)
updated := selection.CloneAuth()
if updated == nil {
m.mu.Unlock()
return
}
for sessionID, owners := range m.homeRuntimeAuthOwners {
for authID, owner := range owners {
if owner != selection || m.homeRuntimeAuths[sessionID] == nil {
continue
}
m.homeRuntimeAuths[sessionID][authID] = updated.Clone()
}
}
m.mu.Unlock()
}
func (m *Manager) forgetHomeRuntimeAuth(sessionID string, authID string, owner *HomeDispatchSelection) {
sessionID = strings.TrimSpace(sessionID)
authID = strings.TrimSpace(authID)
if m == nil || sessionID == "" || authID == "" {
return
}
m.mu.Lock()
owners := m.homeRuntimeAuthOwners[sessionID]
if owner != nil && owners[authID] != owner {
m.mu.Unlock()
return
}
sessionAuths := m.homeRuntimeAuths[sessionID]
delete(sessionAuths, authID)
delete(owners, authID)
if len(sessionAuths) == 0 {
delete(m.homeRuntimeAuths, sessionID)
}
if len(owners) == 0 {
delete(m.homeRuntimeAuthOwners, sessionID)
}
m.mu.Unlock()
}
func (m *Manager) rememberHomeRuntimeAuth(sessionID string, auth *Auth) {
sessionID = strings.TrimSpace(sessionID)
authID := ""
if auth != nil {
authID = strings.TrimSpace(auth.ID)
}
if m == nil || auth == nil || sessionID == "" || authID == "" || !authWebsocketsEnabled(auth) {
return
}
m.mu.Lock()
if m.homeRuntimeAuths == nil {
m.homeRuntimeAuths = make(map[string]map[string]*Auth)
}
sessionAuths := m.homeRuntimeAuths[sessionID]
if sessionAuths == nil {
sessionAuths = make(map[string]*Auth)
m.homeRuntimeAuths[sessionID] = sessionAuths
}
sessionAuths[authID] = auth.Clone()
m.mu.Unlock()
}
func (m *Manager) homeRuntimeAuthByID(sessionID string, authID string) (*Auth, ProviderExecutor, string, bool) {
sessionID = strings.TrimSpace(sessionID)
authID = strings.TrimSpace(authID)
if m == nil || sessionID == "" || authID == "" {
return nil, nil, "", false
}
m.mu.RLock()
sessionAuths := m.homeRuntimeAuths[sessionID]
auth := sessionAuths[authID]
m.mu.RUnlock()
if auth == nil || !authWebsocketsEnabled(auth) {
return nil, nil, "", false
}
logicalProvider := strings.ToLower(strings.TrimSpace(auth.Provider))
executorKey := executorKeyFromAuth(auth)
if logicalProvider == "" || executorKey == "" {
return nil, nil, "", false
}
executor, ok := m.Executor(executorKey)
if !ok && auth.Attributes != nil && strings.TrimSpace(auth.Attributes["base_url"]) != "" {
executor, ok = m.Executor("openai-compatibility")
}
if !ok {
return nil, nil, "", false
}
return auth.Clone(), executor, logicalProvider, true
}
func (m *Manager) pickNextViaHome(ctx context.Context, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, string, error) {
if m == nil {
return nil, nil, "", &Error{Code: "auth_not_found", Message: "no auth available"}
}
if ctx == nil {
ctx = context.Background()
}
selection, errSelection := m.pickHomeDispatchSelection(ctx, model, withHomeExcludedAuthIDs(opts, tried))
if errSelection != nil {
return nil, nil, "", errSelection
}
selectionAuth := selection.CloneAuth()
if selectionAuth == nil || homeAuthAlreadyTried(tried, selectionAuth.ID) {
selection.End("repeated_auth")
return nil, nil, "", repeatedHomeAuthError()
}
auth := selection.CloneAuthForRoute(model)
executor := selection.Executor
provider := selection.Provider
selection.End("legacy_selection_unbound")
return auth, executor, provider, nil
}
func (m *Manager) pickHomeDispatchSelection(ctx context.Context, model string, opts cliproxyexecutor.Options) (*HomeDispatchSelection, error) {
if m == nil {
return nil, &Error{Code: "auth_not_found", Message: "no auth available"}
}
if ctx == nil {
ctx = context.Background()
}
requestedModel := strings.TrimSpace(model)
if requestedModel == "" {
requestedModel = requestedModelFromMetadata(opts.Metadata, model)
}
pinnedAuthID := pinnedAuthIDFromMetadata(opts.Metadata)
retryRound := homeRetryRoundFromMetadata(opts.Metadata)
excludedAuthIDList := homeExcludedAuthIDsFromMetadata(opts.Metadata)
excludedAuthIDs := make(map[string]struct{}, len(excludedAuthIDList))
for _, authID := range excludedAuthIDList {
excludedAuthIDs[authID] = struct{}{}
}
retained, retainedOK, errRetained := m.retainedHomeSessionSelection(ctx, opts, requestedModel, excludedAuthIDs)
if errRetained != nil {
return nil, errRetained
}
if retainedOK {
return retained, nil
}
if sessionID := homeExecutionSessionIDFromMetadata(opts.Metadata); sessionID != "" {
if pinnedAuthID != "" {
if errEnd := m.endMismatchedHomeSessionSelections(ctx, sessionID, pinnedAuthID, requestedModel, true); errEnd != nil {
return nil, errEnd
}
}
}
bundle := m.HomeDispatchBundle()
if bundle == nil || bundle.client == nil || bundle.registry == nil {
return nil, &Error{Code: "home_unavailable", Message: "home dispatch bundle unavailable", HTTPStatus: http.StatusServiceUnavailable}
}
client := bundle.client
registry := bundle.registry
if !client.HeartbeatOK() {
return nil, &Error{Code: "home_unavailable", Message: "home control center unavailable", HTTPStatus: http.StatusServiceUnavailable}
}
if pinnedAuthID != "" {
if _, excluded := excludedAuthIDs[pinnedAuthID]; excluded {
return nil, &Error{Code: "auth_not_found", Message: "pinned auth is unavailable in the current retry round", HTTPStatus: http.StatusServiceUnavailable}
}
}
pending, errBegin := registry.BeginDispatch()
if errBegin != nil {
return nil, &Error{Code: "home_unavailable", Message: "home execution registry unavailable", Retryable: true, HTTPStatus: http.StatusServiceUnavailable}
}
sessionID := m.homeDispatchSessionID(opts)
dispatchHeaders := homeDispatchHeaders(ctx, opts.Headers)
credentialPolicy := credentialPolicyFromContext(ctx)
var raw []byte
var errRPop error
if credentialPolicy == "" {
if retryRoundClient, okRetryRound := client.(homeDispatchRetryRoundConstraintsDispatcher); okRetryRound {
raw, errRPop = retryRoundClient.RPopAuthWithRetryRoundConstraints(ctx, requestedModel, sessionID, dispatchHeaders, homeAuthCountFromMetadata(opts.Metadata), retryRound, excludedAuthIDList, pinnedAuthID)
} else if constrainedClient, okConstraints := client.(homeDispatchConstraintsDispatcher); okConstraints {
raw, errRPop = constrainedClient.RPopAuthWithConstraints(ctx, requestedModel, sessionID, dispatchHeaders, homeAuthCountFromMetadata(opts.Metadata), excludedAuthIDList, pinnedAuthID)
} else {
raw, errRPop = client.RPopAuth(ctx, requestedModel, sessionID, dispatchHeaders, homeAuthCountFromMetadata(opts.Metadata))
}
} else if retryRoundPolicyClient, okRetryRound := client.(homeCredentialPolicyRetryRoundConstraintsDispatcher); okRetryRound {
raw, errRPop = retryRoundPolicyClient.RPopAuthWithPolicyAndRetryRoundConstraints(ctx, requestedModel, sessionID, dispatchHeaders, homeAuthCountFromMetadata(opts.Metadata), credentialPolicy, retryRound, excludedAuthIDList, pinnedAuthID)
} else if policyClient, okPolicy := client.(homeCredentialPolicyDispatcher); okPolicy {
if constrainedClient, okConstraints := client.(homeCredentialPolicyConstraintsDispatcher); okConstraints {
raw, errRPop = constrainedClient.RPopAuthWithPolicyAndConstraints(ctx, requestedModel, sessionID, dispatchHeaders, homeAuthCountFromMetadata(opts.Metadata), credentialPolicy, excludedAuthIDList, pinnedAuthID)
} else {
raw, errRPop = policyClient.RPopAuthWithPolicy(ctx, requestedModel, sessionID, dispatchHeaders, homeAuthCountFromMetadata(opts.Metadata), credentialPolicy)
}
} else {
pending.End()
return nil, &Error{Code: "home_unavailable", Message: "home dispatcher does not support credential policies", HTTPStatus: http.StatusServiceUnavailable}
}
if errRPop != nil {
if home.IsAmbiguousDispatchError(errRPop) {
client.AbortAmbiguousDispatch()
}
pending.End()
if errors.Is(errRPop, home.ErrAuthNotFound) {
return nil, &Error{Code: "auth_not_found", Message: errRPop.Error(), HTTPStatus: http.StatusServiceUnavailable}
}
return nil, &Error{Code: "home_unavailable", Message: errRPop.Error(), Retryable: true, HTTPStatus: http.StatusServiceUnavailable}
}
envelope, errEnvelope := decodeHomeDispatchConcurrencyEnvelope(raw)
if errEnvelope != nil {
if envelope.Present {
client.AbortAmbiguousDispatch()
}
pending.End()
if envelope.Present {
return nil, invalidHomeConcurrencyResponse("Home returned malformed concurrency tuple")
}
return nil, &Error{Code: "invalid_auth", Message: "home returned invalid auth payload", HTTPStatus: http.StatusBadGateway}
}
kind := "http"
if cliproxyexecutor.DownstreamWebsocket(ctx) {
kind = "websocket"
} else if opts.Stream {
kind = "stream"
}
baseScope := executionregistry.ScopeSpec{
RequestID: logging.GetRequestID(ctx),
Model: requestedModel,
Kind: kind,
StartedAt: time.Now(),
}
var scope *executionregistry.Scope
if envelope.Present {
var errInstall error
scope, errInstall = installHomeConcurrencyScope(registry, pending, envelope.Tuple, baseScope)
if errInstall != nil {
client.AbortAmbiguousDispatch()
pending.End()
return nil, homeConcurrencyInstallError(errInstall)
}
}
endScope := func() {
if scope != nil {
scope.End("local_validation_failed")
return
}
pending.End()
}
if errHome := decodeHomeDispatchError(raw); errHome != nil {
if envelope.Present {
client.AbortAmbiguousDispatch()
endScope()
return nil, invalidHomeConcurrencyResponse("Home returned both accounted concurrency and an error")
}
pending.End()
return nil, errHome
}
var dispatch homeAuthDispatchResponse
if errUnmarshal := json.Unmarshal(raw, &dispatch); errUnmarshal != nil {
endScope()
return nil, &Error{Code: "invalid_auth", Message: "home returned invalid auth payload", HTTPStatus: http.StatusBadGateway}
}
auth := dispatch.Auth
if strings.TrimSpace(auth.ID) == "" {
// Backward compatibility: older Home instances returned the auth directly.
if errUnmarshal := json.Unmarshal(raw, &auth); errUnmarshal != nil {
endScope()
return nil, &Error{Code: "invalid_auth", Message: "home returned invalid auth payload", HTTPStatus: http.StatusBadGateway}
}
}
observedModel := canonicalHomeDispatchModel(dispatch.Model, requestedModel)
if envelope.Present {
observedConcurrencyModel, validModel := validCanonicalHomeConcurrencyModelKey(observedModel)
if !validModel || envelope.Tuple.Model != observedConcurrencyModel {
client.AbortAmbiguousDispatch()
endScope()
return nil, invalidHomeConcurrencyResponse("Home concurrency model does not match dispatched model")
}
}
if !envelope.Present {
baseScope.Model = observedModel
}
setHomeUserAPIKeyOnGinContext(ctx, dispatch.UserAPIKey)
if upstreamModel := strings.TrimSpace(dispatch.Model); upstreamModel != "" {
if auth.Attributes == nil {
auth.Attributes = make(map[string]string, 3)
}
auth.Attributes[homeUpstreamModelAttributeKey] = upstreamModel
}
if originalAlias := strings.TrimSpace(dispatch.OriginalAlias); dispatch.ForceMapping && originalAlias != "" {
if auth.Attributes == nil {
auth.Attributes = make(map[string]string, 2)
}
auth.Attributes[homeForceMappingAttributeKey] = "true"
auth.Attributes[homeOriginalAliasAttributeKey] = originalAlias
}
if strings.TrimSpace(auth.ID) == "" {
endScope()
return nil, &Error{Code: "invalid_auth", Message: "home returned auth without id", HTTPStatus: http.StatusBadGateway}
}
if pinnedAuthID != "" && strings.TrimSpace(auth.ID) != pinnedAuthID {
endScope()
return nil, &Error{Code: "auth_not_found", Message: "home returned an auth that does not match the pinned credential", HTTPStatus: http.StatusServiceUnavailable}
}
if errIdentity := verifyAccountedHomeConcurrencyIdentity(envelope.Tuple, &auth, dispatch.AuthIndex); errIdentity != nil {
endScope()
return nil, errIdentity
}
logicalProvider := strings.ToLower(strings.TrimSpace(auth.Provider))
executorKey := executorKeyFromAuth(&auth)
if logicalProvider == "" || executorKey == "" {
endScope()
return nil, &Error{Code: "invalid_auth", Message: "home returned auth without provider", HTTPStatus: http.StatusBadGateway}
}
homeAuthIndex := strings.TrimSpace(dispatch.AuthIndex)
if homeAuthIndex != "" {
auth.Index = homeAuthIndex
auth.indexAssigned = true
} else {
auth.EnsureIndex()
}
executor, okExecutor := m.Executor(executorKey)
if !okExecutor && auth.Attributes != nil && strings.TrimSpace(auth.Attributes["base_url"]) != "" {
executor, okExecutor = m.Executor("openai-compatibility")
}
if !okExecutor {
endScope()
return nil, &Error{Code: "executor_not_found", Message: "executor not registered", HTTPStatus: http.StatusBadGateway}
}
if scope == nil {
var errInstall error
scope, errInstall = installHomeConcurrencyScope(registry, pending, homeConcurrencyTuple{}, executionregistry.ScopeSpec{
RequestID: baseScope.RequestID,
CredentialID: strings.TrimSpace(auth.ID),
Model: baseScope.Model,
Kind: baseScope.Kind,
StartedAt: baseScope.StartedAt,
})
if errInstall != nil {
client.AbortAmbiguousDispatch()
pending.End()
return nil, homeConcurrencyInstallError(errInstall)
}
}
selection, errSelection := newHomeDispatchSelection(auth.Clone(), executor, logicalProvider, scope)
if errSelection != nil {
endScope()
return nil, &Error{Code: "home_unavailable", Message: "home execution registry unavailable", Retryable: true, HTTPStatus: http.StatusServiceUnavailable}
}
if pinnedAuthID == "" && dispatch.RequestRetry != nil && *dispatch.RequestRetry >= 0 {
selection.requestRetry = *dispatch.RequestRetry
selection.hasRequestRetry = true
}
if envelope.Present {
selection.accountedModel = envelope.Tuple.Model
}
if executionSessionID := homeExecutionSessionIDFromMetadata(opts.Metadata); executionSessionID != "" && cliproxyexecutor.DownstreamWebsocket(ctx) {
if errEnd := m.endMismatchedHomeSessionSelections(ctx, executionSessionID, strings.TrimSpace(auth.ID), requestedModel, true); errEnd != nil {
selection.End("target_change_release_failed")
return nil, errEnd
}
}
return selection, nil
}
func homeRetryRoundFromMetadata(metadata map[string]any) int {
if metadata == nil {
return 0
}
switch value := metadata[homeRetryRoundMetadataKey].(type) {
case int:
if value > 0 {
return value
}
case int64:
if value > 0 {
return int(value)
}
case float64:
if value > 0 && value == float64(int(value)) {
return int(value)
}
}
return 0
}
func requestedModelFromMetadata(metadata map[string]any, fallback string) string {
if metadata != nil {
if v, ok := metadata[cliproxyexecutor.RequestedModelMetadataKey]; ok {
switch typed := v.(type) {
case string:
if trimmed := strings.TrimSpace(typed); trimmed != "" {
return trimmed
}
case []byte:
if trimmed := strings.TrimSpace(string(typed)); trimmed != "" {
return trimmed
}
}
}
}
fallback = strings.TrimSpace(fallback)
if fallback == "" {
return "unknown"
}
return fallback
}
func (m *Manager) findAllAntigravityCreditsCandidateAuths(ctx context.Context, routeModel string, opts cliproxyexecutor.Options) ([]creditsCandidateEntry, error) {
if m == nil || !m.localExecutionAllowed() {
return nil, nil
}
pinnedAuthID := pinnedAuthIDFromMetadata(opts.Metadata)
var candidates []creditsCandidateEntry
m.mu.RLock()
for _, auth := range m.auths {
if auth == nil || auth.Disabled || auth.Status == StatusDisabled {
continue
}
if pinnedAuthID != "" && auth.ID != pinnedAuthID {
continue
}
if !strings.EqualFold(strings.TrimSpace(auth.Provider), "antigravity") {
continue
}
if !strings.Contains(strings.ToLower(strings.TrimSpace(routeModel)), "claude") {
continue
}
providerKey := executorKeyFromAuth(auth)
executor, ok := m.executors[providerKey]
if !ok {
continue
}
candidates = append(candidates, creditsCandidateEntry{
auth: auth.Clone(),
executor: executor,
provider: providerKey,
})
}
m.mu.RUnlock()
var known []creditsCandidateEntry
var unknown []creditsCandidateEntry
for _, candidate := range candidates {
hint, okHint, errHint := GetAntigravityCreditsHintRequired(ctx, candidate.auth.ID)
if errHint != nil {
return nil, antigravityCreditsKVUnavailableError(errHint)
}
if okHint && hint.Known {
if !hint.Available {
continue
}
known = append(known, candidate)
continue
}
unknown = append(unknown, candidate)
}
sort.Slice(known, func(i, j int) bool {
return known[i].auth.ID < known[j].auth.ID
})
sort.Slice(unknown, func(i, j int) bool {
return unknown[i].auth.ID < unknown[j].auth.ID
})
return append(known, unknown...), nil
}
type creditsCandidateEntry struct {
auth *Auth
executor ProviderExecutor
provider string
}
func hasAntigravityProvider(providers []string) bool {
for _, p := range providers {
if strings.EqualFold(strings.TrimSpace(p), "antigravity") {
return true
}
}
return false
}
func shouldAttemptAntigravityCreditsFallback(m *Manager, lastErr error, providers []string) bool {
if isRequestTerminatedError(lastErr) {
return false
}
status := statusCodeFromError(lastErr)
log.WithFields(log.Fields{
"lastErr": errorString(lastErr),
"status": status,
"providers": providers,
}).Debug("shouldAttemptAntigravityCreditsFallback")
if m == nil || lastErr == nil || m.HomeEnabled() {
return false
}
cfg, _ := m.runtimeConfig.Load().(*internalconfig.Config)
if cfg == nil || !cfg.QuotaExceeded.AntigravityCredits {
return false
}
switch status {
case http.StatusTooManyRequests, http.StatusServiceUnavailable:
return true
case 0:
var authErr *Error
if errors.As(lastErr, &authErr) && authErr != nil {
return authErr.Code == "auth_not_found" || authErr.Code == "auth_unavailable" || authErr.Code == "model_cooldown"
}
var cooldownErr *modelCooldownError
if errors.As(lastErr, &cooldownErr) {
return true
}
return false
default:
return false
}
}
func (m *Manager) tryAntigravityCreditsExecute(ctx context.Context, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, bool, error) {
if m != nil && m.HomeEnabled() {
return cliproxyexecutor.Response{}, false, &Error{Code: "home_fallback_unsupported", Message: "Home does not support Antigravity credits fallback", HTTPStatus: http.StatusServiceUnavailable}
}
if !m.localExecutionAllowed() {
return cliproxyexecutor.Response{}, false, nil
}
routeModel := req.Model
candidates, errCandidates := m.findAllAntigravityCreditsCandidateAuths(ctx, routeModel, opts)
if errCandidates != nil {
return cliproxyexecutor.Response{}, false, errCandidates
}
for _, c := range candidates {
if ctx.Err() != nil {
return cliproxyexecutor.Response{}, false, nil
}
creditsCtx := WithAntigravityCredits(ctx)
if rt := m.roundTripperFor(c.auth); rt != nil {
creditsCtx = context.WithValue(creditsCtx, roundTripperContextKey{}, rt)
creditsCtx = context.WithValue(creditsCtx, "cliproxy.roundtripper", rt)
}
creditsOpts := ensureRequestedModelMetadata(opts, routeModel)
creditsCtx = contextWithRequestedModelAlias(creditsCtx, creditsOpts, routeModel)
preparedAuth, errPrepare := m.prepareRequestAuth(creditsCtx, c.executor, c.auth)
if errPrepare != nil {
continue
}
c.auth = preparedAuth
publishSelectedAuthMetadata(creditsOpts.Metadata, c.auth)
models, pooled, aliasResult, routing := m.executionModelCandidatesWithAlias(c.auth, routeModel)
if len(models) == 0 {
continue
}
for _, upstreamModel := range models {
resultModel := m.stateModelForExecution(c.auth, routeModel, upstreamModel, pooled)
execReq := req
execReq.Model = upstreamModel
resp, errExec := c.executor.Execute(creditsCtx, c.auth, execReq, creditsOpts)
result := Result{AuthID: c.auth.ID, Provider: c.provider, Model: resultModel, Success: errExec == nil, Options: creditsOpts}
if errExec != nil {
result.Error = resultErrorFromError(errExec)
if ra := retryAfterFromError(errExec); ra != nil {
result.RetryAfter = ra
}
if isCredentialScopedError(errExec) {
result.CredentialScope = true
}
m.MarkResult(creditsCtx, result)
if result.CredentialScope {
break
}
continue
}
m.MarkResult(creditsCtx, result)
attemptAliasResult := resolveAttemptAliasResult(routing, c.auth, routeModel, upstreamModel, aliasResult)
rewriteForceMappedResponse(&resp, attemptAliasResult)
return resp, true, nil
}
}
return cliproxyexecutor.Response{}, false, nil
}
func (m *Manager) tryAntigravityCreditsExecuteStream(ctx context.Context, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (*cliproxyexecutor.StreamResult, bool, error) {
if m != nil && m.HomeEnabled() {
return nil, false, &Error{Code: "home_fallback_unsupported", Message: "Home does not support Antigravity credits fallback", HTTPStatus: http.StatusServiceUnavailable}
}
if !m.localExecutionAllowed() {
return nil, false, nil
}
routeModel := req.Model
candidates, errCandidates := m.findAllAntigravityCreditsCandidateAuths(ctx, routeModel, opts)
if errCandidates != nil {
return nil, false, errCandidates
}
for _, c := range candidates {
if ctx.Err() != nil {
return nil, false, nil
}
creditsCtx := WithAntigravityCredits(ctx)
if rt := m.roundTripperFor(c.auth); rt != nil {
creditsCtx = context.WithValue(creditsCtx, roundTripperContextKey{}, rt)
creditsCtx = context.WithValue(creditsCtx, "cliproxy.roundtripper", rt)
}
creditsOpts := ensureRequestedModelMetadata(opts, routeModel)
preparedAuth, errPrepare := m.prepareRequestAuth(creditsCtx, c.executor, c.auth)
if errPrepare != nil {
continue
}
c.auth = preparedAuth
publishSelectedAuthMetadata(creditsOpts.Metadata, c.auth)
models, pooled, aliasResult, routing := m.executionModelCandidatesWithAlias(c.auth, routeModel)
if len(models) == 0 {
continue
}
result, errStream := m.executeStreamWithModelPool(creditsCtx, c.executor, c.auth, c.provider, req, creditsOpts, routeModel, "", models, pooled, aliasResult, routing, true, false, nil)
if errStream != nil {
continue
}
return result, true, nil
}
return nil, false, nil
}
func antigravityCreditsKVUnavailableError(cause error) error {
if cause == nil {
return &Error{Code: "home_kv_unavailable", Message: "home kv store unavailable", HTTPStatus: http.StatusServiceUnavailable}
}
return &Error{Code: "home_kv_unavailable", Message: "home kv store unavailable: " + cause.Error(), HTTPStatus: http.StatusServiceUnavailable}
}