Last reviewed: November 9, 2026 (Asia/Shanghai); official references checked November 8, 2026 UTC.
Direct answer
Use httr2 to build a JSON POST to CometAPI’s /v1/chat/completions endpoint, then validate the response before returning text to an R report or application. The R-specific detail is the JSON shape: model and each message field must be scalars, while messages must remain an array even when it contains one item. Keep that array as an unnamed outer list, and decode responses without automatic vector, matrix, or data-frame simplification.
The CometAPI chat reference documents the endpoint, bearer authentication, request fields, and completion envelope. This walkthrough implements one non-streaming text result. Its one-choice rule, accepted finish state, prompt ceiling, and parsing ceiling are local application policies.
Who this is for
Use this adapter in an R script, scheduled report, or server process that needs a small HTTP boundary. The code uses the native pipe, so use R 4.1 or newer, with current httr2 and jsonlite packages. Supply COMETAPI_KEY and a reviewed COMETAPI_MODEL through protected process configuration. Choose a model that supports this endpoint and this minimal text request. Model-specific options need their own review.
Keep the credential outside notebooks, rendered reports, source files, and saved workspaces. Request objects contain authentication material even when their printed representation redacts it. Treat an RDS file or saved session containing such an object as sensitive.
Key takeaways
- Represent a single message as
list(list(role = ..., content = ...)). - Enable scalar unboxing for request fields; preserve response arrays as lists.
- Use exact list indexing and validate types before extracting assistant text.
- Set a transfer timeout and explicitly disable redirect following.
- Classify HTTP failures before parsing their bodies.
- Keep the distinction between a post-download parsing ceiling and a transfer-time byte limit visible.
For a different runtime’s transport design, see Build a CometAPI Python Client with HTTPX and asyncio . Compare Build a CometAPI Node.js Fetch Client with Zod when you need a streaming body reader that enforces a limit during transfer.
Preserve the request’s JSON shape
Install the packages in your project’s managed R library:
install.packages(c("httr2", "jsonlite"))
Create comet-client.R with the following helpers:
chat_error <- function(code) stop(code, call. = FALSE)
is_string <- function(x) {
is.character(x) && length(x) == 1L && !is.na(x)
}
chat_payload <- function(prompt, model) {
if (!is_string(model) || !nzchar(trimws(model))) {
chat_error("invalid_model_configuration")
}
if (!is_string(prompt) || !nzchar(trimws(prompt)) ||
nchar(enc2utf8(prompt), type = "bytes") > 8000L) {
chat_error("invalid_prompt")
}
list(
model = model,
messages = list(list(role = "user", content = prompt)),
stream = FALSE
)
}
build_chat_request <- function(prompt, token, model) {
if (!is_string(token) || !nzchar(token) ||
grepl("[[:space:]]", token)) {
chat_error("invalid_credential_configuration")
}
httr2::request("https://api.cometapi.com/v1/chat/completions") |>
httr2::req_auth_bearer_token(token) |>
httr2::req_headers(Accept = "application/json") |>
httr2::req_body_json(chat_payload(prompt, model), auto_unbox = TRUE) |>
httr2::req_timeout(30) |>
httr2::req_options(followlocation = FALSE) |>
httr2::req_error(is_error = function(resp) FALSE)
}
The httr2 request-body reference
documents req_body_json() and its auto_unbox argument. Adding a body selects POST automatically. The jsonlite manual
explains that automatic unboxing converts length-one atomic vectors to JSON scalars. The unnamed outer list preserves the messages array; the named inner list describes one message object.
Do not replace the outer list with the message object itself. That would change messages from an array to an object. Likewise, globally disabling unboxing would turn scalar fields such as model into arrays unless each scalar were explicitly unboxed. Build the payload in one helper so report code cannot accidentally change this boundary.
The timeout reference
documents an error when a request exceeds the configured time. Thirty seconds and 8,000 prompt bytes are illustrative local values, not provider limits. The request-performance reference
explains that redirects are normally followed; curl options
allow followlocation to be disabled. The adapter then treats a redirect as an unexpected status.
Decode lists before extracting text
Append these functions to the same file:
is_record <- function(x) {
is.list(x) && !is.null(names(x)) &&
anyDuplicated(names(x)) == 0L
}
accepted_chat_text <- function(data) {
if (!is_record(data) ||
!identical(data[["object"]], "chat.completion")) {
chat_error("invalid_completion_envelope")
}
choices <- data[["choices"]]
if (!is.list(choices) || !is.null(names(choices)) ||
length(choices) != 1L) {
chat_error("invalid_completion_choices")
}
choice <- choices[[1L]]
if (!is_record(choice) ||
!identical(choice[["finish_reason"]], "stop")) {
chat_error("unaccepted_finish_reason")
}
message <- choice[["message"]]
if (!is_record(message) ||
!identical(message[["role"]], "assistant") ||
!is.null(message[["refusal"]]) ||
!is.null(message[["tool_calls"]])) {
chat_error("unaccepted_assistant_message")
}
text <- message[["content"]]
if (!is_string(text) || !nzchar(trimws(text))) {
chat_error("missing_assistant_text")
}
text
}
parse_chat_response <- function(resp) {
status <- httr2::resp_status(resp)
if (status != 200L) chat_error(paste0("http_", status))
content_type <- httr2::resp_header(resp, "content-type")
if (!is_string(content_type)) chat_error("unexpected_media_type")
media_type <- tolower(trimws(
strsplit(content_type, ";", fixed = TRUE)[[1L]][1L]
))
if (!identical(media_type, "application/json")) {
chat_error("unexpected_media_type")
}
if (length(httr2::resp_body_raw(resp)) > 1048576L) {
chat_error("response_exceeds_parse_limit")
}
data <- tryCatch(
httr2::resp_body_json(
resp, simplifyVector = FALSE,
simplifyDataFrame = FALSE, simplifyMatrix = FALSE
),
error = function(e) chat_error("invalid_response_json")
)
accepted_chat_text(data)
}
chat_complete <- function(
prompt,
token = Sys.getenv("COMETAPI_KEY"),
model = Sys.getenv("COMETAPI_MODEL")
) {
req <- build_chat_request(prompt, token, model)
resp <- tryCatch(
httr2::req_perform(req, verbosity = 0),
error = function(e) chat_error("transport_failed")
)
parse_chat_response(resp)
}
The body-extraction reference documents raw bytes, JSON parsing, media-type checking, and arguments forwarded to jsonlite. The jsonlite manual documents simplification into vectors, data frames, and matrices. Turning all three off keeps the parser’s expected nested lists consistent.
Use [["field"]] for exact extraction rather than relying on partial field matching. Type checks precede nested access, and the choices list must be an unnamed array containing exactly one record. A JSON object with a key that happens to resemble an index is not accepted as the choices array. The code also rejects a vector or list in content, even if it contains one printable value.
The HTTP-error reference
documents the is_error predicate. Returning FALSE lets this adapter inspect statuses itself and emit fixed codes without incorporating upstream error bodies. The response is still rejected unless its status is 200. Keep error text, request objects, and provider bodies out of public report diagnostics.
Understand the response-size boundary
req_perform() ordinarily brings the response into memory. Consequently, the one-mebibyte check above runs after the download. It limits the body admitted to JSON parsing; it does not stop an oversized response during transfer or impose a hard memory ceiling. Error responses are downloaded too, even though this parser rejects their status before inspecting their content.
The request-performance reference
also offers a path argument for downloading to a file instead of retaining the body in memory. That option changes storage ownership and requires a separate cleanup and size policy. For a public service processing untrusted or potentially large responses, enforce an upstream response limit or implement and test a transfer-time reader before adopting this compact adapter. Do not describe a post-download length check as that protection.
Check JSON behavior without credentials
Save these checks as check-client.R beside the client:
source("comet-client.R")
payload <- chat_payload("Explain idempotency.", "fixture-model")
wire <- jsonlite::toJSON(payload, auto_unbox = TRUE)
round_trip <- jsonlite::fromJSON(wire, simplifyVector = FALSE)
stopifnot(
identical(round_trip[["model"]], "fixture-model"),
is.list(round_trip[["messages"]]),
is.null(names(round_trip[["messages"]])),
length(round_trip[["messages"]]) == 1L,
identical(round_trip[["messages"]][[1L]][["role"]], "user"),
identical(round_trip[["stream"]], FALSE)
)
good <- list(
object = "chat.completion",
choices = list(list(
finish_reason = "stop",
message = list(role = "assistant", content = "Fixture answer.")
))
)
stopifnot(identical(accepted_chat_text(good), "Fixture answer."))
expect_code <- function(value, code) {
observed <- tryCatch(
{ accepted_chat_text(value); "accepted" },
error = function(e) conditionMessage(e)
)
stopifnot(identical(observed, code))
}
bad <- good
bad[["choices"]][[1L]][["finish_reason"]] <- "length"
expect_code(bad, "unaccepted_finish_reason")
bad <- good
bad[["choices"]][[1L]][["message"]][["content"]] <- list("text")
expect_code(bad, "missing_assistant_text")
bad <- good
bad[["choices"]][[1L]][["message"]][["refusal"]] <- "Fixture refusal."
expect_code(bad, "unaccepted_assistant_message")
bad <- good
names(bad[["choices"]]) <- "0"
expect_code(bad, "invalid_completion_choices")
Run Rscript check-client.R in your project environment. These checks cover payload serialization and the pure acceptance function. They do not exercise HTTP, timeout behavior, media-type handling, or the post-download limit. Add response fixtures or a local HTTP test server for those boundaries. The examples were reviewed against the live documentation; these R checks and an authenticated request have not been executed for this article.
Contract details to verify
| Boundary | Required fixture or observation | Expected behavior |
|---|---|---|
| Serialization | One message and a scalar model | JSON array for messages; string for model |
| Status | 302, 401, 429, and 500 responses | Fixed status code; no completion or error-body disclosure |
| Media type | HTML with status 200 | unexpected_media_type |
| JSON syntax | Malformed JSON with a JSON media type | invalid_response_json |
| Size | A downloaded body over 1,048,576 bytes | response_exceeds_parse_limit before JSON parsing |
| Completion | Refusal, tools, empty text, and length | Rejected by the text-only acceptance policy |
| Transport | Delayed headers or a stalled body | Fixed transport error when the transfer limit is exceeded |
Failure modes
An R convenience conversion changes the wire contract. Keep message construction in the payload helper and preserve its round-trip fixture. A data frame can be useful for analysis after validation; do not use one as an implicit substitute for the API’s nested message structure.
A 200 response produces an unusable report entry. Validate the envelope and outcome before writing a result row. Retain a separate status column for failures instead of substituting an empty string that looks like a successful answer.
A partial completion is labeled complete. The CometAPI reference distinguishes stop, length, tool_calls, and content_filter. This client accepts only stop; a product that supports other outcomes needs a result type carrying their meaning.
A report retries uncertain generations. The adapter adds no retry policy. A timeout records local failure to obtain an accepted response; it does not establish that remote processing stopped. Add attempt identity, reconciliation, an attempt cap, and a total workflow budget before implementing retries around a paid generation.
FAQ
Why explicitly disable JSON simplification?
The parser expects the same list structure for one choice and several choices. Preserving that structure makes the one-choice policy and field-type checks easier to review. Convert accepted results to a tibble or data frame afterward.
Can this function parse streaming responses?
No. The request sets stream = FALSE. The CometAPI reference describes streaming as SSE events, which require incremental event parsing and explicit incomplete-stream handling.
Does the parsing ceiling control token spend?
No. It limits which downloaded body this function parses. Choose model-specific output controls separately, and account for uncertain attempts using your billing and retry policies.
Is an empty tool-call list accepted?
No. This deliberately narrow policy rejects any non-null tool_calls field. Extend the result model explicitly if your application needs a different policy.
Sources checked
These maintained primary references were fetched successfully on November 8, 2026 UTC. They support an evergreen integration rather than a newly announced release. The httr2 pages displayed version 1.3.0; record the package versions installed in your own environment.
- CometAPI: Create a chat completion — request route, authentication, JSON fields, completion outcomes, and streaming distinction.
- httr2: Send data in request body — JSON serialization, scalar unboxing, and automatic POST selection.
- httr2: Extract body from response — raw bytes, JSON parsing, content-type checks, and simplification options.
- httr2: Perform a request — response storage, redirect behavior, and transport errors.
- httr2: Set time limit for a request — request timeout semantics.
- httr2: Set arbitrary curl options — explicit underlying transport configuration.
- httr2: Control handling of HTTP errors — application-owned status classification.
- jsonlite package manual — scalar unboxing and array, data-frame, and matrix simplification.
Reader next step
Run the offline JSON checks, then complete the HTTP fixture matrix before using the adapter in a report job. Supply protected configuration, make one authorized staging request with a harmless prompt, and record package versions, model ID, status class, elapsed time, and acceptance code. Keep the observed staging result separate from the fixture evidence. If response size cannot be bounded by the surrounding system, complete the transfer-time limit design before exposing this adapter to larger workloads.