feat(antigravity): conductor-level credits fallback for Claude models
Move credits handling from executor-level retry to conductor-level orchestration. When all free-tier auths are exhausted (429/503), the conductor discovers auths with available Google One AI credits and retries with enabledCreditTypes injected via context flag. Key changes: - Add AntigravityCreditsHint system for tracking per-auth credits state - Conductor tries credits fallback after all auths fail (Execute/Stream/Count) - Executor injects enabledCreditTypes only when conductor sets context flag - Credits fallback respects provider scope (requires antigravity in providers) - Add context cancellation check in credits fallback to avoid wasted requests - Remove executor-level attemptCreditsFallback and preferCredits machinery - Restructure 429 decision logic (parse details first, keyword fallback) - Expand shouldAbort to cover INVALID_ARGUMENT/FAILED_PRECONDITION/500+UNKNOWN - Support human-readable retry delay parsing (e.g. "1h43m56s")
This commit is contained in:
@@ -52,8 +52,6 @@ const (
|
||||
defaultAntigravityAgent = "antigravity/1.21.9 darwin/arm64" // fallback only; overridden at runtime by misc.AntigravityUserAgent()
|
||||
antigravityAuthType = "antigravity"
|
||||
refreshSkew = 3000 * time.Second
|
||||
antigravityCreditsRetryTTL = 5 * time.Hour
|
||||
antigravityCreditsAutoDisableDuration = 5 * time.Hour
|
||||
antigravityShortQuotaCooldownThreshold = 5 * time.Minute
|
||||
antigravityInstantRetryThreshold = 3 * time.Second
|
||||
// systemInstruction = "You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.**Absolute paths only****Proactiveness**"
|
||||
@@ -62,8 +60,6 @@ const (
|
||||
type antigravity429Category string
|
||||
|
||||
type antigravityCreditsFailureState struct {
|
||||
Count int
|
||||
DisabledUntil time.Time
|
||||
PermanentlyDisabled bool
|
||||
ExplicitBalanceExhausted bool
|
||||
}
|
||||
@@ -91,28 +87,79 @@ var (
|
||||
randSource = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
randSourceMutex sync.Mutex
|
||||
antigravityCreditsFailureByAuth sync.Map
|
||||
antigravityPreferCreditsByModel sync.Map
|
||||
antigravityShortCooldownByAuth sync.Map
|
||||
antigravityCreditsBalanceByAuth sync.Map // auth.ID → antigravityCreditsBalance
|
||||
antigravityQuotaExhaustedKeywords = []string{
|
||||
"quota_exhausted",
|
||||
"quota exhausted",
|
||||
}
|
||||
antigravityCreditsExhaustedKeywords = []string{
|
||||
"google_one_ai",
|
||||
"insufficient credit",
|
||||
"insufficient credits",
|
||||
"not enough credit",
|
||||
"not enough credits",
|
||||
"credit exhausted",
|
||||
"credits exhausted",
|
||||
"credit balance",
|
||||
"minimumcreditamountforusage",
|
||||
"minimum credit amount for usage",
|
||||
"minimum credit",
|
||||
"resource has been exhausted",
|
||||
}
|
||||
)
|
||||
|
||||
type antigravityCreditsBalance struct {
|
||||
CreditAmount float64
|
||||
MinCreditAmount float64
|
||||
PaidTierID string
|
||||
Known bool
|
||||
}
|
||||
|
||||
func antigravityAuthHasCredits(auth *cliproxyauth.Auth) bool {
|
||||
if auth == nil || strings.TrimSpace(auth.ID) == "" {
|
||||
return false
|
||||
}
|
||||
if hint, ok := cliproxyauth.GetAntigravityCreditsHint(auth.ID); ok && hint.Known {
|
||||
return hint.Available
|
||||
}
|
||||
val, ok := antigravityCreditsBalanceByAuth.Load(strings.TrimSpace(auth.ID))
|
||||
if !ok {
|
||||
return true // optimistic: assume credits available when balance unknown
|
||||
}
|
||||
bal, valid := val.(antigravityCreditsBalance)
|
||||
if !valid {
|
||||
antigravityCreditsBalanceByAuth.Delete(strings.TrimSpace(auth.ID))
|
||||
return false
|
||||
}
|
||||
if !bal.Known {
|
||||
return false
|
||||
}
|
||||
available := bal.CreditAmount >= bal.MinCreditAmount
|
||||
cliproxyauth.SetAntigravityCreditsHint(strings.TrimSpace(auth.ID), cliproxyauth.AntigravityCreditsHint{
|
||||
Known: true,
|
||||
Available: available,
|
||||
CreditAmount: bal.CreditAmount,
|
||||
MinCreditAmount: bal.MinCreditAmount,
|
||||
PaidTierID: bal.PaidTierID,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
return available
|
||||
}
|
||||
|
||||
// parseMetaFloat extracts a float64 from auth.Metadata (handles string and numeric types).
|
||||
func parseMetaFloat(metadata map[string]any, key string) (float64, bool) {
|
||||
v, ok := metadata[key]
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
switch typed := v.(type) {
|
||||
case float64:
|
||||
return typed, true
|
||||
case int:
|
||||
return float64(typed), true
|
||||
case int64:
|
||||
return float64(typed), true
|
||||
case uint64:
|
||||
return float64(typed), true
|
||||
case json.Number:
|
||||
if f, err := typed.Float64(); err == nil {
|
||||
return f, true
|
||||
}
|
||||
case string:
|
||||
if f, err := strconv.ParseFloat(strings.TrimSpace(typed), 64); err == nil {
|
||||
return f, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// AntigravityExecutor proxies requests to the antigravity upstream.
|
||||
type AntigravityExecutor struct {
|
||||
cfg *config.Config
|
||||
@@ -189,7 +236,7 @@ func validateAntigravityRequestSignatures(from sdktranslator.Format, rawJSON []b
|
||||
if from.String() != "claude" {
|
||||
return rawJSON, nil
|
||||
}
|
||||
// Always strip thinking blocks with empty signatures (proxy-generated).
|
||||
// Always strip thinking blocks with invalid signatures (empty or non-Claude-format).
|
||||
rawJSON = antigravityclaude.StripEmptySignatureThinkingBlocks(rawJSON)
|
||||
if cache.SignatureCacheEnabled() {
|
||||
return rawJSON, nil
|
||||
@@ -298,6 +345,41 @@ func decideAntigravity429(body []byte) antigravity429Decision {
|
||||
decision.retryAfter = retryAfter
|
||||
}
|
||||
|
||||
status := strings.TrimSpace(gjson.GetBytes(body, "error.status").String())
|
||||
if !strings.EqualFold(status, "RESOURCE_EXHAUSTED") {
|
||||
return decision
|
||||
}
|
||||
|
||||
details := gjson.GetBytes(body, "error.details")
|
||||
if details.Exists() && details.IsArray() {
|
||||
for _, detail := range details.Array() {
|
||||
if detail.Get("@type").String() != "type.googleapis.com/google.rpc.ErrorInfo" {
|
||||
continue
|
||||
}
|
||||
reason := strings.TrimSpace(detail.Get("reason").String())
|
||||
decision.reason = reason
|
||||
switch {
|
||||
case strings.EqualFold(reason, "QUOTA_EXHAUSTED"):
|
||||
decision.kind = antigravity429DecisionFullQuotaExhausted
|
||||
return decision
|
||||
case strings.EqualFold(reason, "RATE_LIMIT_EXCEEDED"):
|
||||
if decision.retryAfter == nil {
|
||||
decision.kind = antigravity429DecisionSoftRetry
|
||||
return decision
|
||||
}
|
||||
switch {
|
||||
case *decision.retryAfter < antigravityInstantRetryThreshold:
|
||||
decision.kind = antigravity429DecisionInstantRetrySameAuth
|
||||
case *decision.retryAfter < antigravityShortQuotaCooldownThreshold:
|
||||
decision.kind = antigravity429DecisionShortCooldownSwitchAuth
|
||||
default:
|
||||
decision.kind = antigravity429DecisionFullQuotaExhausted
|
||||
}
|
||||
return decision
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lowerBody := strings.ToLower(string(body))
|
||||
for _, keyword := range antigravityQuotaExhaustedKeywords {
|
||||
if strings.Contains(lowerBody, keyword) {
|
||||
@@ -307,123 +389,14 @@ func decideAntigravity429(body []byte) antigravity429Decision {
|
||||
}
|
||||
}
|
||||
|
||||
status := strings.TrimSpace(gjson.GetBytes(body, "error.status").String())
|
||||
if !strings.EqualFold(status, "RESOURCE_EXHAUSTED") {
|
||||
return decision
|
||||
}
|
||||
|
||||
details := gjson.GetBytes(body, "error.details")
|
||||
if !details.Exists() || !details.IsArray() {
|
||||
decision.kind = antigravity429DecisionSoftRetry
|
||||
return decision
|
||||
}
|
||||
|
||||
for _, detail := range details.Array() {
|
||||
if detail.Get("@type").String() != "type.googleapis.com/google.rpc.ErrorInfo" {
|
||||
continue
|
||||
}
|
||||
reason := strings.TrimSpace(detail.Get("reason").String())
|
||||
decision.reason = reason
|
||||
switch {
|
||||
case strings.EqualFold(reason, "QUOTA_EXHAUSTED"):
|
||||
decision.kind = antigravity429DecisionFullQuotaExhausted
|
||||
return decision
|
||||
case strings.EqualFold(reason, "RATE_LIMIT_EXCEEDED"):
|
||||
if decision.retryAfter == nil {
|
||||
decision.kind = antigravity429DecisionSoftRetry
|
||||
return decision
|
||||
}
|
||||
switch {
|
||||
case *decision.retryAfter < antigravityInstantRetryThreshold:
|
||||
decision.kind = antigravity429DecisionInstantRetrySameAuth
|
||||
case *decision.retryAfter < antigravityShortQuotaCooldownThreshold:
|
||||
decision.kind = antigravity429DecisionShortCooldownSwitchAuth
|
||||
default:
|
||||
decision.kind = antigravity429DecisionFullQuotaExhausted
|
||||
}
|
||||
return decision
|
||||
}
|
||||
}
|
||||
|
||||
decision.kind = antigravity429DecisionSoftRetry
|
||||
return decision
|
||||
}
|
||||
|
||||
func antigravityHasQuotaResetDelayOrModelInfo(body []byte) bool {
|
||||
if len(body) == 0 {
|
||||
return false
|
||||
}
|
||||
details := gjson.GetBytes(body, "error.details")
|
||||
if !details.Exists() || !details.IsArray() {
|
||||
return false
|
||||
}
|
||||
for _, detail := range details.Array() {
|
||||
if detail.Get("@type").String() != "type.googleapis.com/google.rpc.ErrorInfo" {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(detail.Get("metadata.quotaResetDelay").String()) != "" {
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(detail.Get("metadata.model").String()) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func antigravityCreditsRetryEnabled(cfg *config.Config) bool {
|
||||
return cfg != nil && cfg.QuotaExceeded.AntigravityCredits
|
||||
}
|
||||
|
||||
func antigravityCreditsFailureStateForAuth(auth *cliproxyauth.Auth) (string, antigravityCreditsFailureState, bool) {
|
||||
if auth == nil || strings.TrimSpace(auth.ID) == "" {
|
||||
return "", antigravityCreditsFailureState{}, false
|
||||
}
|
||||
authID := strings.TrimSpace(auth.ID)
|
||||
value, ok := antigravityCreditsFailureByAuth.Load(authID)
|
||||
if !ok {
|
||||
return authID, antigravityCreditsFailureState{}, true
|
||||
}
|
||||
state, ok := value.(antigravityCreditsFailureState)
|
||||
if !ok {
|
||||
antigravityCreditsFailureByAuth.Delete(authID)
|
||||
return authID, antigravityCreditsFailureState{}, true
|
||||
}
|
||||
return authID, state, true
|
||||
}
|
||||
|
||||
func antigravityCreditsDisabled(auth *cliproxyauth.Auth, now time.Time) bool {
|
||||
authID, state, ok := antigravityCreditsFailureStateForAuth(auth)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if state.PermanentlyDisabled {
|
||||
return true
|
||||
}
|
||||
if state.DisabledUntil.IsZero() {
|
||||
return false
|
||||
}
|
||||
if state.DisabledUntil.After(now) {
|
||||
return true
|
||||
}
|
||||
antigravityCreditsFailureByAuth.Delete(authID)
|
||||
return false
|
||||
}
|
||||
|
||||
func recordAntigravityCreditsFailure(auth *cliproxyauth.Auth, now time.Time) {
|
||||
authID, state, ok := antigravityCreditsFailureStateForAuth(auth)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if state.PermanentlyDisabled {
|
||||
antigravityCreditsFailureByAuth.Store(authID, state)
|
||||
return
|
||||
}
|
||||
state.Count++
|
||||
state.DisabledUntil = now.Add(antigravityCreditsAutoDisableDuration)
|
||||
antigravityCreditsFailureByAuth.Store(authID, state)
|
||||
}
|
||||
|
||||
func clearAntigravityCreditsFailureState(auth *cliproxyauth.Auth) {
|
||||
if auth == nil || strings.TrimSpace(auth.ID) == "" {
|
||||
return
|
||||
@@ -440,6 +413,25 @@ func markAntigravityCreditsPermanentlyDisabled(auth *cliproxyauth.Auth) {
|
||||
ExplicitBalanceExhausted: true,
|
||||
}
|
||||
antigravityCreditsFailureByAuth.Store(authID, state)
|
||||
antigravityCreditsBalanceByAuth.Store(authID, antigravityCreditsBalance{
|
||||
CreditAmount: 0,
|
||||
MinCreditAmount: 1,
|
||||
Known: true,
|
||||
})
|
||||
cliproxyauth.SetAntigravityCreditsHint(authID, cliproxyauth.AntigravityCreditsHint{
|
||||
Known: true,
|
||||
Available: false,
|
||||
CreditAmount: 0,
|
||||
MinCreditAmount: 1,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func clearAntigravityCreditsPermanentlyDisabled(auth *cliproxyauth.Auth) {
|
||||
if auth == nil || strings.TrimSpace(auth.ID) == "" {
|
||||
return
|
||||
}
|
||||
antigravityCreditsFailureByAuth.Delete(strings.TrimSpace(auth.ID))
|
||||
}
|
||||
|
||||
func antigravityHasExplicitCreditsBalanceExhaustedReason(body []byte) bool {
|
||||
@@ -462,81 +454,6 @@ func antigravityHasExplicitCreditsBalanceExhaustedReason(body []byte) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func antigravityPreferCreditsKey(auth *cliproxyauth.Auth, modelName string) string {
|
||||
if auth == nil {
|
||||
return ""
|
||||
}
|
||||
authID := strings.TrimSpace(auth.ID)
|
||||
modelName = strings.TrimSpace(modelName)
|
||||
if authID == "" || modelName == "" {
|
||||
return ""
|
||||
}
|
||||
return authID + "|" + modelName
|
||||
}
|
||||
|
||||
func antigravityShouldPreferCredits(auth *cliproxyauth.Auth, modelName string, now time.Time) bool {
|
||||
key := antigravityPreferCreditsKey(auth, modelName)
|
||||
if key == "" {
|
||||
return false
|
||||
}
|
||||
value, ok := antigravityPreferCreditsByModel.Load(key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
until, ok := value.(time.Time)
|
||||
if !ok || until.IsZero() {
|
||||
antigravityPreferCreditsByModel.Delete(key)
|
||||
return false
|
||||
}
|
||||
if !until.After(now) {
|
||||
antigravityPreferCreditsByModel.Delete(key)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func markAntigravityPreferCredits(auth *cliproxyauth.Auth, modelName string, now time.Time, retryAfter *time.Duration) {
|
||||
key := antigravityPreferCreditsKey(auth, modelName)
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
until := now.Add(antigravityCreditsRetryTTL)
|
||||
if retryAfter != nil && *retryAfter > 0 {
|
||||
until = now.Add(*retryAfter)
|
||||
}
|
||||
antigravityPreferCreditsByModel.Store(key, until)
|
||||
}
|
||||
|
||||
func clearAntigravityPreferCredits(auth *cliproxyauth.Auth, modelName string) {
|
||||
key := antigravityPreferCreditsKey(auth, modelName)
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
antigravityPreferCreditsByModel.Delete(key)
|
||||
}
|
||||
|
||||
func shouldMarkAntigravityCreditsExhausted(statusCode int, body []byte, reqErr error) bool {
|
||||
if reqErr != nil || statusCode == 0 {
|
||||
return false
|
||||
}
|
||||
if statusCode >= http.StatusInternalServerError || statusCode == http.StatusRequestTimeout {
|
||||
return false
|
||||
}
|
||||
lowerBody := strings.ToLower(string(body))
|
||||
for _, keyword := range antigravityCreditsExhaustedKeywords {
|
||||
if strings.Contains(lowerBody, keyword) {
|
||||
if keyword == "resource has been exhausted" &&
|
||||
statusCode == http.StatusTooManyRequests &&
|
||||
decideAntigravity429(body).kind == antigravity429DecisionSoftRetry &&
|
||||
!antigravityHasQuotaResetDelayOrModelInfo(body) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func newAntigravityStatusErr(statusCode int, body []byte) statusErr {
|
||||
err := statusErr{code: statusCode, msg: string(body)}
|
||||
if statusCode == http.StatusTooManyRequests {
|
||||
@@ -547,129 +464,6 @@ func newAntigravityStatusErr(statusCode int, body []byte) statusErr {
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *AntigravityExecutor) attemptCreditsFallback(
|
||||
ctx context.Context,
|
||||
auth *cliproxyauth.Auth,
|
||||
httpClient *http.Client,
|
||||
token string,
|
||||
modelName string,
|
||||
payload []byte,
|
||||
stream bool,
|
||||
alt string,
|
||||
baseURL string,
|
||||
originalBody []byte,
|
||||
) (*http.Response, bool) {
|
||||
if !antigravityCreditsRetryEnabled(e.cfg) {
|
||||
return nil, false
|
||||
}
|
||||
if decideAntigravity429(originalBody).kind != antigravity429DecisionFullQuotaExhausted {
|
||||
return nil, false
|
||||
}
|
||||
now := time.Now()
|
||||
if shouldForcePermanentDisableCredits(originalBody) {
|
||||
clearAntigravityPreferCredits(auth, modelName)
|
||||
markAntigravityCreditsPermanentlyDisabled(auth)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if antigravityHasExplicitCreditsBalanceExhaustedReason(originalBody) {
|
||||
clearAntigravityPreferCredits(auth, modelName)
|
||||
markAntigravityCreditsPermanentlyDisabled(auth)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if antigravityCreditsDisabled(auth, now) {
|
||||
return nil, false
|
||||
}
|
||||
creditsPayload := injectEnabledCreditTypes(payload)
|
||||
if len(creditsPayload) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
httpReq, errReq := e.buildRequest(ctx, auth, token, modelName, creditsPayload, stream, alt, baseURL)
|
||||
if errReq != nil {
|
||||
helps.RecordAPIResponseError(ctx, e.cfg, errReq)
|
||||
clearAntigravityPreferCredits(auth, modelName)
|
||||
recordAntigravityCreditsFailure(auth, now)
|
||||
return nil, true
|
||||
}
|
||||
httpResp, errDo := httpClient.Do(httpReq)
|
||||
if errDo != nil {
|
||||
helps.RecordAPIResponseError(ctx, e.cfg, errDo)
|
||||
clearAntigravityPreferCredits(auth, modelName)
|
||||
recordAntigravityCreditsFailure(auth, now)
|
||||
return nil, true
|
||||
}
|
||||
if httpResp.StatusCode >= http.StatusOK && httpResp.StatusCode < http.StatusMultipleChoices {
|
||||
retryAfter, _ := parseRetryDelay(originalBody)
|
||||
markAntigravityPreferCredits(auth, modelName, now, retryAfter)
|
||||
clearAntigravityCreditsFailureState(auth)
|
||||
return httpResp, true
|
||||
}
|
||||
|
||||
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||
bodyBytes, errRead := io.ReadAll(httpResp.Body)
|
||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||
log.Errorf("antigravity executor: close credits fallback response body error: %v", errClose)
|
||||
}
|
||||
if errRead != nil {
|
||||
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||
clearAntigravityPreferCredits(auth, modelName)
|
||||
recordAntigravityCreditsFailure(auth, now)
|
||||
return nil, true
|
||||
}
|
||||
helps.AppendAPIResponseChunk(ctx, e.cfg, bodyBytes)
|
||||
if shouldForcePermanentDisableCredits(bodyBytes) {
|
||||
clearAntigravityPreferCredits(auth, modelName)
|
||||
markAntigravityCreditsPermanentlyDisabled(auth)
|
||||
return nil, true
|
||||
}
|
||||
|
||||
if antigravityHasExplicitCreditsBalanceExhaustedReason(bodyBytes) {
|
||||
clearAntigravityPreferCredits(auth, modelName)
|
||||
markAntigravityCreditsPermanentlyDisabled(auth)
|
||||
return nil, true
|
||||
}
|
||||
|
||||
clearAntigravityPreferCredits(auth, modelName)
|
||||
recordAntigravityCreditsFailure(auth, now)
|
||||
return nil, true
|
||||
}
|
||||
|
||||
func (e *AntigravityExecutor) handleDirectCreditsFailure(ctx context.Context, auth *cliproxyauth.Auth, modelName string, reqErr error) {
|
||||
if reqErr != nil {
|
||||
if shouldForcePermanentDisableCredits(reqErrBody(reqErr)) {
|
||||
clearAntigravityPreferCredits(auth, modelName)
|
||||
markAntigravityCreditsPermanentlyDisabled(auth)
|
||||
return
|
||||
}
|
||||
|
||||
if antigravityHasExplicitCreditsBalanceExhaustedReason(reqErrBody(reqErr)) {
|
||||
clearAntigravityPreferCredits(auth, modelName)
|
||||
markAntigravityCreditsPermanentlyDisabled(auth)
|
||||
return
|
||||
}
|
||||
|
||||
helps.RecordAPIResponseError(ctx, e.cfg, reqErr)
|
||||
}
|
||||
clearAntigravityPreferCredits(auth, modelName)
|
||||
recordAntigravityCreditsFailure(auth, time.Now())
|
||||
}
|
||||
func reqErrBody(reqErr error) []byte {
|
||||
if reqErr == nil {
|
||||
return nil
|
||||
}
|
||||
msg := reqErr.Error()
|
||||
if strings.TrimSpace(msg) == "" {
|
||||
return nil
|
||||
}
|
||||
return []byte(msg)
|
||||
}
|
||||
|
||||
func shouldForcePermanentDisableCredits(body []byte) bool {
|
||||
return antigravityHasExplicitCreditsBalanceExhaustedReason(body)
|
||||
}
|
||||
|
||||
// Execute performs a non-streaming request to the Antigravity API.
|
||||
func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
||||
if opts.Alt == "responses/compact" {
|
||||
@@ -721,6 +515,8 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au
|
||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
|
||||
|
||||
useCredits := cliproxyauth.AntigravityCreditsRequested(ctx) && antigravityCreditsRetryEnabled(e.cfg)
|
||||
|
||||
baseURLs := antigravityBaseURLFallbackOrder(auth)
|
||||
httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
|
||||
attempts := antigravityRetryAttempts(auth, e.cfg)
|
||||
@@ -733,11 +529,10 @@ attemptLoop:
|
||||
|
||||
for idx, baseURL := range baseURLs {
|
||||
requestPayload := translated
|
||||
usedCreditsDirect := false
|
||||
if antigravityCreditsRetryEnabled(e.cfg) && antigravityShouldPreferCredits(auth, baseModel, time.Now()) {
|
||||
if creditsPayload := injectEnabledCreditTypes(translated); len(creditsPayload) > 0 {
|
||||
requestPayload = creditsPayload
|
||||
usedCreditsDirect = true
|
||||
if useCredits {
|
||||
if cp := injectEnabledCreditTypes(translated); len(cp) > 0 {
|
||||
requestPayload = cp
|
||||
helps.MarkCreditsUsed(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -785,7 +580,6 @@ attemptLoop:
|
||||
wait := antigravityInstantRetryDelay(*decision.retryAfter)
|
||||
log.Debugf("antigravity executor: instant retry for model %s, waiting %s", baseModel, wait)
|
||||
if errWait := antigravityWait(ctx, wait); errWait != nil {
|
||||
|
||||
return resp, errWait
|
||||
}
|
||||
}
|
||||
@@ -794,34 +588,13 @@ attemptLoop:
|
||||
case antigravity429DecisionShortCooldownSwitchAuth:
|
||||
if decision.retryAfter != nil && *decision.retryAfter > 0 {
|
||||
markAntigravityShortCooldown(auth, baseModel, time.Now(), *decision.retryAfter)
|
||||
log.Debugf("antigravity executor: short quota cooldown (%s) for model %s, recorded cooldown and skipping credits fallback", *decision.retryAfter, baseModel)
|
||||
log.Debugf("antigravity executor: short quota cooldown (%s) for model %s, recorded cooldown", *decision.retryAfter, baseModel)
|
||||
}
|
||||
case antigravity429DecisionFullQuotaExhausted:
|
||||
if usedCreditsDirect {
|
||||
clearAntigravityPreferCredits(auth, baseModel)
|
||||
recordAntigravityCreditsFailure(auth, time.Now())
|
||||
} else {
|
||||
creditsResp, _ := e.attemptCreditsFallback(ctx, auth, httpClient, token, baseModel, translated, false, opts.Alt, baseURL, bodyBytes)
|
||||
if creditsResp != nil {
|
||||
helps.RecordAPIResponseMetadata(ctx, e.cfg, creditsResp.StatusCode, creditsResp.Header.Clone())
|
||||
creditsBody, errCreditsRead := io.ReadAll(creditsResp.Body)
|
||||
if errClose := creditsResp.Body.Close(); errClose != nil {
|
||||
log.Errorf("antigravity executor: close credits success response body error: %v", errClose)
|
||||
}
|
||||
if errCreditsRead != nil {
|
||||
helps.RecordAPIResponseError(ctx, e.cfg, errCreditsRead)
|
||||
err = errCreditsRead
|
||||
return resp, err
|
||||
}
|
||||
helps.AppendAPIResponseChunk(ctx, e.cfg, creditsBody)
|
||||
reporter.Publish(ctx, helps.ParseAntigravityUsage(creditsBody))
|
||||
var param any
|
||||
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, creditsBody, ¶m)
|
||||
resp = cliproxyexecutor.Response{Payload: converted, Headers: creditsResp.Header.Clone()}
|
||||
reporter.EnsurePublished(ctx)
|
||||
return resp, nil
|
||||
}
|
||||
if useCredits && antigravityHasExplicitCreditsBalanceExhaustedReason(bodyBytes) {
|
||||
markAntigravityCreditsPermanentlyDisabled(auth)
|
||||
}
|
||||
// No credits logic - just fall through to error return below
|
||||
}
|
||||
}
|
||||
|
||||
@@ -870,6 +643,10 @@ attemptLoop:
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Success
|
||||
if useCredits {
|
||||
clearAntigravityCreditsFailureState(auth)
|
||||
}
|
||||
reporter.Publish(ctx, helps.ParseAntigravityUsage(bodyBytes))
|
||||
var param any
|
||||
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bodyBytes, ¶m)
|
||||
@@ -935,6 +712,8 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth *
|
||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
|
||||
|
||||
useCredits := cliproxyauth.AntigravityCreditsRequested(ctx) && antigravityCreditsRetryEnabled(e.cfg)
|
||||
|
||||
baseURLs := antigravityBaseURLFallbackOrder(auth)
|
||||
httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
|
||||
|
||||
@@ -948,11 +727,10 @@ attemptLoop:
|
||||
|
||||
for idx, baseURL := range baseURLs {
|
||||
requestPayload := translated
|
||||
usedCreditsDirect := false
|
||||
if antigravityCreditsRetryEnabled(e.cfg) && antigravityShouldPreferCredits(auth, baseModel, time.Now()) {
|
||||
if creditsPayload := injectEnabledCreditTypes(translated); len(creditsPayload) > 0 {
|
||||
requestPayload = creditsPayload
|
||||
usedCreditsDirect = true
|
||||
if useCredits {
|
||||
if cp := injectEnabledCreditTypes(translated); len(cp) > 0 {
|
||||
requestPayload = cp
|
||||
helps.MarkCreditsUsed(ctx)
|
||||
}
|
||||
}
|
||||
httpReq, errReq := e.buildRequest(ctx, auth, token, baseModel, requestPayload, true, opts.Alt, baseURL)
|
||||
@@ -1014,7 +792,6 @@ attemptLoop:
|
||||
wait := antigravityInstantRetryDelay(*decision.retryAfter)
|
||||
log.Debugf("antigravity executor: instant retry for model %s, waiting %s", baseModel, wait)
|
||||
if errWait := antigravityWait(ctx, wait); errWait != nil {
|
||||
|
||||
return resp, errWait
|
||||
}
|
||||
}
|
||||
@@ -1023,25 +800,16 @@ attemptLoop:
|
||||
case antigravity429DecisionShortCooldownSwitchAuth:
|
||||
if decision.retryAfter != nil && *decision.retryAfter > 0 {
|
||||
markAntigravityShortCooldown(auth, baseModel, time.Now(), *decision.retryAfter)
|
||||
log.Debugf("antigravity executor: short quota cooldown (%s) for model %s, recorded cooldown and skipping credits fallback", *decision.retryAfter, baseModel)
|
||||
log.Debugf("antigravity executor: short quota cooldown (%s) for model %s, recorded cooldown", *decision.retryAfter, baseModel)
|
||||
}
|
||||
case antigravity429DecisionFullQuotaExhausted:
|
||||
if usedCreditsDirect {
|
||||
clearAntigravityPreferCredits(auth, baseModel)
|
||||
recordAntigravityCreditsFailure(auth, time.Now())
|
||||
} else {
|
||||
creditsResp, _ := e.attemptCreditsFallback(ctx, auth, httpClient, token, baseModel, translated, true, opts.Alt, baseURL, bodyBytes)
|
||||
if creditsResp != nil {
|
||||
httpResp = creditsResp
|
||||
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||
}
|
||||
if useCredits && antigravityHasExplicitCreditsBalanceExhaustedReason(bodyBytes) {
|
||||
markAntigravityCreditsPermanentlyDisabled(auth)
|
||||
}
|
||||
// No credits logic - just fall through to error return below
|
||||
}
|
||||
}
|
||||
|
||||
if httpResp.StatusCode >= http.StatusOK && httpResp.StatusCode < http.StatusMultipleChoices {
|
||||
goto streamSuccessClaudeNonStream
|
||||
}
|
||||
lastStatus = httpResp.StatusCode
|
||||
lastBody = append([]byte(nil), bodyBytes...)
|
||||
lastErr = nil
|
||||
@@ -1085,7 +853,10 @@ attemptLoop:
|
||||
return resp, err
|
||||
}
|
||||
|
||||
streamSuccessClaudeNonStream:
|
||||
// Stream success
|
||||
if useCredits {
|
||||
clearAntigravityCreditsFailureState(auth)
|
||||
}
|
||||
out := make(chan cliproxyexecutor.StreamChunk)
|
||||
go func(resp *http.Response) {
|
||||
defer close(out)
|
||||
@@ -1389,6 +1160,7 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya
|
||||
if updatedAuth != nil {
|
||||
auth = updatedAuth
|
||||
}
|
||||
|
||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
|
||||
|
||||
@@ -1400,6 +1172,8 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya
|
||||
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
|
||||
|
||||
useCredits := cliproxyauth.AntigravityCreditsRequested(ctx) && antigravityCreditsRetryEnabled(e.cfg)
|
||||
|
||||
baseURLs := antigravityBaseURLFallbackOrder(auth)
|
||||
httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
|
||||
|
||||
@@ -1413,11 +1187,10 @@ attemptLoop:
|
||||
|
||||
for idx, baseURL := range baseURLs {
|
||||
requestPayload := translated
|
||||
usedCreditsDirect := false
|
||||
if antigravityCreditsRetryEnabled(e.cfg) && antigravityShouldPreferCredits(auth, baseModel, time.Now()) {
|
||||
if creditsPayload := injectEnabledCreditTypes(translated); len(creditsPayload) > 0 {
|
||||
requestPayload = creditsPayload
|
||||
usedCreditsDirect = true
|
||||
if useCredits {
|
||||
if cp := injectEnabledCreditTypes(translated); len(cp) > 0 {
|
||||
requestPayload = cp
|
||||
helps.MarkCreditsUsed(ctx)
|
||||
}
|
||||
}
|
||||
httpReq, errReq := e.buildRequest(ctx, auth, token, baseModel, requestPayload, true, opts.Alt, baseURL)
|
||||
@@ -1478,7 +1251,6 @@ attemptLoop:
|
||||
wait := antigravityInstantRetryDelay(*decision.retryAfter)
|
||||
log.Debugf("antigravity executor: instant retry for model %s, waiting %s", baseModel, wait)
|
||||
if errWait := antigravityWait(ctx, wait); errWait != nil {
|
||||
|
||||
return nil, errWait
|
||||
}
|
||||
}
|
||||
@@ -1487,25 +1259,16 @@ attemptLoop:
|
||||
case antigravity429DecisionShortCooldownSwitchAuth:
|
||||
if decision.retryAfter != nil && *decision.retryAfter > 0 {
|
||||
markAntigravityShortCooldown(auth, baseModel, time.Now(), *decision.retryAfter)
|
||||
log.Debugf("antigravity executor: short quota cooldown (%s) for model %s, recorded cooldown and skipping credits fallback", *decision.retryAfter, baseModel)
|
||||
log.Debugf("antigravity executor: short quota cooldown (%s) for model %s recorded", *decision.retryAfter, baseModel)
|
||||
}
|
||||
case antigravity429DecisionFullQuotaExhausted:
|
||||
if usedCreditsDirect {
|
||||
clearAntigravityPreferCredits(auth, baseModel)
|
||||
recordAntigravityCreditsFailure(auth, time.Now())
|
||||
} else {
|
||||
creditsResp, _ := e.attemptCreditsFallback(ctx, auth, httpClient, token, baseModel, translated, true, opts.Alt, baseURL, bodyBytes)
|
||||
if creditsResp != nil {
|
||||
httpResp = creditsResp
|
||||
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||
}
|
||||
if useCredits && antigravityHasExplicitCreditsBalanceExhaustedReason(bodyBytes) {
|
||||
markAntigravityCreditsPermanentlyDisabled(auth)
|
||||
}
|
||||
// No credits logic - just fall through to error return below
|
||||
}
|
||||
}
|
||||
|
||||
if httpResp.StatusCode >= http.StatusOK && httpResp.StatusCode < http.StatusMultipleChoices {
|
||||
goto streamSuccessExecuteStream
|
||||
}
|
||||
lastStatus = httpResp.StatusCode
|
||||
lastBody = append([]byte(nil), bodyBytes...)
|
||||
lastErr = nil
|
||||
@@ -1549,7 +1312,10 @@ attemptLoop:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
streamSuccessExecuteStream:
|
||||
// Stream success
|
||||
if useCredits {
|
||||
clearAntigravityCreditsFailureState(auth)
|
||||
}
|
||||
out := make(chan cliproxyexecutor.StreamChunk)
|
||||
go func(resp *http.Response) {
|
||||
defer close(out)
|
||||
@@ -1792,6 +1558,9 @@ func (e *AntigravityExecutor) ensureAccessToken(ctx context.Context, auth *clipr
|
||||
accessToken := metaStringValue(auth.Metadata, "access_token")
|
||||
expiry := tokenExpiry(auth.Metadata)
|
||||
if accessToken != "" && expiry.After(time.Now().Add(refreshSkew)) {
|
||||
if !cliproxyauth.HasKnownAntigravityCreditsHint(auth.ID) {
|
||||
e.updateAntigravityCreditsBalance(ctx, auth, accessToken)
|
||||
}
|
||||
return accessToken, nil, nil
|
||||
}
|
||||
refreshCtx := context.Background()
|
||||
@@ -1882,6 +1651,7 @@ func (e *AntigravityExecutor) refreshToken(ctx context.Context, auth *cliproxyau
|
||||
if errProject := e.ensureAntigravityProjectID(ctx, auth, tokenResp.AccessToken); errProject != nil {
|
||||
log.Warnf("antigravity executor: ensure project id failed: %v", errProject)
|
||||
}
|
||||
e.updateAntigravityCreditsBalance(ctx, auth, tokenResp.AccessToken)
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
@@ -1918,6 +1688,94 @@ func (e *AntigravityExecutor) ensureAntigravityProjectID(ctx context.Context, au
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *AntigravityExecutor) updateAntigravityCreditsBalance(ctx context.Context, auth *cliproxyauth.Auth, accessToken string) {
|
||||
if auth == nil || strings.TrimSpace(auth.ID) == "" {
|
||||
return
|
||||
}
|
||||
token := strings.TrimSpace(accessToken)
|
||||
if token == "" {
|
||||
token = metaStringValue(auth.Metadata, "access_token")
|
||||
}
|
||||
if token == "" {
|
||||
return
|
||||
}
|
||||
|
||||
loadReqBody := `{"metadata":{"ideType":"ANTIGRAVITY","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}}`
|
||||
endpointURL := "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist"
|
||||
httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodPost, endpointURL, strings.NewReader(loadReqBody))
|
||||
if errReq != nil {
|
||||
log.Debugf("antigravity executor: create loadCodeAssist request error: %v", errReq)
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("User-Agent", "google-api-nodejs-client/9.15.1")
|
||||
|
||||
httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
|
||||
httpResp, errDo := httpClient.Do(httpReq)
|
||||
if errDo != nil {
|
||||
log.Debugf("antigravity executor: loadCodeAssist request error: %v", errDo)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||
log.Errorf("antigravity executor: close loadCodeAssist response body error: %v", errClose)
|
||||
}
|
||||
}()
|
||||
|
||||
bodyBytes, errRead := io.ReadAll(httpResp.Body)
|
||||
if errRead != nil || httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices {
|
||||
log.Debugf("antigravity executor: loadCodeAssist returned status %d, err=%v", httpResp.StatusCode, errRead)
|
||||
return
|
||||
}
|
||||
|
||||
authID := strings.TrimSpace(auth.ID)
|
||||
paidTierID := strings.TrimSpace(gjson.GetBytes(bodyBytes, "paidTier.id").String())
|
||||
|
||||
credits := gjson.GetBytes(bodyBytes, "paidTier.availableCredits")
|
||||
if !credits.IsArray() {
|
||||
cliproxyauth.SetAntigravityCreditsHint(authID, cliproxyauth.AntigravityCreditsHint{
|
||||
Known: true,
|
||||
Available: false,
|
||||
PaidTierID: paidTierID,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
return
|
||||
}
|
||||
for _, credit := range credits.Array() {
|
||||
if !strings.EqualFold(credit.Get("creditType").String(), "GOOGLE_ONE_AI") {
|
||||
continue
|
||||
}
|
||||
creditAmount, errCA := strconv.ParseFloat(strings.TrimSpace(credit.Get("creditAmount").String()), 64)
|
||||
if errCA != nil {
|
||||
continue
|
||||
}
|
||||
minAmount, errMA := strconv.ParseFloat(strings.TrimSpace(credit.Get("minimumCreditAmountForUsage").String()), 64)
|
||||
if errMA != nil {
|
||||
continue
|
||||
}
|
||||
bal := antigravityCreditsBalance{
|
||||
CreditAmount: creditAmount,
|
||||
MinCreditAmount: minAmount,
|
||||
PaidTierID: paidTierID,
|
||||
Known: true,
|
||||
}
|
||||
antigravityCreditsBalanceByAuth.Store(authID, bal)
|
||||
cliproxyauth.SetAntigravityCreditsHint(authID, cliproxyauth.AntigravityCreditsHint{
|
||||
Known: true,
|
||||
Available: creditAmount >= minAmount,
|
||||
CreditAmount: creditAmount,
|
||||
MinCreditAmount: minAmount,
|
||||
PaidTierID: paidTierID,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
if creditAmount >= minAmount {
|
||||
clearAntigravityCreditsPermanentlyDisabled(auth)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyauth.Auth, token, modelName string, payload []byte, stream bool, alt, baseURL string) (*http.Request, error) {
|
||||
if token == "" {
|
||||
return nil, statusErr{code: http.StatusUnauthorized, msg: "missing access token"}
|
||||
|
||||
Reference in New Issue
Block a user