This vignette details how we can use EndpointR to interact with services hosted by major model providers, such as Anthropic, Google, and OpenAI. The focus is on sending texts for a specific, one-off purpose - similar to how traditional Machine Learning endpoints work. Though, we will see that it’s not always straightforward to get LLMs to do what we want them to do!
Before we get started, we need to make sure we have a couple of things in place:
- First, get your API key and store it as “OPENAI_API_KEY” with
set_api_key() - Second, figure out if you need the Responses API or the Chat Completions API. Currently EndpointR only supports the Chat Completions API.
- Third, choose your model - EndpointR is configured to select the smaller, cheaper model but you are free to choose specific models according to your needs.
Information: if you are unsure whether you should prefer the Completions API or the Responses API, checkout this webpage
OpenAI - Quick Start - Chat Completions API - Single Text
To get a completion for a single text we can use the
oai_complete_text() function:
set_api_key("OPENAI_API_KEY")
sentiment_system_prompt = "Analyse the sentiment of the given text."
text = "The weather has been absolutely fantastic this summer. I wish it could be like this every year, maybe I'll move to the South of France where they get 300 days of sunshine a year. Oh to dream."
oai_complete_text(
text = text,
system_prompt = sentiment_system_prompt,
model = "gpt-5.4-nano"
)Output: [1] “The sentiment of the text is positive. The speaker expresses happiness and admiration for the fantastic weather, along with a hopeful wish to experience similar conditions elsewhere.”
OpenAI - Quick Start - Chat Completions API - Data Frame of Texts
We can input a data frame directly into the
oai_complete_df() function:
review_df <- data.frame(
id = 1:5,
text = c(
"Absolutely fantastic service! The staff were incredibly helpful and friendly.",
"Terrible experience. Food was cold and the waiter was rude.",
"Pretty good overall, but nothing special. Average food and service.",
"Outstanding meal! Best restaurant I've been to in years. Highly recommend!",
"Disappointed with the long wait times. Food was okay when it finally arrived."
)
)
oai_complete_df(
review_df,
text_var = text,
id_var = id,
output_dir = NULL, # leave this to 'auto' to have your results written to a directory in your current working directory
system_prompt = sentiment_system_prompt,
concurrent_requests = 2,
chunk_size = 5
)
ℹ Processing 5 texts in 1 chunk of up to 5 each
ℹ Performing 5 requests in parallel (with 2 concurrent requests)...
✔ Batch 1: 5 successful, 0 failed
✔ Completed processing: 5 successful, 0 failed
# A tibble: 5 × 5
id content .error .error_msg .chunk
1 1 "The sentiment of the text is highly positive." FALSE NA 1
2 2 "The sentiment of the text is negative." FALSE NA 1
3 3 "The sentiment of the text is generally neutral with a slight lean… FALSE NA 1
4 4 "The sentiment of the text is highly positive." FALSE NA 1
5 5 "The sentiment of the text is negative." FALSE NA 1
>
OpenAI - Quick Start - Structured Outputs
An issue with our responses so far, is that they do not follow a specific format. This means we need to parse the results again after. However, we can use a schema to determine the format of the response:
sentiment_schema <- create_json_schema(
name = "simple_sentiment_schema",
schema = schema_object(
sentiment = schema_string(description = "Sentiment classification",
enum = c("positive", "negative", "neutral")),
required = list("sentiment")
)
)
structured_df <- oai_complete_df(
review_df,
text_var = text,
id_var = id,
schema = sentiment_schema,
output_dir = NULL,
system_prompt = sentiment_system_prompt,
concurrent_requests = 2,
chunk_size = 5
)
Processing 5 texts in 1 chunk of up to 5 each
ℹ Performing 5 requests in parallel (with 2 concurrent requests)...
✔ Batch 1: 5 successful, 0 failed
✔ Completed processing: 5 successful, 0 failed
# A tibble: 5 × 5
id content .error .error_msg .chunk
1 1 "{\"sentiment\":\"positive\"}" FALSE NA 1
2 2 "{\"sentiment\":\"negative\"}" FALSE NA 1
3 3 "{\"sentiment\":\"neutral\"}" FALSE NA 1
4 4 "{\"sentiment\":\"positive\"}" FALSE NA 1
5 5 "{\"sentiment\":\"negative\"}" FALSE NA 1
>
Now we need to extract each sentiment value into a sentiment column, one per row:
structured_df |>
dplyr::mutate(content = purrr::map(content, ~safely_from_json(.x))) |>
tidyr::unnest_wider(content)
# A tibble: 5 × 5
id sentiment .error .error_msg .chunk
1 1 positive FALSE NA 1
2 2 negative FALSE NA 1
3 3 neutral FALSE NA 1
4 4 positive FALSE NA 1
5 5 negative FALSE NA 1
WARNING: if you have a lot of data - i.e. 100,000s, or 1,000,000s of rows, you may want to split your data up and do this last step in chunks or via parallel processing to reduce peak memory usage.
OpenAI - Under the Hood
We looked at oai_complete_text() and
oai_complete_df() which are high-level convenience
functions, but it’s good to know what’s happening under the hood.
Chat Completions API
“The Chat Completions API endpoint will generate a model response from a list of messages comprising a conversation.”
For most use-cases EndpointR covers, the conversation will be a single interaction between the user and the model. Usually the goal is to achieve some narrow task repeatedly.
TIP: If you are looking for persistent, open-ended chats with LLMs, it’s best to head to Claude, ChatGPT, Gemini - or your provider of choice.
So what type of tasks might we use the Completions API for?
- Document classification, e.g. Sentiment Analysis
- Translation
- Structured data extraction
Sentiment Analysis
Whilst we would generally not recommend using OpenAI’s models for sentiment analysis, 1 it is a task most people are familiar with.
We set up a very basic 2 system prompt to tell the LLM what we want it to do with the text we send it:
system_prompt <- "Analyse the text's sentiment: "
text <- "Oh man, I'm getting to the end of my limit with this thing. WHY DOESN'T IT JUST WORK?!?"Then we can create our request and inspect it with {httr2}’s
req_dry_run() function - it’s good hygiene to check your
request is formatted as you expect it to be, particularly when using it
for the first time. For example, if you intend to have a system prompt,
is there a {“role”: “system”} key:value pair inside the messages list?
Is the value of “model”: a valid model ID? Etc.
TIP:You may need to familiarise yourself with the OpenAI API Documentation before proceeding.
sentiment_request <- oai_build_completions_request(
text,
system_prompt = system_prompt
)
sentiment_request |>
req_dry_run()Your generated HTTP request should look something like this:
POST /v1/chat/completions HTTP/1.1
accept: */*
accept-encoding: deflate, gzip
authorization:
content-length: 247
content-type: application/json
host: api.openai.com
user-agent: EndpointR
{
"model": "gpt-5.4-nano",
"messages": [
{
"role": "system",
"content": "Analyse the text's sentiment: "
},
{
"role": "user",
"content": "Oh man, I'm getting to the end of my limit with this thing. WHY DOESN'T IT JUST WORK?!?"
}
],
"max_completion_tokens": 500
}
EndpointR (via {httr2}) handles the HTTP mechanics: authentication,
request headers, and endpoint configuration. It then constructs the JSON
payload with your specified model, system prompt, and user message,
whilst setting a sensible default for max_tokens 3 (sent as
max_completion_tokens). Temperature 4 is only included when
you set it explicitly - reasoning models such as the GPT-5 family reject
non-default values.
For demonstrative purposes, we ran the same prompt with the same data three times (see below)
sentiment_response <- sentiment_request |>
perform_request_or_return_error()
sentiment_response |>
resp_check_status() |>
resp_body_json() |>
pluck("choices", 1, "message", "content")First run:
“The sentiment of the text is frustrated and exasperated.”
Second run:
“The sentiment of the text is quite negative. The speaker appears frustrated and exasperated, expressing dissatisfaction with the situation.”
Third run:
““The sentiment of the text is quite negative. The speaker appears frustrated and exasperated, expressing dissatisfaction and impatience with the situation.”
Whilst the responses are directionally/approximately accurate, the inconsistent output can be very unpleasant to work with. When scaled to hundreds or thousands of requests, this inconsistency can be extremely draining, and productivity reducing.
If we wanted the response to conform to the usual sentiment categories, we would need to write a custom parser to extract ‘negative’, ‘neutral’, or ‘positive’ from each output. Looking at the first output, it’s clear the parser would need to be reasonably sophisticated. Or we could send another request, asking our model to please output only ‘positive’, ‘negative’, or ‘neutral’ 5.
Clearly this is more work than we should be willing to do. We’ll look at techniques for how to deal with this systematically, and achieve predictable outputs in the Structured Outputs section.
What does the model do when we hand it a text which is difficult for traditional, three-category 6, document-level sentiment analysis?
ambiguous_text <- "The interface is brilliant but the performance is absolutely dreadful"
ambiguous_sentiment <- oai_build_completions_request(
ambiguous_text,
system_prompt = system_prompt
) |>
perform_request_or_return_error() |>
resp_body_json() |>
pluck("choices", 1, "message", "content")[1] “The sentiment of the text is mixed, with positive feelings expressed about the interface (”brilliant”) and negative feelings about the performance (“absolutely dreadful”).”
The model is smart enough to recognise that the sentiment does not fit neatly into ‘positive’, ‘negative’, ‘neutral’ but once again the output is not formatted in a nice way for downstream use.
Multiple Texts
Individual requests will be useful for some applications but EndpointR was built to handle more, whilst taking care of implementation details like concurrent requests, retries, and failing gracefully.
Let’s experiment with a better system prompt and a list of texts:
updated_sentiment_prompt <- "Classify the text into sentiment categories.
The accepted categories are 'positive', 'negative', 'neutral', and 'mixed'.
A 'mixed' text contains elements of positive and negative.
"
classical_texts <- c(
"It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.",
"All happy families are alike; each unhappy family is unhappy in its own way.",
"It was the best of times, it was the worst of times.",
"Call me Ishmael.",
"The sun shone, having no alternative, on the nothing new.",
"All animals are equal, but some animals are more equal than others.",
"So we beat on, boats against the current, borne back ceaselessly into the past.",
"The heart was made to be broken.",
"Tomorrow, and tomorrow, and tomorrow, creeps in this petty pace from day to day.",
"I have always depended on the kindness of strangers."
)
classical_requests <- oai_build_completions_request_list(
classical_texts,
system_prompt = updated_sentiment_prompt
)When running this in testing, it took 8.5 seconds for the 10 requests if we send one at a time.
start_seq <- Sys.time()
classical_responses <- classical_requests |>
perform_requests_with_strategy()
end_seq <- Sys.time() - start_seqAnd it took 1.8 seconds we send 10 requests in parallel - a ~4.7x speed up, showing there is some overhead cost in sending requests in parallel - i.e. we did not see a 10x speed increase for 10x requests.
start_par <- Sys.time()
classical_responses <- classical_requests |>
perform_requests_with_strategy(concurrent_requests = 10)
end_par <- Sys.time() - start_parNow when we extract the content of the response, we can see 7 that each response is a classification belonging to our classes - making our prompt slightly better helped us get to a better final result. However, if we were to repeat this over many texts - hundreds/thousands, it’s unlikely every single response would conform to our categories. Without further instruction, the models have a tendency to slightly change the output at unpredictable intervals.
classical_responses |>
map(~ resp_body_json(.x) |>
pluck("choices", 1, "message", "content") |>
as_tibble()
) |>
list_rbind() |>
mutate(text = classical_texts, .before = 1)Data Frame of Texts
df_classical_texts <- tibble(
id = 1:10,
text = classical_texts
)We have a function oai_complete_df() which takes a data
frame, an id variable, and a text variable as mandatory inputs, and
returns a data frame with columns: id_var,
content, .error, .error_msg,
.chunk.
oai_complete_df(df_classical_texts,
text_var = text,
id_var = id,
concurrent_requests = 5,
output_dir = NULL # set this to write to a temporary directory, useful for documentation and testing.
)The oai_complete_df() function has a few knobs that you
can turn to improve throughput, reduce memory usage, and increase the
likelihood of a successful response. By default it will split your data
frame into chunk_size = chunks. Each chunk is then
processed in order, and a file is created to store the results of the
previous batches. At the end of all chunks the results are retrieved
from the file.
This should help to reduce the likelihood you:
- Lose the API responses of long-running calculations which fail part-way through
- Run out of memory as the responses list grows with the size of your data frame.
The function reports on progress as it happens, letting you know which chunk is being processed, e.g. 1/5, and a progress bar for the requests within that chunk. The function provides a summary of failures and successes directly in your console after each chunk, and at the end of all chunks.
OpenAI Structured Outputs
The textual responses we get from LLM providers are difficult to deal with programmatically, because they make no guarantees on the form of the response. For example, if we ask the LLM to classify documents, sometimes it will give just the classification - ‘positive’, sometimes it will add some pre-amble ‘the document is positive’, and sometimes it will do something else entirely.
For detailed information on creating JSON schemas for structured
outputs, see
vignette("structured_outputs_json_schema").
Anthropic - Quick Start - Messages API - Single Text
Before getting started with Anthropic:
- First, get your API key from the Anthropic Console and store it
as “ANTHROPIC_API_KEY” with
set_api_key() - Second, choose your model. EndpointR defaults to “claude-haiku-5-5” for cost efficiency, but you can use any Claude model
- Third, note that Anthropic’s temperature range is 0-1 (unlike OpenAI’s 0-2)
To get a completion for a single text we can use the
ant_complete_text() function:
set_api_key("ANTHROPIC_API_KEY")
sentiment_system_prompt = "Analyse the sentiment of the given text."
text = "The weather has been absolutely fantastic this summer. I wish it could be like this every year, maybe I'll move to the South of France where they get 300 days of sunshine a year. Oh to dream."
ant_complete_text(
text = text,
system_prompt = sentiment_system_prompt,
model = "claude-haiku-4-5"
)Output: [1] “The sentiment of the text is positive and wistful. The author expresses happiness about the fantastic summer weather and daydreams about experiencing similar conditions year-round in the South of France.”
Anthropic - Quick Start - Messages API - Data Frame of Texts
We can input a data frame directly into the
ant_complete_df() function:
review_df <- data.frame(
id = 1:5,
text = c(
"Absolutely fantastic service! The staff were incredibly helpful and friendly.",
"Terrible experience. Food was cold and the waiter was rude.",
"Pretty good overall, but nothing special. Average food and service.",
"Outstanding meal! Best restaurant I've been to in years. Highly recommend!",
"Disappointed with the long wait times. Food was okay when it finally arrived."
)
)
ant_complete_df(
review_df,
text_var = text,
id_var = id,
output_dir = NULL, # leave this to 'auto' to have results written to a timestamped directory
system_prompt = sentiment_system_prompt,
concurrent_requests = 5,
chunk_size = 5
)
ℹ Processing 5 texts in 1 chunk of up to 5 each
ℹ Results will be saved as parquet files in [directory]
Processing chunk 1/1 (5 texts)
✔ Chunk 1: 5 successful, 0 failed
ℹ Processing completed, there were 5 successes and 0 failures.
# A tibble: 5 × 6
id content .error .error_msg .status .chunk
1 1 "The sentiment is highly positive." FALSE NA NA 1
2 2 "The sentiment is negative." FALSE NA NA 1
3 3 "The sentiment is neutral with a slight positive lean." FALSE NA NA 1
4 4 "The sentiment is extremely positive." FALSE NA NA 1
5 5 "The sentiment is negative." FALSE NA NA 1
>
Anthropic - effort and thinking
Most users only need the effort argument. It controls
how much the model thinks and how many tokens it uses overall, and it
takes “low”, “medium”, “high”, “xhigh” or “max”. Lower effort is cheaper
and faster, so “low” is a good place to start for classification. The
default is “high” on most models, and “medium” on Claude Opus 5.5 and
Claude Haiku 5.5. Haiku 4.5 and Sonnet 4.5 don’t accept
effort.
ant_complete_text(
text = text,
system_prompt = sentiment_system_prompt,
effort = "low"
)The thinking argument is for turning thinking on for
older models, or off on the models that allow it:
- Haiku 4.5, Sonnet 4.5 and Opus 4.5 only think with a fixed budget,
e.g.
thinking = 2048. The budget must be at least 1024 and less thanmax_tokens. - Models from 4.6 onwards use
thinking = "adaptive", and models from 4.7 onwards reject a fixed budget. -
thinking = "disabled"turns thinking off on Opus 4.7, Opus 4.8, Sonnet 5, Opus 5 and Haiku 5.5. Opus 5 and Haiku 5.5 only accept it at effort “high” or lower. - Sonnet 5.5 uses
thinking = "between_tools"instead, at effort “high” or lower. - Opus 5.5, Fable and Mythos can’t turn thinking off, so lower
effortinstead.
EndpointR checks these rules before it sends a request, and stops with a message that suggests a setting that works.
Thinking tokens count towards max_tokens. If a model
spends all of max_tokens thinking, the response has no
text, so content is NA and
stop_reason is “max_tokens”, and EndpointR shows a warning.
Raise max_tokens, or lower effort or the
thinking budget, if you see that warning. A thinking budget is a target
rather than a hard limit, so leave plenty of room above it. In our
tests, Haiku 4.5 with thinking = 2048 sometimes thought for
over 4000 tokens. The thinking_tokens column in the results
of ant_complete_df() shows how many tokens each response
spent thinking, when the API reports it.
Anthropic - Quick Start - Structured Outputs
Anthropic supports structured outputs on current Claude models. Like with OpenAI, we can use a schema to ensure predictable, parseable responses. Check the official documentation for which models support them.
sentiment_schema <- create_json_schema(
name = "simple_sentiment_schema",
schema = schema_object(
sentiment = schema_string(description = "Sentiment classification",
enum = c("positive", "negative", "neutral")),
required = c("sentiment")
)
)
structured_df <- ant_complete_df(
review_df,
text_var = text,
id_var = id,
schema = sentiment_schema,
output_dir = NULL,
system_prompt = sentiment_system_prompt,
concurrent_requests = 5,
chunk_size = 5
)
ℹ Processing 5 texts in 1 chunk of up to 5 each
ℹ Results will be saved as parquet files in [directory]
Processing chunk 1/1 (5 texts)
✔ Chunk 1: 5 successful, 0 failed
ℹ Processing completed, there were 5 successes and 0 failures.
# A tibble: 5 × 6
id content .error .error_msg .status .chunk
1 1 "{\"sentiment\":\"positive\"}" FALSE NA NA 1
2 2 "{\"sentiment\":\"negative\"}" FALSE NA NA 1
3 3 "{\"sentiment\":\"neutral\"}" FALSE NA NA 1
4 4 "{\"sentiment\":\"positive\"}" FALSE NA NA 1
5 5 "{\"sentiment\":\"negative\"}" FALSE NA NA 1
>
Now we extract the sentiment values:
structured_df |>
dplyr::mutate(content = purrr::map(content, ~safely_from_json(.x))) |>
tidyr::unnest_wider(content)
# A tibble: 5 × 6
id sentiment .error .error_msg .status .chunk
1 1 positive FALSE NA NA 1
2 2 negative FALSE NA NA 1
3 3 neutral FALSE NA NA 1
4 4 positive FALSE NA NA 1
5 5 negative FALSE NA NA 1
Anthropic - Under the Hood
We looked at ant_complete_text() and
ant_complete_df() which are high-level convenience
functions, but let’s understand what’s happening under the hood.
Messages API
Anthropic’s Messages API is similar to OpenAI’s Chat Completions API - it generates model responses from a conversation of messages. For EndpointR use cases, this is typically a single user-model interaction for a specific task.
Building Requests
Let’s create a request and inspect it:
sentiment_text <- "This is absolutely brilliant! I couldn't be happier with the results."
sentiment_request <- ant_build_messages_request(
input = sentiment_text,
system_prompt = sentiment_system_prompt,
model = "claude-haiku-4-5"
)
sentiment_request |>
req_dry_run()Your generated HTTP request should look something like this:
POST /v1/messages HTTP/1.1
Host: api.anthropic.com
User-Agent: EndpointR
Content-Type: application/json
x-api-key: [YOUR_API_KEY]
anthropic-version: 2023-06-01
{
"model": "claude-haiku-4-5",
"messages": [
{
"role": "user",
"content": "This is absolutely brilliant! I couldn't be happier with the results."
}
],
"max_tokens": 500,
"temperature": 0,
"system": "Analyse the sentiment of the given text."
}
Key differences from OpenAI: - Authentication:
Anthropic uses x-api-key header (not
Authorization: Bearer) - System prompts:
Passed as a separate system parameter (not in messages
array) - Temperature range: 0-1 for Anthropic (vs 0-2
for OpenAI) - Required parameter:
max_tokens is required for Anthropic requests
Structured Outputs with Anthropic
Anthropic’s structured outputs use the output_config
parameter with JSON schemas:
structured_request <- ant_build_messages_request(
input = sentiment_text,
system_prompt = sentiment_system_prompt,
schema = sentiment_schema,
model = "claude-haiku-4-5"
)When a schema is provided, the request includes your schema in
output_config$format. Structured outputs are generally
available, so no beta header is needed. If you also set
effort, it goes in output_config$effort next
to the schema.
The schema is validated and formatted automatically by EndpointR’s
create_json_schema() helper.
Processing Multiple Texts
Like with OpenAI, we can process multiple texts efficiently with concurrent requests:
classical_texts <- c(
"It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.",
"All happy families are alike; each unhappy family is unhappy in its own way.",
"It was the best of times, it was the worst of times."
)
# Build requests for each text
requests <- purrr::map(
classical_texts,
~ant_build_messages_request(
input = .x,
system_prompt = "Classify the sentiment as positive, negative, neutral, or mixed."
)
)
# Send all requests concurrently
responses <- perform_requests_with_strategy(
requests,
concurrent_requests = 3
)
# Extract content from responses
responses |>
purrr::map_chr(~.extract_ant_message_content(.x))Memory-Efficient Batch Processing
The ant_complete_chunks() and
ant_complete_df() functions handle large-scale processing
by:
- Dividing input into chunks (default 5000 texts per chunk)
- Processing each chunk with concurrent requests
- Writing results to parquet files immediately
- Tracking metadata including the full schema structure
This approach minimises memory usage and prevents data loss if processing is interrupted.
Key advantage: Anthropic’s metadata includes the complete schema definition (not just a boolean flag), making it easy to reproduce results and debug issues.
TBC - a native Gemini integration is in progress. In the meantime, Gemini models can be used today through Google’s OpenAI-compatible endpoint - see the next section.
OpenAI- and Anthropic-Compatible Providers
Many model providers expose APIs which are compatible with OpenAI’s
Chat Completions API, or with Anthropic’s Messages API. Because every
EndpointR request-building function accepts an endpoint_url
and a key_name, you can point EndpointR at these providers
without writing any new code:
- Store the provider’s API key under a name of your choosing with
set_api_key(), e.g.set_api_key("DEEPSEEK_API_KEY") - Pass the provider’s endpoint URL to
endpoint_url- this is the full URL of the chat completions (or messages) route, not just the base URL - Pass your chosen key name to
key_name, and setmodelto a model the provider serves
The OpenAI-flavoured functions (oai_complete_*,
oai_embed_*) send a Bearer token in the
Authorization header, which is what OpenAI-compatible
providers expect. The Anthropic-flavoured functions
(ant_complete_*) send x-api-key and
anthropic-version headers, which is what
Anthropic-compatible providers expect.
DeepSeek via the Chat Completions API
DeepSeek’s API is OpenAI-compatible, so the
oai_complete_* functions work directly:
set_api_key("DEEPSEEK_API_KEY") # one-time setup, then restart R
deepseek_url <- "https://api.deepseek.com/chat/completions"
oai_complete_text(
text = "Briefly explain the difference between a list and an atomic vector in R.",
model = "deepseek-v4-flash",
endpoint_url = deepseek_url,
key_name = "DEEPSEEK_API_KEY"
)The same applies to the data frame and chunk-based functions:
reviews <- tibble::tibble(
id = 1:3,
text = c(
"Great product, would buy again!",
"Terrible experience, broke after a week.",
"Does what it says on the tin."
)
)
deepseek_results <- oai_complete_df(
df = reviews,
text_var = text,
id_var = id,
model = "deepseek-v4-flash",
system_prompt = "Classify the sentiment of the text as positive, negative, or neutral. Respond with the label only.",
endpoint_url = deepseek_url,
key_name = "DEEPSEEK_API_KEY"
)Information: DeepSeek’s older
deepseek-chatanddeepseek-reasonermodel names are deprecated as of July 2026, and map to the non-thinking and thinking modes ofdeepseek-v4-flashrespectively. Check the DeepSeek API docs for current model names.
DeepSeek via the Messages API
DeepSeek also serves an Anthropic-compatible endpoint, so the
ant_complete_* functions work too - point them at
DeepSeek’s Anthropic-format messages route:
ant_complete_text(
text = "Briefly explain the difference between a list and an atomic vector in R.",
model = "deepseek-v4-pro",
endpoint_url = "https://api.deepseek.com/anthropic/v1/messages",
key_name = "DEEPSEEK_API_KEY"
)DeepSeek ignores the anthropic-version header and the
cache_control fields EndpointR sends for prompt caching -
both are harmless, but note you won’t get Anthropic-style prompt-caching
discounts. See DeepSeek’s
Anthropic API guide for the full list of supported and ignored
fields.
Google Gemini via the Chat Completions API
Google exposes an OpenAI-compatible layer for the Gemini API, covering both chat completions and embeddings. Get an API key from Google AI Studio, then:
set_api_key("GEMINI_API_KEY")
oai_complete_text(
text = "Briefly explain the difference between a list and an atomic vector in R.",
model = "gemini-3.5-flash",
endpoint_url = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
key_name = "GEMINI_API_KEY"
)Embeddings work the same way with the oai_embed_*
functions:
oai_embed_text(
text = "Convert this text to embeddings",
model = "gemini-embedding-001",
endpoint_url = "https://generativelanguage.googleapis.com/v1beta/openai/embeddings",
key_name = "GEMINI_API_KEY"
)Google describes this compatibility layer as beta: OpenAI parameters it doesn’t recognise are silently ignored. See the Gemini OpenAI-compatibility docs for details.
Other Compatible Providers
The same pattern works for any provider advertising OpenAI (or Anthropic) API compatibility. Some verified examples:
| Provider | Compatible with | Chat completions endpoint_url
|
|---|---|---|
| DeepSeek | OpenAI + Anthropic | https://api.deepseek.com/chat/completions |
| Google Gemini | OpenAI | https://generativelanguage.googleapis.com/v1beta/openai/chat/completions |
| Groq | OpenAI | https://api.groq.com/openai/v1/chat/completions |
| OpenRouter | OpenAI | https://openrouter.ai/api/v1/chat/completions |
| Ollama (local) | OpenAI | http://localhost:11434/v1/chat/completions |
Others (Mistral, xAI, Together AI, vLLM, LM Studio, etc.) follow the
same pattern - check the provider’s documentation for the exact URL, and
remember EndpointR needs the full route (ending
/chat/completions), not the base URL you would give an
OpenAI SDK.
For local servers like Ollama that don’t require authentication,
get_api_key() still expects the environment variable to
exist - store a placeholder,
e.g. set_api_key("OLLAMA_API_KEY") with any non-empty
value.
Caveats
Compatible does not mean identical - a few things to watch out for:
-
Structured outputs vary. The
schemaargument sends OpenAI’sresponse_formatwithjson_schema(or Anthropic’soutput_config), which not every provider supports. DeepSeek, for example, only supports the looserjson_objectmode, notjson_schema. Check the provider’s docs before relying on schemas. -
Batch APIs are not portable. The
oai_batch_*andant_batch_*functions target OpenAI’s and Anthropic’s own batch infrastructure - don’t point them at a compatible provider unless its documentation explicitly supports it. - Embeddings availability differs. Gemini serves embeddings through its OpenAI-compatible layer; DeepSeek doesn’t offer an embeddings endpoint at all.
-
Parameter handling differs at the margins. For
example, Groq converts an explicit
temperature = 0to a very small positive value, and rejects some OpenAI-only fields. -
The Hugging Face functions are different.
hf_embed_*andhf_classify_*send Hugging Face Inference payloads, not OpenAI-format ones - use them for HF Inference Endpoints only.