An API key is a unique identifier that allows software programs to communicate securely with external services, such as a large language model hosted by a provider. The key acts like a digital credential: it tells the system who you are, verifies that you have permission to use the service, and tracks how much you use it for billing and security purposes. Unfortunately, if you have a subscription to an LLM provider (e.g., Anthropic, OpenAI, etc.), this does not automatically come with API access. You generally have to set up the API account separately.
For users integrating LLMs into R or other analytic workflows, the API key serves as the bridge between your local environment and the remote model. When your script makes an API call, the key authenticates that request and allows the provider to return the model’s response. Because API keys grant direct access to paid and potentially sensitive resources, they should be stored securely—never shared publicly, committed to version control, or embedded directly in reproducible code examples.
In essence, the API key is what allows researchers to treat a model as a callable function, enabling controlled, programmatic access to a complex system running on remote infrastructure. Understanding how to manage this key responsibly is a foundational step in incorporating LLMs into reproducible research and educational applications.
In practice, an API key is a long string of letters and numbers that you include in your code whenever you send a request to the model. I asked OpenAI’s ChatGPT5 to generate a fake API key for demonstrative purposes. They generally look something like this:
sk-1234567890abcdefGHIJKLMNOPQRSTUVWXYZ1234.
The actual prefixes and length of the random alphanumeric characters usually vary by provider.
When you have an API account, you can usually generate additional keys fairly easily. This can be helpful when you want to track usage for different projects or team members. You may also be able to associate different projects or billing arrangements with different keys, which can be helpful when using APIs for different clients.
5.1 Choosing a Generative AI Model
In my opinion, there is no definitive choice for the “best” generative AI model. Each provider, and the different models offered by each provider, has its own strengths and weaknesses. Some models may perform better on certain tasks than others. I have chosen to use Anthropic’s Claude because I took a workshop from Hadley Wickham a few years ago in which he said Claude was the best assistant for R programming. Although a lot has changed since then, and all models have significantly improved their ability to help with coding, I’ve stuck with Anthropic. Who am I to argue with Hadley Wickham about something related to R? (or anything, really)
If you must use a specific provider or model due to institutional policy or other agreements, any of the foundational flagship models from the major providers can now complete most tasks reasonably well, provided that you are using them effectively (the purpose of this workshop!). If you do have some freedom in model selection, I encourage you to empirically test which provider and model combination gives you output that is most aligned with what you want.
Qwen API Pricing; more details are available by clicking on specific models on the page above
5.1.1 Calling the Model via API
We used a very simple function to call the Anthropic Claude Sonnet 4.6 model earlier. I’ve repeated it below for those who want to review it without leaving this section. I’ve provided more comments in this version about what needs to be changed to call another Anthropic model, include additional parameters, and so on.
One important thing to note is the max_tokens argument. I’ve set a fairly high default (4096, which is approximately 3000–3200 words). When using a reasoning model, especially one where you’re increasing the effort parameter, the model may use tokens during intermediate reasoning before producing the final response. This means that the total tokens required for a reasoning-model response may include both the tokens used during intermediate inference and the tokens in the visible output.
If you set max_tokens too low, the model may use all available tokens during inference and fail to produce a final result. I learned this the hard way when trying to produce short text outputs with a high effort level; the model spent all of the available tokens “thinking” and did not have enough left to generate a response. You still get charged for those tokens, so be sure to test the number of tokens needed for both the expected output length and the selected reasoning effort to ensure that you get what you need.
Code
#```{r call_claude}library(httr)library(jsonlite)call_claude <-function(prompt,model ="claude-sonnet-4-6",system =NULL,temperature =0.5,max_tokens =4096,effort ="low") {# Get API key from environment api_key <-Sys.getenv("ANTHROPIC_API_KEY")# Convert text prompt to required message format messages <-list(list(role ="user", content = prompt))# Build request body request_body <-list(model = model, # Can change to newer or different mdoels (e.g., claude-opus-4-6)messages = messages, # The content you send to the reviewermax_tokens = max_tokens, # The maximum number of tokens allowed for the model responsetemperature = temperature, # Controls the sampling of candidate tokens; not available for all modelsoutput_config =list(effort = effort) )# Add system prompt if providedif (!is.null(system)) { request_body$system <- system }# Set up headers headers <-add_headers("x-api-key"= api_key,"anthropic-version"="2023-06-01","content-type"="application/json" )# Make the API request response <-POST(url ="https://api.anthropic.com/v1/messages", headers,body =toJSON(request_body, auto_unbox =TRUE) )# Check if request was successfulif (http_status(response)$category !="Success") {stop(paste("API request failed:", http_status(response)$message,"\nDetails:", content(response, "text", encoding ="UTF-8"))) }# Parse response and extract text content result <-fromJSON(content(response, "text", encoding ="UTF-8"))return(result$content$text)}
5.1.2 OpenAI ChatGPT5 example
Here is a version of a function you can use to call OpenAI’s GPT5.2. The generation parameter of reasoning_effort is similar to the effort parameter in the Claude model.
# API Keys {#sec-api-keys}An API key is a unique identifier that allows software programs to communicate securely with external services, such as a large language model hosted by a provider. The key acts like a digital credential: it tells the system who you are, verifies that you have permission to use the service, and tracks how much you use it for billing and security purposes.Unfortunately, if you have a subscription to an LLM provider (e.g., Anthropic, OpenAI, etc.), this does not automatically come with API access.You generally have to set up the API account separately.For users integrating LLMs into R or other analytic workflows, the API key serves as the bridge between your local environment and the remote model. When your script makes an API call, the key authenticates that request and allows the provider to return the model’s response. Because API keys grant direct access to paid and potentially sensitive resources, they should be stored securely—never shared publicly, committed to version control, or embedded directly in reproducible code examples.In essence, the API key is what allows researchers to treat a model as a callable function, enabling controlled, programmatic access to a complex system running on remote infrastructure. Understanding how to manage this key responsibly is a foundational step in incorporating LLMs into reproducible research and educational applications.In practice, an API key is a long string of letters and numbers that you include in your code whenever you send a request to the model.I asked OpenAI's ChatGPT5 to generate a fake API key for demonstrative purposes.They generally look something like this: `sk-1234567890abcdefGHIJKLMNOPQRSTUVWXYZ1234`.The actual prefixes and length of the random alphanumeric characters usually vary by provider.When you have an API account, you can usually generate additional keys fairly easily.This can be helpful when you want to track usage for different projects or team members.You may also be able to associate different projects or billing arrangements with different keys, which can be helpful when using APIs for different clients.## Choosing a Generative AI ModelIn my opinion, there is no definitive choice for the "best" generative AI model. Each provider, and the different models offered by each provider, has its own strengths and weaknesses.Some models may perform better on certain tasks than others.I have chosen to use Anthropic's Claude because I took a workshop from Hadley Wickham a few years ago in which he said Claude was the best assistant for R programming. Although a lot has changed since then, and all models have significantly improved their ability to help with coding, I've stuck with Anthropic. Who am I to argue with Hadley Wickham about something related to R? (or anything, really)If you _must_ use a specific provider or model due to institutional policy or other agreements, any of the foundational flagship models from the major providers can now complete most tasks reasonably well, provided that you are using them effectively (the purpose of this workshop!).If you do have some freedom in model selection, I encourage you to empirically test which provider and model combination gives you output that is most aligned with what you want.Links to documentation for API keys:- [Anthropic](https://console.anthropic.com/) - [Anthropic API pricing](https://www.claude.com/pricing#api)- [OpenAI](https://platform.openai.com/api-keys) - [OpenAI API pricing](https://openai.com/api/pricing/)- [Google Gemini](https://aistudio.google.com/welcome) - [Google Gemini pricing](https://ai.google.dev/gemini-api/docs/pricing) (you get used to get in free credits to use in the first 90 days when making an account; unsure if this has changed) - [Gemini API quickstart guide](https://ai.google.dev/gemini-api/docs/quickstart)- [DeepSeek](https://api-docs.deepseek.com/); many DeepSeek models are available through Hugging Face, but using their API gives you access to their most recent models - [DeepSeek API pricing](https://api-docs.deepseek.com/quick_start/pricing)- [Mistral](https://docs.mistral.ai/api) - [Mistral API pricing](https://mistral.ai/pricing#api-pricing)- [Qwen is available through Alibaba Cloud](https://www.alibabacloud.com/en/product/modelstudio); this is another model family for which some models are freely available, but using their API has its benefits - [Qwen API Pricing](https://www.alibabacloud.com/help/en/model-studio/models); more details are available by clicking on specific models on the page above### Calling the Model via APIWe used a very simple function to call the Anthropic Claude Sonnet 4.6 model [earlier](03-test-connect.qmd#sec-call-claude).I've repeated it below for those who want to review it without leaving this section. I've provided more comments in this version about what needs to be changed to call another Anthropic model, include additional parameters, and so on.One important thing to note is the `max_tokens` argument. I've set a fairly high default (`4096`, which is approximately 3000–3200 words). When using a reasoning model, especially one where you're increasing the `effort` parameter, the model may use tokens during intermediate reasoning before producing the final response. This means that the total tokens required for a reasoning-model response may include both the tokens used during intermediate inference and the tokens in the visible output.If you set `max_tokens` too low, the model may use all available tokens during inference and fail to produce a final result. I learned this the hard way when trying to produce short text outputs with a high effort level; the model spent all of the available tokens "thinking" and did not have enough left to generate a response. You still get charged for those tokens, so be sure to test the number of tokens needed for both the expected output length and the selected reasoning effort to ensure that you get what you need.```{r, eval = FALSE}#```{r call_claude}library(httr)library(jsonlite)call_claude <-function(prompt,model ="claude-sonnet-4-6",system =NULL,temperature =0.5,max_tokens =4096,effort ="low") {# Get API key from environment api_key <-Sys.getenv("ANTHROPIC_API_KEY")# Convert text prompt to required message format messages <-list(list(role ="user", content = prompt))# Build request body request_body <-list(model = model, # Can change to newer or different mdoels (e.g., claude-opus-4-6)messages = messages, # The content you send to the reviewermax_tokens = max_tokens, # The maximum number of tokens allowed for the model responsetemperature = temperature, # Controls the sampling of candidate tokens; not available for all modelsoutput_config =list(effort = effort) )# Add system prompt if providedif (!is.null(system)) { request_body$system <- system }# Set up headers headers <-add_headers("x-api-key"= api_key,"anthropic-version"="2023-06-01","content-type"="application/json" )# Make the API request response <-POST(url ="https://api.anthropic.com/v1/messages", headers,body =toJSON(request_body, auto_unbox =TRUE) )# Check if request was successfulif (http_status(response)$category !="Success") {stop(paste("API request failed:", http_status(response)$message,"\nDetails:", content(response, "text", encoding ="UTF-8"))) }# Parse response and extract text content result <-fromJSON(content(response, "text", encoding ="UTF-8"))return(result$content$text)}```### OpenAI ChatGPT5 example {#sec-gpt5-call}Here is a version of a function you can use to call OpenAI's GPT5.2. The generation parameter of `reasoning_effort` is similar to the `effort` parameter in the Claude model.[Here latest model page the OpenAI Platform; will change when new model is released.)](https://platform.openai.com/docs/guides/latest-model) In fact, GPT 5.4 is now available, but since I won't be using it in this workshop, I won't be covering that implementation via API.```{r, eval = FALSE}library(httr)library(jsonlite)call_gpt52 <-function(prompt,model ="gpt-5.2",system =NULL,max_tokens =4096,reasoning_effort ="low") { # "none", "low", "medium", "high", "xhigh" api_key <-Sys.getenv("OPENAI_API_KEY")if (api_key =="") {stop("OpenAI API key is not set. Please set OPENAI_API_KEY in your environment.") }# Build messages list messages <-list(list(role ="user", content = prompt))# Add system message if providedif (!is.null(system)) { messages <-c(list(list(role ="system", content = system)), messages) }# Build request body body_list <-list(model = model,messages = messages,max_completion_tokens = max_tokens,reasoning =list(effort = reasoning_effort) )# Set up headers headers <-add_headers(Authorization =paste("Bearer", api_key),`Content-Type`="application/json" )# Make the API request resp <-POST(url ="https://api.openai.com/v1/chat/completions", headers,body =toJSON(body_list, auto_unbox =TRUE) )# Check if request was successfulif (http_status(resp)$category !="Success") {stop(paste0("API request failed (", http_status(resp)$message, "): ",content(resp, "text", encoding ="UTF-8"))) }# Parse response and extract text content res <-fromJSON(content(resp, "text", encoding ="UTF-8"), simplifyVector =TRUE)return(res$choices[[1]]$message$content)}# Example usages:# 1. Default call# cat(call_gpt52("Explain item response theory in simple terms."))# 2. Lowest reasoning effort# cat(call_gpt52("Explain item response theory in simple terms.",# reasoning_effort = "none"))# 3. Highest reasoning effort# cat(call_gpt52("Explain item response theory in simple terms.",# reasoning_effort = "xhigh"))# 4. With a system prompt# cat(call_gpt52("Explain item response theory in simple terms.",# system = "You are an expert in educational measurement. Respond concisely."))```