Sys.getenv("OPENAI_API_KEY")
I will demonstrate using two methods via: Open AI and Ollama. You can choose to code along with one or both, or none!
install.packages(c('usethis', 'ellmer'))1 Using LLM with ellmer
The ellmer R package provides a user-friendly interface to large language models.
The ellmer R package is in very active development. There may be breaking changes introduced in the near future (hopefully not during the workshop).
For this workshop we are using version 0.2.0 of ellmer.
1.1 LLM with Open AI
First to use Open AI, you will need to get the Open AI API key. Register an account with Open AI and go here to generate a new API key and add some credit.
For workshop participants, you would have received a API key/token in your email. You can use this during the workshop, but please note it will stop working after the workshop so you will need to register your own account and API key as above afterwards.
You will need to store the API key as OPENAI_API_KEY in your .Renviron file.
- Find the
.Renvironfile. The easiest way to find.Renvironfile is to use
usethis::edit_r_environ()This should open the .Renviron file.
- Enter your API key in a new line in
.Renvironlike below.
.Renviron
OPENAI_API_KEY=<YOUR API KEY>- Restart your R session so the above environmental variable takes effect.
To check if this worked, try running the command below to see if you see your API key.
library(ellmer)Once you have set up the API key, you can use the ellmer package to interact with the AI provider’s API.
chat <- chat_openai(model = "gpt-4o-mini")
chat$chat("Tell me a statistics joke!")Why did the statistician bring a ladder to the bar?
Because they heard the drinks were on the house!
1.2 Local LLM with Ollama
A local LLM via Ollama:
- require no payment for usage,
- require no internet access after you download/pull the LLM, and
- does not transfer your data to a provider.
You do require sufficient RAM to use these models locally (at least 16GB for 13b models), so it is preferable to use a more powerful computer.
Download Ollama at https://ollama.com/download.
Download at least one local LLM. Please note that these local LLMs are large (several GB) and may take some time (>10 minutes) to download. I will mainly use llama3.1:8b which can be downloaded/pulled by using the command below in the terminal (not the R console):
ollama pull llama3.1:8bIf you do not have a large RAM (<8GB), you may try a smaller model such as llama3.2:1b (but its output may not be as accurate):
ollama pull llama3.2:1bTo check that Ollama is installed properly, open a terminal and check the version of Ollama
ollama --versionchat <- chat_ollama(model = "llama3.1:8b")
chat$chat("Tell me a statistics joke!")Here's one:
Why was the math book looking so sad?
Because it had too many problems.
(Sorry, I know it's a bit of a statistically improbable pun...)
1.3 Other AI provider’s API
ellmer, as of version 0.2.0, supports the AI providers in Table 1. To use the API, you need to sign up for an account with the provider and obtain an API key or some other authentication or connection method. You may need to pay for some small credit to use it. The API key can be set in a similar manner to Open AI API key (described in Section 1.1), but using the corresponding environmental variable name.
| Provider | Name | Link | Environmental Variable |
|---|---|---|---|
| Anthropic | chat_anthropic() |
https://www.anthropic.com/claude | ANTHROPIC_API_KEY |
| Amazon | chat_aws_bedrock() |
https://aws.amazon.com/bedrock/ | |
| Azure OpenAI | chat_azure_openai() |
https://azure.microsoft.com/en-us/products/ai-services/openai-service | AZURE_OPENAI_API_KEY |
| CloudFlare | chat_cloudflare() |
https://www.cloudflare.com/en-gb/developer-platform/products/workers-ai/ | CLOUDFLARE_API_KEY |
| Cortex Analyst | chat_cortex_analyst() |
https://docs.snowflake.com/en/user-guide/snowflake-cortex/cortex-analyst | |
| Databricks | chat_databricks() |
https://www.databricks.com/ | |
| Deepseek | chat_deepseek() |
https://platform.deepseek.com/ | DEEPSEEK_API_KEY |
| GitHub | chat_github() |
https://github.com/marketplace/models | GITHUB_PAT |
| Google Gemini | chat_google_gemini() |
https://aistudio.google.com// | GEMINI_API_KEY or GOOGLE_API_KEY |
| Google Vertex AI | chat_google_vertex() |
https://cloud.google.com/vertex-ai | GOOGLE_API_KEY |
| Groq | chat_groq() |
https://groq.com | GROQ_API_KEY |
| Hugging Face | chat_huggingface() |
https://huggingface.co/ | HUGGINGFACE_API_KEY |
| Mistral | chat_mistral() |
https://console.mistral.ai | MISTRAL_API_KEY |
| Ollama | chat_ollama() |
https://ollama.com/ | |
| Open AI | chat_openai() |
https://platform.openai.com/ | OPENAI_API_KEY |
| Openrouter | chat_openrouter() |
https://openrouter.ai/ | OPENROUTER_API_KEY |
| Perplexity AI | chat_perplexity() |
https://www.perplexity.ai | PERPLEXITY_API_KEY |
| Portkey AI | chat_portkey() |
https://portkey.ai | PORTKEY_API_KEY |
| Snowflake | chat_snowflake() |
https://docs.snowflake.com/ | |
| vLLM | chat_vllm() |
https://docs.vllm.ai | VLLM_API_KEY |
1.3.1 Gemini via ellmer
The setup is similar to Open AI. First you need an account at https://aistudio.google.com/, then you need to get your API key at https://aistudio.google.com/apikey. Set this key as the environmental variable GOOGLE_API_KEY.
To check if it’s read correctly in R, run the following code to check.
Sys.getenv("GOOGLE_API_KEY")chat <- chat_google_gemini(model = "gemini-2.0-flash")
chat$chat("Tell me a statistics joke!")Why did the statistician bring a ladder to his math class?
Because he heard the class was going to be about higher powers!
1.3.2 Claude via ellmer
The setup is similar as before. First you need an account at https://console.anthropic.com/, then you need to get your API key at https://console.anthropic.com/settings/keys. Set this key as the environmental variable ANTHROPIC_API_KEY.
Run the following code to check if the key is stored correctly.
Sys.getenv("ANTHROPIC_API_KEY")Some people have claimed that Anthropic’s Claude model are better at R code tasks. Those interested can have a look at the vitals package, which provides a framework to evaluate LLM and contains a dataset with R coding tasks.
chat <- chat_anthropic(model = "claude-3-7-sonnet-latest")
chat$chat("Tell me a statistics joke!")Why did the statistician get thrown out of the pool?
For performing an illegal z-dive!
I guess you could say the probability of that joke making you laugh was...
statistically significant. Or maybe just mean. 😄
Which joke was funnier? 😄
2 The “hard” way with httr2
The httr2 package gives a method to create, modify, and perform HTTP requests. The ellmer package is a user friendly wrapper for making HTTP requests to get responses from the provider’s LLM. When the LLM is available in ellmer, it’s more convenient to use it, but sometimes there is a lag before it gets implemented in ellmer.
library(httr2)2.1 Using Open AI
Open AI’s API reference shows an example HTTP request for chat completions like below.
curl https://api.openai.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-4.1",
"messages": [
{
"role": "user",
"content": "All models are wrong, but some models are"
}
]
}'If you want to make a request like above in R, this is where it’s handy to use the httr2 package. Notice the similarity in the HTTP request above and the R code below.
json <- request(base_url = "https://api.openai.com/v1/chat/completions") |>
req_headers('Authorization' = paste('Bearer', Sys.getenv("OPENAI_API_KEY")),
'Content-Type' = 'application/json') |>
req_body_json(data = list(model = "gpt-4.1",
messages = list(list(role = "user",
content = "All models are wrong, but some models are")))) |>
req_perform() |>
resp_body_json()The benefit of using above is that above JSON response has been returned as a list object in R.
str(json)List of 8
$ id : chr "chatcmpl-BeAhKfPmu9oJBQ1Rba9bFc8iKun36"
$ object : chr "chat.completion"
$ created : int 1748913942
$ model : chr "gpt-4.1-2025-04-14"
$ choices :List of 1
..$ :List of 4
.. ..$ index : int 0
.. ..$ message :List of 4
.. .. ..$ role : chr "assistant"
.. .. ..$ content : chr "useful.\n\nThis phrase is a well-known quote by the statistician **George Box**. The full quote is:\n\n> “All m"| __truncated__
.. .. ..$ refusal : NULL
.. .. ..$ annotations: list()
.. ..$ logprobs : NULL
.. ..$ finish_reason: chr "stop"
$ usage :List of 5
..$ prompt_tokens : int 16
..$ completion_tokens : int 86
..$ total_tokens : int 102
..$ prompt_tokens_details :List of 2
.. ..$ cached_tokens: int 0
.. ..$ audio_tokens : int 0
..$ completion_tokens_details:List of 4
.. ..$ reasoning_tokens : int 0
.. ..$ audio_tokens : int 0
.. ..$ accepted_prediction_tokens: int 0
.. ..$ rejected_prediction_tokens: int 0
$ service_tier : chr "default"
$ system_fingerprint: chr "fp_51e1070cf2"
The response is found in one of the elements in the returned list object:
cat(json$choices[[1]]$message$content)useful.
This phrase is a well-known quote by the statistician **George Box**. The full quote is:
> “All models are wrong, but some are useful.”
It means that while no mathematical model or theory can capture the full complexity of reality, models can still provide valuable insights and predictions. Therefore, models should not be judged solely by their accuracy but also by their utility in helping us understand and make decisions.
2.2 Using Ollama
Ollama has two APIs:
- the one with endpoint:
http://localhost:11434/api/generateand - the one with endpoint:
http://localhost:11434/v1/chat/completions, which mimics the OpenAI API. This is the one thatellmeris using under the hood.
You may notice that the Ollama documentation is not well polished, so you may at times need to dig into the issues or ask the developer.
Let’s use the first endpoint as demonstration.
curl http://localhost:11434/api/generate -d '{
"model": "llama3.1:8b",
"prompt": "All models are wrong, but some models are",
"stream": false
}'The corresponding code in R would be as follows.
json <- request(base_url = "http://localhost:11434/api/generate") |>
req_body_json(data = list(model = "llama3.1:8b",
prompt = "All models are wrong, but some models are",
stream = FALSE)) |>
req_perform() |>
resp_body_json()Notice that the return object is a different structure to before.
str(json)List of 12
$ model : chr "llama3.1:8b"
$ created_at : chr "2025-06-03T01:25:48.275688Z"
$ response : chr "A classic quote!\n\nThe phrase \"All models are wrong, but some models are useful\" is a paraphrased version of"| __truncated__
$ done : logi TRUE
$ done_reason : chr "stop"
$ context :List of 276
..$ : int 128006
..$ : int 882
..$ : int 128007
..$ : int 271
..$ : int 2460
..$ : int 4211
..$ : int 527
..$ : int 5076
..$ : int 11
..$ : int 719
..$ : int 1063
..$ : int 4211
..$ : int 527
..$ : int 128009
..$ : int 128006
..$ : int 78191
..$ : int 128007
..$ : int 271
..$ : int 32
..$ : int 11670
..$ : int 12929
..$ : int 2268
..$ : int 791
..$ : int 17571
..$ : int 330
..$ : int 2460
..$ : int 4211
..$ : int 527
..$ : int 5076
..$ : int 11
..$ : int 719
..$ : int 1063
..$ : int 4211
..$ : int 527
..$ : int 5505
..$ : int 1
..$ : int 374
..$ : int 264
..$ : int 63330
..$ : int 81
..$ : int 1503
..$ : int 2373
..$ : int 315
..$ : int 264
..$ : int 11495
..$ : int 5224
..$ : int 1903
..$ : int 555
..$ : int 10058
..$ : int 8425
..$ : int 11
..$ : int 264
..$ : int 8013
..$ : int 43589
..$ : int 1122
..$ : int 323
..$ : int 24490
..$ : int 382
..$ : int 644
..$ : int 813
..$ : int 220
..$ : int 4468
..$ : int 21
..$ : int 5684
..$ : int 330
..$ : int 36500
..$ : int 323
..$ : int 25647
..$ : int 1359
..$ : int 8425
..$ : int 6267
..$ : int 1473
..$ : int 68149
..$ : int 4211
..$ : int 527
..$ : int 10049
..$ : int 97476
..$ : int 13
..$ : int 763
..$ : int 279
..$ : int 1972
..$ : int 1917
..$ : int 682
..$ : int 24654
..$ : int 527
..$ : int 3917
..$ : int 311
..$ : int 6103
..$ : int 13
..$ : int 2052
..$ : int 4211
..$ : int 527
..$ : int 5076
..$ : int 2266
..$ : int 4071
..$ : int 1243
..$ : int 568
..$ : int 3779
..$ : int 1473
.. [list output truncated]
$ total_duration : num 4.33e+09
$ load_duration : int 33130500
$ prompt_eval_count : int 19
$ prompt_eval_duration: int 217737916
$ eval_count : int 259
$ eval_duration : num 4.07e+09
To access the model response, you can find this in the response element:
cat(json$response)A classic quote!
The phrase "All models are wrong, but some models are useful" is a paraphrased version of a famous statement made by George Box, a British statistician and engineer.
In his 1976 paper "Science and Statistics," Box wrote:
"All models are approximations. In the real world all observations are subject to errors. All models are wrong."
But then he added:
"But some models are more useful than others."
This phrase has since become a mantra in many fields, including science, statistics, economics, and business, emphasizing that while no model can perfectly capture reality, some models can still provide valuable insights, predictions, or guidance.
The idea is that even though all models have limitations and make assumptions about the world, some models are more accurate, reliable, or useful than others. This might be due to factors like:
* The quality of data used to build the model
* The simplicity or complexity of the model itself
* The domain expertise and knowledge of the people who created the model
So, while all models may be wrong in some sense, Box's quote encourages us to seek out models that are still useful, even if they're not perfect.
Do you have any specific context where this phrase might apply? I'm here to help!
🎉 Aha! You’ve reached the end of this exercise! You are bee’s knees! Proceed to …