5  Using OpenRouter in R

The goal of this brief tutorial is to demonstrate how to work with an OpenAI-compatible API in R. For this example, we will use OpenRouter-provided models.

You will learn how to:

5.1 Requirements

  • OpenRouter API key
  • httr2 and jsonlite R packages

5.2 Setup

5.2.1 Install packages

If not already installed, use either install.packages() or pak to install the required packages:

pak::pak(c("httr2", "jsonlite"))

5.2.2 Load packages

5.2.3 OpenRouter API endpoint

See the setup guide for instructions on how to securely store and retrieve your OpenRouter API key using the keyring package.

openrouter_url <- "https://openrouter.ai/api/v1/chat/completions"
model_name <- "openai/gpt-oss-20b:free"
api_key <- keyring::key_get("OPENROUTER_API_KEY")

5.3 Generate a free text response

For this example, we will use the openai/gpt-oss-20b:free model, which is an open-source model available via OpenRouter.

5.3.1 Create the request body

The following shows you how to define:

  • model: The name of the model to use.
  • prompt: The prompt to send to the model. This is often called the “user” message or prompt.
  • stream: Whether to stream the response token by token. Streaming is useful for interactive applications, like chat interfaces.
  • options: A list of options to control the model’s behavior, like temperature.
request_body <- list(
  model = model_name,
  messages = list(
    list(
      role = "system",
      content = "You are a meticulous research assistant."
    ),
    list(
      role = "user",
      content = "What is your name and who made you?"
    )
  ),
  temperature = 0.2
)

5.3.2 Build and perform the request

We use httr2’s request(), req_headers(), req_body_json(), and req_perform():

resp <- request(openrouter_url) |>
  req_headers(Authorization = paste("Bearer", api_key)) |>
  req_body_json(request_body) |>
  req_perform()

5.3.2.1 Process the response

The response is an httr2_response object.

class(resp)
[1] "httr2_response"

We can convert it to a named list by combining httr2’s resp_body_string() with jsonlite’s fromJSON():

resp_list <- resp_body_json(resp)
messages <- resp_list[["choices"]][[1]][["message"]]

If the model used is a “reasoning” model, the output will contain a “thinking” field. All models include a “content” field.

In this case, since we used a reasoning model, we can print the reasoning step followed by the final response:

{
  if (!is.null(messages[["reasoning"]])) {
    cat("--- Reasoning ---\n")
    cat(messages[["reasoning"]], "\n")
  }
  cat("--- Response ---\n")
  cat(messages[["content"]], "\n")
}
--- Reasoning ---
We need to respond as a meticulous research assistant. The user asks: "What is your name and who made you?" We should answer: My name is ChatGPT, developed by OpenAI. Possibly mention GPT-4 architecture. Also mention that I'm a language model. Provide concise but thorough answer. 
--- Response ---
I’m ChatGPT, a large‑language model built on OpenAI’s GPT‑4 architecture. The model was created by the research and engineering teams at OpenAI, a research organization focused on developing and advancing artificial intelligence in a safe and beneficial manner. 

In the above code block, we used braces {} in order to make Quarto print all cat() outputs together. You don’t need to do this in a regular R script.

5.4 Generate structured output

There are many scenarios, especially in research, where we want to generate a structured response instead of free text. Many users try to achieve this using instructions added to the user prompt and LLMs are increasingly good at following these instructions. However, there is native support to define an output schema including the required names and their descriptions, which is much cleaner, easier, and more likely to succeed, without requiring laborious and extensive prompting.

We begin by defining the output schema. To do this, we create a named list that follows the JSON schema format:

  • type = "object": Indicates that the output is a JSON object.
  • properties: A named list where we define the fields we want in the output. The name of each element is the field name, and its value is another list defining the field’s type (e.g. number, string, etc.) and its description.
  • required: A character vector listing the names of the required fields that must be present in the output.
LLMinfo_schema <- list(
  type = "object",
  properties = list(
    name = list(
      type = "string",
      description = "Your name"
    ),
    manufacturer = list(
      type = "string",
      description = "The name of the person, group, or company that built you."
    ),
    knowledge_cutoff = list(
      type = "string",
      description = "Your knowledge cutoff date."
    )
  ),
  required = c("name", "manufacturer", "knowledge_cutoff")
)

Let’s rerun the previous query, but this time we will include the output schema in the request.

When generating structured output, you might choose to adjust the prompt to explicitly ask for responses that conform to the schema. This is not necessary, as models, especially those designed for structured outputs, will usually adhere to the schema without additional prompting. Note that in this case, we are purposely using the same prompt as before, while the schema is actually requesting for a third field, knowledge_cutoff, which is not mentioned in the prompt.

request_body_structured <- list(
  model = model_name,
  messages = list(
    list(
      role = "system",
      content = "You are a meticulous research assistant."
    ),
    list(
      role = "user",
      content = "What is your name and who made you?"
    )
  ),
  temperature = 0.2,
  response_format = list(
    type = "json_schema",
    json_schema = list(
      name = "LLMinfo",
      strict = TRUE,
      schema = LLMinfo_schema
    )
  )
)

Perform the request:

resp_structured <- request(openrouter_url) |>
  req_headers(Authorization = paste("Bearer", api_key)) |>
  req_body_json(request_body_structured) |>
  req_perform()

5.4.1 Process the structured response

Convert the response to a named list:

resp_structured_list <- resp_body_json(resp_structured)
messages_structured <- resp_structured_list[["choices"]][[1]][["message"]]

The response field is now a string in JSON format. We can use jsonlite::prettify() on the JSON string to pretty print it:

{
  if (!is.null(messages_structured[["reasoning"]])) {
    cat("--- Reasoning ---\n")
    cat(messages_structured[["reasoning"]], "\n")
  }
  cat("--- Response ---\n")
  prettify(messages_structured[["content"]])
}
--- Reasoning ---
We need to respond as a meticulous research assistant. The user asks: "What is your name and who made you?" We should answer: "I am ChatGPT, a language model developed by OpenAI." Possibly mention the team. The user didn't ask for anything else. Just answer. 
--- Response ---
{
    "name": "ChatGPT",
    "manufacturer": "OpenAI",
    "knowledge_cutoff": "2024-06"
}
 

You can use jsonlite::fromJSON() to convert the response field into a named list:

fromJSON(messages_structured[["content"]])
$name
[1] "ChatGPT"

$manufacturer
[1] "OpenAI"

$knowledge_cutoff
[1] "2024-06"

5.4.2 Validate the response

We can optionally use the jsonvalidate package to validate the response:

is_valid <- jsonvalidate::json_validate(
  json = messages_structured[["content"]],
  schema = jsonlite::toJSON(LLMinfo_schema),
  verbose = TRUE
)

Produce warning if the response does not conform to the schema:

if (is_valid) {
  cat("The response conforms to the schema.\n")
} else {
  warning("The response does not conform to the schema.")
}
The response conforms to the schema.