99 ACT: Working with OpenAI’s API

This module introduces the basics of interacting with OpenAI’s API from R. We’ll explore how to make API calls, handle responses, and integrate AI capabilities into data science workflows. You can find the API documentation here and the R package documentation for httr and jsonlite for making HTTP requests and handling JSON data.

99.1 Getting Started

First, we need to load the required packages:

99.1.1 API Authentication

To use OpenAI’s API, you’ll need an API key. Like we learned with other APIs, it’s important to keep this secure:

# Store API key securely (NEVER commit to Git!)
openai_api_key <- readLines("path/to/api_key.txt")

99.1.2 Making API Requests

The core workflow involves:

  • Constructing the API request
  • Sending it to OpenAI’s endpoint
  • Processing the response

Next, we define a function to generate text using OpenAI’s API. The function takes a prompt as input and returns the generated text.

Here’s a basic function for text generation:

generate_text <- function(prompt, model = "gpt-5-nano", max_output_tokens = 200) {
  response <- POST(
    # curl https://api.openai.com/v1/chat/completions
    url = "https://api.openai.com/v1/chat/completions",
    # -H "Authorization: Bearer $OPENAI_API_KEY"
    add_headers(Authorization = paste("Bearer", openai_api_key)),
    # -H "Content-Type: application/json"
    content_type_json(),
    # -d '{
    #   "model": "gpt-5-nano",
    #   "messages": [{"role": "user", "content": "What is a banana?"}]
    # }'
    encode = "json",
    body = list(
      model = model,
      messages = list(list(
        role = "user", content = prompt,
        max_output_tokens = max_output_tokens
      ))
    )
  )

  str_content <- content(response, "text", encoding = "UTF-8")
  parsed <- fromJSON(str_content)

  # return(parsed$choices[[1]]$text)
  return(parsed)
}

I have included comments in the code to show how the API request corresponds to a typical curl command you might use in the terminal.

99.2 Example Usage and Handling the Response

Now that we’ve defined our generate_text() function, let’s test it by sending a request to OpenAI’s API and working with the response.

99.2.1 Step 1: Send a Request

prompt <- "Write a haiku about data science."
generated_text <- generate_text(prompt)

99.2.2 Step 2: Examine the Raw API Response

When we call the generate_text(prompt) function, OpenAI’s API returns a structured response in JSON format, which R reads as a list. This response contains multiple components, but the most important part is the generated text.

Let’s print the raw response to see its structure.

print(generated_text)
#> $id
#> [1] "chatcmpl-DOxMn8k8a7KTXOL4TMsBwzWCW8507"
#> 
#> $object
#> [1] "chat.completion"
#> 
#> $created
#> [1] 1774840449
#> 
#> $model
#> [1] "gpt-5-nano-2025-08-07"
#> 
#> $choices
#>   index message.role
#> 1     0    assistant
#>                                                            message.content
#> 1 Charts glow in the night\nModels hum, patterns unfold\nInsight from data
#>   message.refusal message.annotations finish_reason
#> 1              NA                NULL          stop
#> 
#> $usage
#> $usage$prompt_tokens
#> [1] 14
#> 
#> $usage$completion_tokens
#> [1] 792
#> 
#> $usage$total_tokens
#> [1] 806
#> 
#> $usage$prompt_tokens_details
#> $usage$prompt_tokens_details$cached_tokens
#> [1] 0
#> 
#> $usage$prompt_tokens_details$audio_tokens
#> [1] 0
#> 
#> 
#> $usage$completion_tokens_details
#> $usage$completion_tokens_details$reasoning_tokens
#> [1] 768
#> 
#> $usage$completion_tokens_details$audio_tokens
#> [1] 0
#> 
#> $usage$completion_tokens_details$accepted_prediction_tokens
#> [1] 0
#> 
#> $usage$completion_tokens_details$rejected_prediction_tokens
#> [1] 0
#> 
#> 
#> 
#> $service_tier
#> [1] "default"
#> 
#> $system_fingerprint
#> NULL

