Skip to contents

The client is an object, not a global. Passing it explicitly is what lets a Shiny app serve two users with two different keys in one R process. The old code called Sys.setenv(OPENAI_API_KEY = ...), which is process-wide, so the second user's key silently billed the first user's requests.

Usage

gr_client(
  model = NULL,
  api = NULL,
  api_key = NULL,
  base_url = NULL,
  embedding_model = NULL,
  max_retries = NULL,
  retry_pause_base = NULL,
  timeout = NULL,
  extra_body = list(),
  headers = NULL
)

Arguments

model

Default chat model id.

api

"responses" (default) or "chat".

api_key

Optional key; resolved lazily at call time if omitted.

base_url

API base, for proxies and compatible endpoints.

embedding_model

Default embedding model id.

max_retries, retry_pause_base

Retry policy for transient failures.

timeout

Per-request timeout in seconds.

extra_body

Named list merged into every request body.

headers

Named character vector of extra HTTP headers, for endpoints that do not authenticate with a bearer token. Defaults to the api_headers option. NA as a value suppresses a header rather than sending it, which is how the automatic Authorization is dropped.

Value

An object of class gr_client: a list of the settings above. Constructing one makes no request and does not require a key; the key is resolved at call time by gr_api_key().

Endpoints behind a company gateway

base_url alone is enough when the gateway speaks the OpenAI shape and takes Authorization: Bearer. It is not enough anywhere else, and most corporate gateways are somewhere else: Azure OpenAI authenticates with api-key, API Management adds a subscription key, and many require a cost-centre or correlation id. headers covers those.

Two rules make it predictable. A header you name replaces the automatic Authorization rather than joining it, matched without regard to case; and naming any header at all makes the API key optional, because nothing here can tell which of your headers is the credential. So a gateway with its own scheme needs no OPENAI_API_KEY set at all:

gr_client(
  base_url = "https://gateway.example.com/openai/v1", api = "chat",
  headers  = c("api-key" = Sys.getenv("GATEWAY_KEY"), Authorization = NA))

Headers are credentials and routing metadata, not part of what answers, so they are excluded from the gr_cache() key for the same reason api_key is. A rotating bearer or a per-request correlation id would otherwise make every cache lookup miss. If a header changes which model answers, give that client its own base_url or model so the cache can tell them apart.

See also

gr_call() to use it, gr_mock_client() to work offline, gr_cache_client() to make repeat calls free, gr_replay_client() to re-run a recorded run, gr_api_key() for key resolution, gr_options() for the defaults, gr_result for what a call returns

Examples

# A client is a value, not a global. Two of them, two keys, one R process.
# That is what makes a Shiny app serving two users safe.
a <- gr_client(model = "gpt-4o",  api_key = "sk-user-a")
b <- gr_client(model = "gpt-4.1", api_key = "sk-user-b", timeout = 30)
vapply(list(a = a, b = b), function(cl) cl$model, character(1))
#>         a         b 
#>  "gpt-4o" "gpt-4.1" 

# What that model can actually take.
unlist(gr_model_info(a$model)[c("context_window", "max_output")])
#> context_window     max_output 
#>         128000          16384 

# No key needed to develop against the pipeline.
gr_call(gr_mock_client(function(m, p) "hi there"), "hello")$text
#> [1] "hi there"

if (FALSE) { # \dontrun{
cl <- gr_client(model = "gpt-5.6-terra")
gr_call(cl, list(list(role = "user", content = "Say hi.")), max_output = 20)
} # }