rgoogle-gemini

ellmer vignette code for chat_google_gemini() returns non-specific error / Google API working


I'm trying to use the ellmer package to access Google Gemini with the chat_google_gemini functions. I use the following code, with GOOGLE_API_KEY saved in my .Renviron file.

library(ellmer)
# 1. Verify the API key exists
my_api_key <- Sys.getenv("GOOGLE_API_KEY")
if (my_api_key == "") {
  stop("GOOGLE_API_KEY environment variable not set or found.")
}

# 2. Create the chat object, specifying a model
# Models like "gemini-1.5-flash-latest" are cost-effective and fast.
chat <- chat_google_gemini(
  model = "gemini-2.5-flash-lite",
  system_prompt = "You are a friendly but terse assistant.",
  api_key = my_api_key
)

### One option that does not work
live_console(chat)

### Another option that does not work
chat$chat("Tell me three jokes about statisticians")

I receive the following error:

Error in `req_perform_connection()`:
! Failed to parse error body with method defined in `req_error()`.
Caused by error in `resp_body_raw()`:
! Can't retrieve empty body.
Run `rlang::last_trace()` to see where the error occurred.

Asking Gemini about the issue, I receive the following explanation about the error: "usually indicates that your request reached Google's servers, but something prevented the API from processing it and returning a standard error message."

I then spend a lot of time testing the Google API to see if it works. With the help of the Google Cloud Assistant, I was able to create the following testing function:

library(httr)
library(jsonlite)

test_google_ai_studio_api_with_params <- function(api_key, model_name) {
  if (missing(api_key) || !is.character(api_key) || nchar(api_key) == 0) {
    stop("Error: API Key is missing or invalid. Please provide your Google AI Studio API Key.")
  }
  
  url <- paste0("https://generativelanguage.googleapis.com/v1beta/", model_name,
                ":generateContent?key=", api_key)
  
  headers <- c("Content-Type" = "application/json")
  
  body <- list(
    contents = list(
      list(
        parts = list(
          list(text = "Tell me a short, simple, happy story.") # Use a clear, positive prompt
        )
      )
    )
  )
  
  cat(paste0("Attempting to call the API for model: ", model_name, " with explicit parameters...\n"))
  
  response <- tryCatch({
    POST(url, add_headers(headers), body = toJSON(body, auto_unbox = TRUE))
  }, error = function(e) {
    stop(paste("Network or HTTP request error:", e$message))
  })
  
  status_code <- status_code(response)
  cat(paste0("HTTP Status Code: ", status_code, "\n"))
  
  if (status_code == 200) {
    content <- content(response, "text", encoding = "UTF-8")
    parsed_content <- fromJSON(content)
    
    # Check if 'candidates' exists and has content
    if (!is.null(parsed_content$candidates) && length(parsed_content$candidates) > 0) {
      first_candidate <- parsed_content$candidates[[1]]
      if (!is.null(first_candidate$parts) && !is.null(first_candidate$parts) && length(first_candidate$parts) > 0) {
        generated_text <- first_candidate$parts[[1]]$text
        if (nchar(generated_text) > 0) {
          cat("API Call Successful! Generated Text:\n")
          cat(generated_text, "\n")
          return(TRUE)
        } else {
          cat("API Call Successful, but generated text is empty despite explicit parameters. Response:\n")
          print(parsed_content) # Print full response to see if other fields exist
          return(FALSE)
        }
      } else {
        cat("API Call Successful, but 'content' or 'parts' is missing in candidate. Response:\n")
        print(parsed_content)
        return(FALSE)
      }
    } else {
      cat("API Call Successful, but no candidates returned. Response:\n")
      print(parsed_content)
      return(FALSE)
    }
  } else {
    error_content <- content(response, "text", encoding = "UTF-8")
    parsed_error <- tryCatch(fromJSON(error_content), error = function(e) list(error = list(message = error_content)))
    
    cat("API Call Failed with HTTP Status Code: ", status_code, "\n")
    if (!is.null(parsed_error$error) && !is.null(parsed_error$error$message)) {
      cat("Error Message from API: ", parsed_error$error$message, "\n")
    } else {
      cat("Raw Error Response:\n")
      print(error_content)
    }
    return(FALSE) # Simplified error messages for brevity, use previous detailed ones if needed
  }
}

# --- How to use it ---
my_api_key <- Sys.getenv("GOOGLE_API_KEY")
# Use the model that showed up in your ListModels output and you previously tested
# For example, "models/gemini-1.5-flash-latest" or "models/gemini-2.5-flash-lite"
model_to_test <- "models/gemini-2.5-flash-lite" 

api_test_success_with_params <- test_google_ai_studio_api_with_params(
  api_key = my_api_key,
  model_name = model_to_test
)

This works without issues and I get the following response:

Attempting to call the API for model: models/gemini-2.5-flash-lite with explicit parameters...
HTTP Status Code: 200
API Call Successful! Generated Text:
Lily the ladybug woke up with the sun. A tiny dewdrop sparkled on her back, catching the light like a miniature diamond. She wiggled her antennae, feeling a tickle of excitement. Today was the day she'd explore the big, green leaf just across the path.

With a happy buzz, she crawled onto a blade of grass, then another, a little adventurer on a grand journey. The world seemed vast and full of wonder. A bright yellow butterfly fluttered by, its wings a blur of sunshine, and Lily felt a smile spread across her tiny face.