As you can see, the response is a nested list containing various metadata (e.g., request ID, model name, creation time), the AI-generated response (inside $choices[[1]]$message$content), token usage information (inside \(usage\)total_tokens), and more.

99.2.3 Step 3: Extract the AI-Generated Text

Since the response contains both metadata and content, we need to extract only the generated text. The key part of the response is stored in:

ai_response <- generated_text$choices$message$content

Now, let’s print the AI-generated text:

print(ai_response)
#> [1] "Charts glow in the night\nModels hum, patterns unfold\nInsight from data"

Ok, so that wasn’t really readable. Let’s try to format it a bit better:

cat(ai_response, sep = "\n")

Charts glow in the night Models hum, patterns unfold Insight from data

Now we can see the haiku about data science that the model generated in response to our prompt. This is the core workflow for interacting with OpenAI’s API: send a request, receive a structured response, and extract the relevant content for use in your applications.

99.2.4 Step 4: Understanding Token Usage

Since OpenAI charges based on token usage, it’s useful to monitor how many tokens are used per request. The API response includes:

  • usage$prompt_tokens → Tokens in the input prompt
  • usage$completion_tokens → Tokens generated by the model
  • usage$total_tokens → The total token count for billing

To check token usage:

print(generated_text$usage$total_tokens) # Total tokens used
#> [1] 806
print(generated_text$usage$completion_tokens) # Tokens used for output
#> [1] 792
print(generated_text$usage$prompt_tokens) # Tokens used for input
#> [1] 14

The token usage information can help you optimize your prompts and manage costs when using the API.

99.3 Error Handling

Like we’ve seen with other APIs, it’s important to handle errors gracefully. As with any API call, errors can occur due to network issues, invalid requests, or rate limits. To ensure our script doesn’t crash, we can wrap API calls in tryCatch():

generate_text_safe <- function(prompt) {
  tryCatch(
    {
      generate_text(prompt)
    },
    error = function(e) {
      warning("API call failed: ", e$message)
      return(NULL)
    }
  )
}

Now, we can use generate_text_safe() to handle errors. If an error occurs, the function will return NULL and print a warning message.

99.4 Processing Multiple Requests

When working with multiple prompts, we can use purrr::map_chr() to process them efficiently:

library(purrr)
prompts <- c(
  "Define p-value",
  "Explain Type I error",
  "What is statistical power?"
)
responses <- list()
responses <- map(prompts, generate_text_safe)

This code generates text for each prompt in the prompts vector. If an error occurs, the response will be NULL. After running this code, we can examine the responses and handle any errors. I’ve included a table below to display the responses.

As you can see, the table displays the prompts, AI-generated responses, token usage, model name, and completion time for each request. This information can help us monitor the API usage and response quality.

99.4.1 Rate Limiting

OpenAI has rate limits we need to respect. We can add delays between requests to avoid exceeding these limits. Here’s a throttled version of the generate_text() function:

generate_text_throttled <- function(prompt) {
  Sys.sleep(1) # Wait 1 second between requests
  generate_text_safe(prompt)
}

This function adds a 1-second delay between requests to avoid exceeding OpenAI’s rate limits. You can adjust the delay as needed based on the API’s rate limits.

99.5 Your Turn!

Now it’s your turn to experiment with the OpenAI API! Try different prompts, explore various models, and see how you can integrate AI-generated text into your projects. Remember to monitor your token usage and handle errors gracefully as you work with the API.

I’ve crafted a prompt to generate your very own activity for this module. You can modify the prompt to create different activities or explore other topics. Here’s the prompt I used:

activity_prompt <- "Create a meme-tastic data science activity for graduate students learning about using OpenAI's API in Tidyverse. The activity should involve making API calls, handling responses, and analyzing the results. Include clear, concise instructions and learning objectives."


activity_response <- generate_text(activity_prompt,
  model = "gpt-5-nano",
  max_output_tokens = 3000
)

