diff --git a/internal/runtime/executor/gemini_cli_executor.go b/internal/runtime/executor/gemini_cli_executor.go index a2e0ecec..85448f41 100644 --- a/internal/runtime/executor/gemini_cli_executor.go +++ b/internal/runtime/executor/gemini_cli_executor.go @@ -8,6 +8,8 @@ import ( "fmt" "io" "net/http" + "regexp" + "strconv" "strings" "time" @@ -770,20 +772,45 @@ func parseRetryDelay(errorBody []byte) (*time.Duration, error) { // Try to parse the retryDelay from the error response // Format: error.details[].retryDelay where @type == "type.googleapis.com/google.rpc.RetryInfo" details := gjson.GetBytes(errorBody, "error.details") - if !details.Exists() || !details.IsArray() { - return nil, fmt.Errorf("no error.details found") + if details.Exists() && details.IsArray() { + for _, detail := range details.Array() { + typeVal := detail.Get("@type").String() + if typeVal == "type.googleapis.com/google.rpc.RetryInfo" { + retryDelay := detail.Get("retryDelay").String() + if retryDelay != "" { + // Parse duration string like "0.847655010s" + duration, err := time.ParseDuration(retryDelay) + if err != nil { + return nil, fmt.Errorf("failed to parse duration") + } + return &duration, nil + } + } + } + + // Fallback: try ErrorInfo.metadata.quotaResetDelay (e.g., "373.801628ms") + for _, detail := range details.Array() { + typeVal := detail.Get("@type").String() + if typeVal == "type.googleapis.com/google.rpc.ErrorInfo" { + quotaResetDelay := detail.Get("metadata.quotaResetDelay").String() + if quotaResetDelay != "" { + duration, err := time.ParseDuration(quotaResetDelay) + if err == nil { + return &duration, nil + } + } + } + } } - for _, detail := range details.Array() { - typeVal := detail.Get("@type").String() - if typeVal == "type.googleapis.com/google.rpc.RetryInfo" { - retryDelay := detail.Get("retryDelay").String() - if retryDelay != "" { - // Parse duration string like "0.847655010s" - duration, err := time.ParseDuration(retryDelay) - if err != nil { - return nil, fmt.Errorf("failed to parse duration") - } + // Fallback: parse from error.message "Your quota will reset after Xs." + message := gjson.GetBytes(errorBody, "error.message").String() + if message != "" { + re := regexp.MustCompile(`after\s+(\d+)s\.?`) + if matches := re.FindStringSubmatch(message); len(matches) > 1 { + seconds, err := strconv.Atoi(matches[1]) + if err == nil { + duration := time.Duration(seconds) * time.Second return &duration, nil } }