When she finally reached the big, green leaf, it was even more amazing than she'd imagined. There were smooth, cool surfaces, and the air smelled sweet and fresh. She found a plump, juicy aphid and enjoyed a delicious breakfast. As the sun warmed her shell, Lily stretched out her wings, ready for more adventures, feeling perfectly content and happy. 

Is my code to access Google Gemini via chat_google_gemini wrong or is this maybe an issue that ellmer uses broken syntax to access Google Gemini?

sessionInfo()
R version 4.4.1 (2024-06-14 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 26100)

Matrix products: default


locale:
[1] LC_COLLATE=English_Switzerland.utf8  LC_CTYPE=English_Switzerland.utf8    LC_MONETARY=English_Switzerland.utf8 LC_NUMERIC=C                        
[5] LC_TIME=English_Switzerland.utf8    

time zone: Europe/Zurich
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] jsonlite_1.8.9 httr_1.4.7     ellmer_0.2.1  

loaded via a namespace (and not attached):
 [1] coro_1.1.0        utf8_1.2.4        R6_2.5.1          magrittr_2.0.3    rappdirs_0.3.3    glue_1.8.0        lifecycle_1.0.4   cli_3.6.3         S7_0.2.0          fansi_1.0.6      
[11] vctrs_0.6.5       withr_3.0.1       pkgload_1.4.0     compiler_4.4.1    rstudioapi_0.17.0 tools_4.4.1       curl_6.4.0        pillar_1.9.0      httr2_1.2.0       crayon_1.5.3     
[21] rlang_1.1.4  

Update: additional information based on @margusl's comment

httr2::with_verbosity(chat$chat("Tell me three jokes about statisticians"), verbosity = 2)
-> POST /v1beta/models/gemini-2.0-flash%3AstreamGenerateContent?alt=sse HTTP/2
-> Host: generativelanguage.googleapis.com
-> User-Agent: r-ellmer/0.2.1
-> Accept: */*
-> Accept-Encoding: deflate, gzip
-> x-goog-api-key: <REDACTED>
-> Content-Type: application/json
-> Content-Length: 172
-> 
>> {
>>   "contents": [
>>     {
>>       "role": "user",
>>       "parts": [
>>         {
>>           "text": "Tell me three jokes about statisticians"
>>         }
>>       ]
>>     }
>>   ],
>>   "systemInstruction": {
>>     "parts": {
>>       "text": "You are a friendly but terse assistant."
>>     }
>>   }
>> }
<- HTTP/2 404 
<- content-type: text/html
<- date: Wed, 23 Jul 2025 15:48:35 GMT
<- server: scaffolding on HTTPServer2
<- content-length: 0
<- x-xss-protection: 0
<- x-frame-options: SAMEORIGIN
<- x-content-type-options: nosniff
<- server-timing: gfet4t7; dur=14
<- alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
<- 
Error in `req_perform_connection()`:
! Failed to parse error body with method defined in `req_error()`.
Caused by error in `resp_body_raw()`:
! Can't retrieve empty body.
Run `rlang::last_trace()` to see where the error occurred.

Solution

  • This is related to a change in httr2-1.2.0 which could lead to colon escaping in request URLs, was fixed in 1.2.1 - https://github.com/r-lib/httr2/issues/780 .
    Also related - https://github.com/tidyverse/ellmer/issues?q=httr2%201.2.0

    So make sure you have updated to at least httr2-1.2.1, CRAN publish date 2025-07-22.


    Testing with httr2@1.2.0 and recovering last httr2 response with httr2::last_response() (it also includes request, if you need to check its exact details), note the 404 Not Found response status and %3A instead of a : in model endpoint ( gemini-2.0-flash%3AstreamGenerateContent ):

    pak::pak("httr2@1.2.0", ask = FALSE) 
    pak::pkg_status("httr2")$version 
    #> [1] "1.2.0"
    
    ellmer::chat_google_gemini()$chat("Tell me three jokes about statisticians") 
    #> Using model = "gemini-2.0-flash".
    #> Error in `req_perform_connection()`:
    #> ! Failed to parse error body with method defined in `req_error()`.
    #> Caused by error in `resp_body_raw()`:
    #> ! Can't retrieve empty body.
    
    httr2::last_response()
    #> <httr2_response>
    #> POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash%3AstreamGenerateContent?alt=sse
    #> Status: 404 Not Found
    #> Content-Type: text/html
    #> Body: None
    

    Switching to httr2@1.2.1 (in a new R session) :

    pak::pak("httr2@1.2.1", ask = FALSE) 
    pak::pkg_status("httr2")$version 
    #> [1] "1.2.1"
    
    ellmer::chat_google_gemini()$chat("Tell me three jokes about statisticians") 
    #> Using model = "gemini-2.0-flash".
    #> Alright, here are three jokes about statisticians:
    #> 
    #> 1.  **Why did the statistician break up with the data scientist?**
    #>     Because he said she was always trying to normalize their relationship.
    #> 
    #> 2.  **A statistician is someone who is good with numbers but lacks the 
    #> personality to be an accountant.**
    #> 
    #> 3.  **A statistician may be able to live on just one piece of bread.**
    #>     ... But only if it is the mean.
    
    httr2::last_response()
    #> <httr2_response>
    #> POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent?alt=sse
    #> Status: 200 OK
    #> Content-Type: text/event-stream
    #> Body: None
    

    To replicate, make sure GOOGLE_API_KEY environment variable includes valid Gemini key.