writeLines(activity_response$choices$message$content, "includes/activity.txt")

Because the response is quite long (at 6803 tokens), I’ve written it to a text file in the includes directory. You can open that file to see the generated activity. The activity is designed to help students learn how to use OpenAI’s API in R, including making API calls, handling responses, and analyzing results. It includes clear instructions and learning objectives to guide students through the process.

You may notice that the activity is generated each time I render this book. If you want to keep a specific version of the activity, you can find it in the commit history of the includes/activity.txt file in the GitHub repository for this book. You can also modify the prompt to generate a new activity or explore different topics as you see fit. Happy experimenting!

Remember that this activity is generated by the OpenAI API, so it requires careful review and editing to ensure it is accurate, clear, and appropriate. Always review AI-generated content before using it. This advice is especially important in an educational setting to ensure it meets your standards and learning objectives. Don’t be just an AI passenger. Trust but verify, as they say.

Click to see the generated activity
#> Meme-tastic Data Science with OpenAI API in the Tidyverse
#> 
#> Overview
#> This graduate-level activity lets students design data-driven prompts, call the OpenAI API from R, handle and parse the responses, and analyze the captions like a data scientist. Students will use a tidyverse workflow (dplyr, purrr, tidyr, stringr, ggplot2, tidytext) to manage prompts, capture usage metrics, and perform text analysis (sentiment, word frequencies, and simple correlations). The tone is playful (meme-ready prompts) but the analysis remains rigorous.
#> 
#> Learning objectives
#> - Call OpenAI’s chat/completions API from R in a reproducible, auditable way.
#> - Build a robust wrapper to handle API calls, collect response content, and log usage metrics (prompt_tokens, completion_tokens, total_tokens).
#> - Manage and transform API results with a tidyverse workflow (tibbles, map/pmap, unnest_tokens, joins).
#> - Perform text-analysis on generated captions (sentiment, word frequencies) using tidytext.
#> - Visualize results and interpret how prompt design and temperature affect output quality and sentiment.
#> - Communicate findings in a clear, reproducible R Markdown/Notebook deliverable.
#> 
#> Prerequisites
#> - R (4.x) and RStudio
#> - Basic familiarity with the tidyverse and text analysis concepts
#> - OpenAI API key with access to gpt-3.5-turbo or another supported model
#> - Optional: an R environment variable OPENAI_API_KEY set to your API key
#> 
#> Setup (do once)
#> - Install and load packages
#>   - Packages: httr, jsonlite, dplyr, purrr, tidyr, stringr, tibble, ggplot2, readr, tidytext, tibble, purrr
#>   - Optional: openai package (if you prefer a higher-level wrapper)
#> - Set API key
#>   - Sys.setenv(OPENAI_API_KEY = "<your-api-key>") or set in your '.Renviron' file
#> 
#> Two code-path options to call the API
#> Option A (low-level, robust): using httr
#> - This is model-agnostic and transparent.
#> 
#> ```
#> library(httr)
#> library(jsonlite)
#> library(dplyr)
#> 
#> get_openai_response <- function(prompt, model = "gpt-3.5-turbo",
#>                                max_tokens = 60, temperature = 0.6){
#>   body <- list(
#>     model = model,
#>     messages = list(list(role = "user", content = prompt)),
#>     max_tokens = max_tokens,
#>     temperature = temperature
#>   )
#> 
#>   r <- POST(
#>     "https://api.openai.com/v1/chat/completions",
#>     add_headers(
#>       Authorization = paste("Bearer", Sys.getenv("OPENAI_API_KEY")),
#>       `Content-Type` = "application/json"
#>     ),
#>     body = toJSON(body, auto_unbox = TRUE)
#>   )
#> 
#>   stop_for_status(r)
#>   res <- content(r, as = "parsed")
#>   content <- res$choices[[1]]$message$content
#>   usage <- res$usage
#>   list(content = content, usage = usage)
#> }
#> ```
#> 
#> Option B (easy wrapper): using openai R package
#> - If you have the openai package, you can use a simpler interface.
#> 
#> ```
#> # install.packages("openai")
#> library(openai)
#> Sys.setenv(OPENAI_API_KEY = Sys.getenv("OPENAI_API_KEY"))
#> 
#> response <- create_chat_completion(
#>   model = "gpt-3.5-turbo",
#>   messages = list(list(role = "user", content = prompt)),
#>   temperature = 0.6,
#>   max_tokens = 60
#> )
#> caption <- response$choices[[1]]$message$content
#> ```
#> 
#> Activity structure (2–3 hours; adapt to your course)
#> 
#> Part 1: Meme prompt design and API calls
#> Goal: generate meme captions for a small set of meme templates and capture usage metrics.
#> 
#> 1) Prepare a meme template dataset
#> - Create a small tibble with a set of meme templates and a brief context. Example templates:
#>   - "Distracted Boyfriend" – data science edition
#>   - "Two Buttons" – choosing between models
#>   - "Expanding Brain" – levels of model understanding
#>   - "First World Problems" – data wrangling pain points
#>   - "Mocking Spongebob" – commenting on common mistakes
#>   - "Success Kid" – a data win
#> 
#> 2) Design prompts that ask OpenAI to generate captions in a meme caption style
#> - Example prompts (per row, fill in the template name and context):
#>   - Prompt: "Create a witty meme caption in the style of the {template} about a data scientist choosing between 'interpretability' and 'accuracy' in a model."
#>   - Prompt: "Write a short, punchy caption for the {template} meme about data cleaning with Python/R, focusing on edge cases."
#>   - Prompt: "Produce a humorous caption for the {template} meme about overfitting versus cross-validation."
#> 
#> 3) Run API calls and collect data
#> - For each row, call the API with the prompt, set max_tokens to a conservative value (e.g., 60–80), and use a small temperature (e.g., 0.6) to balance creativity and reliability.
#> - Capture:
#>   - template
#>   - prompt
#>   - caption (response content)
#>   - model
#>   - temperature
#>   - usage: prompt_tokens, completion_tokens, total_tokens
#> - Store results as a tibble:
#> ```
#> library(purrr)
#> library(dplyr)
#> 
#> templates <- c("Distracted Boyfriend", "Two Buttons",
#>                "Expanding Brain", "First World Problems",
#>                "Mocking Spongebob", "Success Kid")
#> 
#> prompts <- c(
#>   "Create a witty caption for the Distracted Boyfriend meme about a data scientist choosing between interpretability and accuracy.",
#>   "Generate a Two Buttons caption about choosing between a simple model and a complex model.",
#>   "Write an Expanding Brain caption about levels of understanding data science topics.",
#>   "Produce a First World Problems caption about data wrangling headaches.",
#>   "Mocking Spongebob caption about an over-caffeinated notebook with too many tabs.",
#>   "Create a Success Kid caption about achieving a clean, well-documented codebase."
#> )
#> 
#> # Example scaffold—students fill in loop to call API
#> results <- tibble(template = templates, prompt = prompts) %>%
#>   mutate(api = map2(template, prompt,
#>                     ~ get_openai_response(.y, model = "gpt-3.5-turbo",
#>                                        max_tokens = 60, temperature = 0.6)
#>                    ),
#>          caption = map_chr(api, "content"),
#>          usage = map(api, "usage")) %>%
#>   select(-api)
#> ```
#> 
#> 4) Save and inspect
#> - Inspect a few captions, check for content quality, and ensure there are no API errors or obvious hallucinations.
#> - Example quick check:
#> ```
#> results %>% glimpse()
#> results %>% arrange(total_tokens) %>% head()
#> ```
#> 
#> Part 2: Clean handling and tidyverse integration
#> Goal: turn raw API results into tidy data suitable for analysis and visualization.
#> 
#> 1) Normalize usage data and extract tokens
#> ```
#> library(purrr)
#> library(dplyr)
#> 
#> results2 <- results %>%
#>   mutate(
#>     prompt_tokens = map_int(usage, ~ .x$prompt_tokens),
#>     completion_tokens = map_int(usage, ~ .x$completion_tokens),
#>     total_tokens = map_int(usage, ~ .x$total_tokens)
#>   ) %>%
#>   select(template, prompt, caption, model, temperature, prompt_tokens, completion_tokens, total_tokens)
#> ```
#> 
#> 2) Basic text prep for analysis
#> ```
#> library(tidytext)
#> library(stringr)
#> 
#> tok <- results2 %>%
#>   unnest_tokens(word, caption)
#> 
#> # Quick quality check: word counts per caption
#> word_counts <- tok %>% count(template, word, sort = TRUE)
#> ```
#> 
#> Part 3: Analysis and visualization (text analytics with tidyverse)
#> Goal: explore sentiment, word usage, and relationships between prompts and outputs.
#> 
#> 1) Simple sentiment by template (using the Bing lexicon)
#> ```
#> library(tidytext)
#> library(ggplot2)
#> 
#> sentiment_by_template <- tok %>%
#>   inner_join(get_sentiments("bing"), by = "word") %>%
#>   group_by(template) %>%
#>   summarize(sentiment_score = sum(if_else(sentiment == "positive", 1, -1, 0), na.rm = TRUE))
#> 
#> # If you want proportion instead:
#> # sentiment_by_template <- tok %>%
#> #   inner_join(get_sentiments("bing"), by = "word") %>%
#> #   group_by(template) %>%
#> #   summarize(positive = sum(sentiment == "positive"),
#> #             negative = sum(sentiment == "negative"),
#> #             net = positive - negative)
#> 
#> ggplot(sentiment_by_template, aes(x = template, y = sentiment_score, fill = template)) +
#>   geom_col(show.legend = FALSE) +
#>   coord_flip() +
#>   labs(title = "Caption sentiment by meme template",
#>        x = "Meme Template",
#>        y = "Sentiment score (positive minus negative)")
#> ```
#> 
#> 2) Caption length and distribution
#> ```
#> results2 <- results2 %>% mutate(caption_len = str_length(caption))
#> 
#> library(ggplot2)
#> ggplot(results2, aes(x = caption_len)) +
#>   geom_histogram(binwidth = 5, fill = "steelblue", color = "white") +
#>   labs(title = "Caption length distribution",
#>        x = "Caption length (characters)", y = "Count")
#> ```
#> 
#> 3) Word frequency per template (optional, creates a quick wordcloud-like sense)
#> ```
#> library(tidyr)
#> 
#> top_words <- tok %>%
#>   group_by(template, word) %>%
#>   summarize(n = n()) %>%
#>   arrange(template, desc(n)) %>%
#>   slice_head(n = 10)
#> 
#> top_words
#> ```
#> 
#> 4) Optional: learn about the effect of temperature on sentiment
#> - If you ran multiple temperatures, you can join by a temperature column and compare sentiment scores across temps.
#> 
#> ```
#> # Suppose you extended results2 to include temperature variants
#> ggplot(results2, aes(x = factor(temperature), y = sentiment_score)) +
#>   geom_boxplot() +
#>   labs(x = "Temperature", y = "Sentiment score", title = "Effect of temperature on caption sentiment")
#> ```
#> 
#> Deliverables
#> - A reproducible R script or R Markdown notebook that:
#>   - Sets up API access securely
#>   - Generates captions for each meme template via the OpenAI API
#>   - Captures and stores usage statistics
#>   - Performs basic tidytext sentiment analysis and descriptive visualizations
#>   - Documents the design decisions for prompt construction and temperature
#>   - Includes a short discussion interpreting the results and any caveats (bias, model limitations, token usage)
#> - A brief reflective write-up on prompt design: What worked, what didn’t, how prompt wording affected tone, and how temperature influenced creativity.
#> 
#> Extensions (for advanced students)
#> - Expand to 20–30 templates and use a loop/map to batch generate prompts
#> - Implement a simple rubric to rate caption quality and have human raters score a sample, then compare to model sentiment
#> - Build a small dashboard (Shiny or R Markdown with interactive ggplotly) to explore results by template, temperature, and tokens
#> - Compare multiple models (gpt-3.5-turbo vs. gpt-4o if available) and contrast performance
#> - Add moderation checks: screen captions for sensitive or inappropriate content using a simple heuristic or a moderation API
#> 
#> Tips and best practices
#> - Start with a small batch of templates to manage API costs and latency.
#> - Use a conservative max_tokens (e.g., 60–120) to keep responses concise and affordable.
#> - Use a moderate temperature (0.5–0.7) for creativity without losing coherence.
#> - Always log and save the exact prompts, model, temperature, and usage tokens for reproducibility and cost accounting.
#> - Encourage students to consider prompt engineering trade-offs: specificity vs. creativity, tone consistency, and readability.
#> - Emphasize ethical considerations: avoid generating content that is harmful or discriminatory; review outputs with a content-safety lens.
#> 
#> Assessment rubric (sample)
#> - API handling: wrapper correctly captures content and usage tokens (20 points)
#> - Data handling: prompts, responses, and metadata are cleanly stored in a tidyverse workflow (20 points)
#> - Analysis: sentiment and text features computed and visualized appropriately (20 points)
#> - Reproducibility: clear README/RMarkdown with setup, steps, and discussion (20 points)
#> - Communication: concise interpretation of results and thoughtful discussion of limitations (20 points)
#> 
#> Safety and usage notes
#> - Do not share API keys. Use environment variables or a local .Renviron file.
#> - Monitor and limit usage to manage costs and rate limits.
#> - Be mindful of OpenAI’s content policies; filter or moderate outputs as needed.
#> - Instruct students to present results as data-driven analysis, not absolute truth; discuss model limitations and biases.
#> 
#> Optional starter prompt templates (copy-paste for quick use)
#> - Prompt 1: “Create a witty meme caption in the style of the Distracted Boyfriend meme about a data scientist choosing between interpretability and accuracy.”
#> - Prompt 2: “Write a Two Buttons caption about a data scientist deciding between a fast model and a robust model, with a humorous data-science twist.”
#> - Prompt 3: “Generate an Expanding Brain caption about levels of data science expertise, from novice to expert.”
#> - Prompt 4: “Produce a First World Problems caption about the pain of cleaning messy datasets and debugging pipelines.”
#> - Prompt 5: “Mocking Spongebob caption about an over-caffeinated notebook full of tabs and failing plots.”
#> - Prompt 6: “Create a Success Kid caption about finally getting a clean, well-documented codebase with reproducible results.”
#> 
#> If you’d like, I can tailor the prompts, dataset size, or deliverables to your course length (e.g., a 2-hour sprint vs. a 3–4 hour lab) or provide a ready-to-run RMarkdown skeleton with all the steps pre-filled.

99.6 Best Practices

  • Always keep your API key secure and never hard-code it in your scripts.
  • Monitor token usage to manage costs effectively.
  • Handle errors gracefully to ensure your application remains robust.
  • Use batching and throttling to manage multiple requests and respect rate limits.
  • Regularly check OpenAI’s API documentation for updates and changes to endpoints, parameters, and best practices.

99.7 Conclusion

In this guide, we’ve covered how to generate text using OpenAI’s API in R. We’ve defined a function to interact with the API, handled responses, extracted generated text, monitored token usage, and processed multiple requests. We’ve also discussed error handling, rate limiting, and best practices for working with the API.

Now that you have the basics down, you can start experimenting with different prompts, models, and applications. The OpenAI API is powerful and flexible, allowing you to integrate AI capabilities into a wide range of projects, from chatbots to content generation to data analysis. Happy coding!