| Title: | Minimal LLM Chat Interface |
|---|---|
| Description: | A minimal-dependency client for Large Language Model chat APIs. Supports 'OpenAI' <https://openai.com/>, 'Anthropic' 'Claude' <https://claude.com/>, 'Moonshot' 'Kimi' <https://www.moonshot.ai/>, 'Ollama' <https://ollama.com/>, and other 'OpenAI'-compatible endpoints. Includes an agent loop with tool use and a 'Model Context Protocol' client <https://modelcontextprotocol.io/>. API design is derived from the 'ellmer' package, reimplemented with only base R, 'curl', and 'jsonlite'. |
| Authors: | Troy Hernandez [aut, cre] (ORCID: <https://orcid.org/0009-0005-4248-604X>), cornball.ai [cph], ellmer team [cph] (API design derived from ellmer) |
| Maintainer: | Troy Hernandez <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.4 |
| Built: | 2026-05-24 22:56:25 UTC |
| Source: | https://github.com/cornball-ai/llm.api |
Send a prompt to an LLM with tools. Automatically handles tool calls in a loop until the model responds with text only.
agent(prompt, tools = list(), tool_handler = NULL, system = NULL, model = NULL, provider = c("anthropic", "openai", "moonshot", "ollama"), max_turns = 20L, verbose = TRUE, history = NULL, history_callback = NULL, cache = c("none", "5m", "1h"), thinking_budget_tokens = NULL, ...)agent(prompt, tools = list(), tool_handler = NULL, system = NULL, model = NULL, provider = c("anthropic", "openai", "moonshot", "ollama"), max_turns = 20L, verbose = TRUE, history = NULL, history_callback = NULL, cache = c("none", "5m", "1h"), thinking_budget_tokens = NULL, ...)
prompt |
Character. The user message. |
tools |
List. Tool definitions (from mcp_tools_for_claude or manual). |
tool_handler |
Function. Called with (name, args), returns result string. |
system |
Character. System prompt. |
model |
Character. Model name. |
provider |
Character. Provider: "anthropic", "openai", "moonshot", or "ollama". |
max_turns |
Integer. Maximum tool-use turns (default: 20). |
verbose |
Logical. Print tool calls and results. |
history |
List or NULL. Previous conversation history to continue from. |
history_callback |
Function or NULL. Called as
|
cache |
Character. Anthropic prompt caching for the system
message: |
thinking_budget_tokens |
Integer or NULL. Anthropic extended
thinking budget; must be at least 1024 and less than
|
... |
Additional parameters passed to the API. |
List with final response and conversation history. The
returned $usage carries cumulative input_tokens,
output_tokens, total_tokens, and cost (USD
scalar, derived from the bundled price snapshot; 0 for
Ollama; NA_real_ for models not in the snapshot). It also
carries cumulative cache activity: cache_read_input_tokens
(Anthropic cache reads plus OpenAI/Moonshot cached prompt tokens),
cache_creation_input_tokens (total Anthropic cache writes),
and the per-TTL split cache_creation$ephemeral_5m_input_tokens
/ cache_creation$ephemeral_1h_input_tokens. Passing this
$usage back to usage_cost recomputes the same
cost.
## Not run: # With MCP server conn <- mcp_connect("r", "mcp_server.R") tools <- mcp_tools_for_claude(conn) result <- agent( "What files are in the current directory?", tools = tools, tool_handler = function(name, args) { mcp_call(conn, name, args)$text } ) ## End(Not run)## Not run: # With MCP server conn <- mcp_connect("r", "mcp_server.R") tools <- mcp_tools_for_claude(conn) result <- agent( "What files are in the current directory?", tools = tools, tool_handler = function(name, args) { mcp_call(conn, name, args)$text } ) ## End(Not run)
Send a message to a Large Language Model and get a response.
chat(prompt, model = NULL, system = NULL, history = NULL, temperature = NULL, max_tokens = NULL, provider = c("auto", "openai", "anthropic", "moonshot", "ollama"), stream = FALSE, cache = c("none", "5m", "1h"), thinking_budget_tokens = NULL, ...)chat(prompt, model = NULL, system = NULL, history = NULL, temperature = NULL, max_tokens = NULL, provider = c("auto", "openai", "anthropic", "moonshot", "ollama"), stream = FALSE, cache = c("none", "5m", "1h"), thinking_budget_tokens = NULL, ...)
prompt |
Character. The user message to send. |
model |
Character. Model name (e.g., "gpt-5.4-mini", "claude-sonnet-4-6", "qwen3.5:9b"). |
system |
Character or NULL. System prompt to set context. |
history |
List or NULL. Previous conversation turns. |
temperature |
Numeric or NULL. Sampling temperature (0-2). |
max_tokens |
Integer or NULL. Maximum tokens in response. |
provider |
Character. Provider: "auto", "openai", "anthropic", "moonshot", or "ollama". |
stream |
Logical. Stream the response (prints as it arrives). |
cache |
Character. Anthropic prompt caching for the system
message: |
thinking_budget_tokens |
Integer or NULL. Anthropic extended
thinking budget; must be at least 1024 and less than
|
... |
Additional parameters passed to the API. |
A list with:
content |
The assistant's response text |
thinking |
Chain-of-thought from reasoning models, or NULL.
Populated from |
finish_reason |
Why generation stopped. |
model |
Model used |
usage |
Token usage (if available). When the model is in the
bundled price snapshot, also carries |
history |
Updated conversation history |
## Not run: # Simple chat chat("What is 2+2?") # With system prompt chat("Explain R", system = "You are a helpful programming tutor.") # Continue conversation result <- chat("Hello") chat("Tell me more", history = result$history) ## End(Not run)## Not run: # Simple chat chat("What is 2+2?") # With system prompt chat("Explain R", system = "You are a helpful programming tutor.") # Continue conversation result <- chat("Hello") chat("Tell me more", history = result$history) ## End(Not run)
Convenience wrapper for 'Anthropic' 'Claude' models.
chat_claude(prompt, model = "claude-sonnet-4-6", ...)chat_claude(prompt, model = "claude-sonnet-4-6", ...)
prompt |
Character. The user message to send. |
model |
Character. Model name (e.g., "gpt-5.4-mini", "claude-sonnet-4-6", "qwen3.5:9b"). |
... |
Additional parameters passed to the API. |
The assistant's response as a character string, or a list when
history is in use. See chat for details.
## Not run: chat_claude("Explain the theory of relativity") chat_claude("Write a poem", model = "claude-haiku-4-5") ## End(Not run)## Not run: chat_claude("Explain the theory of relativity") chat_claude("Write a poem", model = "claude-haiku-4-5") ## End(Not run)
Convenience wrapper for local 'Ollama' models.
chat_ollama(prompt, model = "qwen3.5:9b", ...)chat_ollama(prompt, model = "qwen3.5:9b", ...)
prompt |
Character. The user message to send. |
model |
Character. Model name (e.g., "gpt-5.4-mini", "claude-sonnet-4-6", "qwen3.5:9b"). |
... |
Additional parameters passed to the API. |
The assistant's response as a character string, or a list when
history is in use. See chat for details.
## Not run: chat_ollama("What is machine learning?") chat_ollama("Explain Docker", model = "mistral") ## End(Not run)## Not run: chat_ollama("What is machine learning?") chat_ollama("Explain Docker", model = "mistral") ## End(Not run)
Convenience wrapper for 'OpenAI' models.
chat_openai(prompt, model = "gpt-5.4-mini", ...)chat_openai(prompt, model = "gpt-5.4-mini", ...)
prompt |
Character. The user message to send. |
model |
Character. Model name (e.g., "gpt-5.4-mini", "claude-sonnet-4-6", "qwen3.5:9b"). |
... |
Additional parameters passed to the API. |
The assistant's response as a character string, or a list when
history is in use. See chat for details.
## Not run: chat_openai("Explain quantum computing") chat_openai("Write a haiku", model = "gpt-5.4-mini") ## End(Not run)## Not run: chat_openai("Explain quantum computing") chat_openai("Write a haiku", model = "gpt-5.4-mini") ## End(Not run)
Creates a chat session that maintains conversation history internally. Returns a list of functions for interacting with the session.
chat_session(model = NULL, system_prompt = NULL, provider = c("openai", "anthropic", "moonshot", "ollama"), ...)chat_session(model = NULL, system_prompt = NULL, provider = c("openai", "anthropic", "moonshot", "ollama"), ...)
model |
Character. Model name. |
system_prompt |
Character or NULL. System prompt. |
provider |
Character. Provider: "openai", "anthropic", "moonshot", or "ollama". |
... |
Additional parameters passed to chat(). |
A list with functions:
chat(message) |
Send a message, returns response text |
stream(message) |
Stream a response, returns response text |
stream_async(message) |
Async stream for shinychat (returns string) |
last_turn() |
Get the last response as list(role, text) |
history() |
Get full conversation history |
clear() |
Clear conversation history |
## Not run: # Create a session session <- chat_session(model = "gpt-5.4-mini", system_prompt = "You are helpful.") # Chat (history maintained automatically) response <- session$chat("Hello") response2 <- session$chat("Tell me more") # Get last response last <- session$last_turn() last$text # Clear and start over session$clear() ## End(Not run)## Not run: # Create a session session <- chat_session(model = "gpt-5.4-mini", system_prompt = "You are helpful.") # Chat (history maintained automatically) response <- session$chat("Hello") response2 <- session$chat("Tell me more") # Get last response last <- session$last_turn() last$text # Clear and start over session$clear() ## End(Not run)
Convenience wrapper for chat_session with Anthropic provider.
chat_session_anthropic(model = "claude-sonnet-4-6", system_prompt = NULL, ...)chat_session_anthropic(model = "claude-sonnet-4-6", system_prompt = NULL, ...)
model |
Character. Model name (default: "claude-sonnet-4-6"). |
system_prompt |
Character or NULL. System prompt. |
... |
Additional parameters passed to chat_session(). |
A chat session list.
# Construct a session (no network call yet) session <- chat_session_anthropic(system_prompt = "You are helpful.") ## Not run: session$chat("Hello") ## End(Not run)# Construct a session (no network call yet) session <- chat_session_anthropic(system_prompt = "You are helpful.") ## Not run: session$chat("Hello") ## End(Not run)
Convenience wrapper for chat_session with Ollama provider.
chat_session_ollama(model = "qwen3.5:9b", system_prompt = NULL, ...)chat_session_ollama(model = "qwen3.5:9b", system_prompt = NULL, ...)
model |
Character. Model name (default: "qwen3.5:9b"). |
system_prompt |
Character or NULL. System prompt. |
... |
Additional parameters passed to chat_session(). |
A chat session list.
# Construct a session (no network call yet) session <- chat_session_ollama(system_prompt = "You are helpful.") ## Not run: session$chat("Hello") ## End(Not run)# Construct a session (no network call yet) session <- chat_session_ollama(system_prompt = "You are helpful.") ## Not run: session$chat("Hello") ## End(Not run)
Convenience wrapper for chat_session with OpenAI provider.
chat_session_openai(model = "gpt-5.4-mini", system_prompt = NULL, ...)chat_session_openai(model = "gpt-5.4-mini", system_prompt = NULL, ...)
model |
Character. Model name (default: "gpt-5.4-mini"). |
system_prompt |
Character or NULL. System prompt. |
... |
Additional parameters passed to chat_session(). |
A chat session list.
# Construct a session (no network call yet) session <- chat_session_openai(system_prompt = "You are helpful.") ## Not run: session$chat("Hello") ## End(Not run)# Construct a session (no network call yet) session <- chat_session_openai(system_prompt = "You are helpful.") ## Not run: session$chat("Hello") ## End(Not run)
Convenience function that sets up MCP connections and returns a function for chatting with tools.
create_agent(servers = list(), system = NULL, model = NULL, provider = c("anthropic", "openai", "moonshot", "ollama"), verbose = TRUE)create_agent(servers = list(), system = NULL, model = NULL, provider = c("anthropic", "openai", "moonshot", "ollama"), verbose = TRUE)
servers |
Named list of server configs. Each can be: - 'list(port = 7850)' for already-running servers - 'list(command = "r", args = "server.R", port = 7850)' to start and connect |
system |
Character. Default system prompt. |
model |
Character. Default model. |
provider |
Character. Provider: "anthropic", "openai", "moonshot", or "ollama". |
verbose |
Logical. Print tool calls. |
A function that takes a prompt and returns a response.
## Not run: # Connect to already-running server chat_fn <- create_agent( servers = list(codeR = list(port = 7850)), system = "You are a helpful coding assistant." ) # Or start server automatically chat_fn <- create_agent( servers = list( codeR = list(command = "r", args = "mcp_server.R", port = 7850) ) ) result <- chat_fn("List files in current directory") ## End(Not run)## Not run: # Connect to already-running server chat_fn <- create_agent( servers = list(codeR = list(port = 7850)), system = "You are a helpful coding assistant." ) # Or start server automatically chat_fn <- create_agent( servers = list( codeR = list(command = "r", args = "mcp_server.R", port = 7850) ) ) result <- chat_fn("List files in current directory") ## End(Not run)
Thin convenience wrapper over [history_tool_calls()]. Pass 'completed_only = TRUE' to count only calls that have matching results (i.e., to skip mid-flight calls at the end of a turn that got cut off).
history_count_tool_calls(history, completed_only = FALSE)history_count_tool_calls(history, completed_only = FALSE)
history |
List of messages. |
completed_only |
Logical. When 'TRUE', count only calls whose result is present in the history. Default 'FALSE'. |
Single integer.
Accepts either a 'history' list as returned by [agent()] (in which case every entry is treated as a message) or any list-of-messages that follows the same shape. Returns a list of records, one per tool call, each with:
history_tool_calls(history)history_tool_calls(history)
history |
List of messages, typically the 'history' element from an [agent()] return value. |
Canonical call id (synthesized for Ollama responses that omit one).
Tool name.
Argument list. Parsed from JSON for OpenAI-style shapes; passed through for Anthropic.
Tool result text, or 'NULL' if the call has no matching result yet.
'TRUE' when 'result' is non-'NULL'.
1-based index of the assistant message that issued the call.
1-based index of the message carrying the result, or 'NA_integer_' for unfinished calls.
'"anthropic"' or '"openai"'. Useful when a consumer needs to branch on shape (rare).
A list of tool-call records (possibly empty). Records are returned in the order calls were issued.
List all models downloaded in Ollama.
list_ollama_models(base_url = "http://localhost:11434")list_ollama_models(base_url = "http://localhost:11434")
base_url |
Character. Ollama server URL (default: http://localhost:11434). |
A data frame with model information (name, size, modified).
## Not run: list_ollama_models() ## End(Not run)## Not run: list_ollama_models() ## End(Not run)
Stores a base URL in the llm.api.api_base option, which
chat uses as the default endpoint.
llm_base(url)llm_base(url)
url |
Character. Base URL for the API endpoint. |
The previous value of the option, invisibly.
old <- llm_base("http://localhost:11434") # 'Ollama' llm_base(old) # restoreold <- llm_base("http://localhost:11434") # 'Ollama' llm_base(old) # restore
Stores an API key in the llm.api.api_key option, which
chat prefers over environment variables.
llm_key(key)llm_key(key)
key |
Character. API key for authentication. |
The previous value of the option, invisibly.
old <- llm_key("sk-not-a-real-key") llm_key(old) # restoreold <- llm_key("sk-not-a-real-key") llm_key(old) # restore
Call a tool on an MCP server
mcp_call(conn, name, arguments = list())mcp_call(conn, name, arguments = list())
conn |
An MCP connection object. |
name |
Character. Tool name. |
arguments |
List. Tool arguments. |
Tool result (list with content and text).
## Not run: conn <- mcp_connect(port = 7850) result <- mcp_call(conn, "read_file", list(path = "README.md")) mcp_close(conn) ## End(Not run)## Not run: conn <- mcp_connect(port = 7850) result <- mcp_call(conn, "read_file", list(path = "README.md")) mcp_close(conn) ## End(Not run)
Close an MCP connection
mcp_close(conn)mcp_close(conn)
conn |
An MCP connection object. |
NULL, invisibly. Called for its side effect of closing
the underlying socket.
## Not run: conn <- mcp_connect(port = 7850) mcp_close(conn) ## End(Not run)## Not run: conn <- mcp_connect(port = 7850) mcp_close(conn) ## End(Not run)
Connects to an MCP server via TCP socket.
mcp_connect(host = "localhost", port, name = NULL, timeout = 30)mcp_connect(host = "localhost", port, name = NULL, timeout = 30)
host |
Character. Server hostname (default: "localhost"). |
port |
Integer. Server port. |
name |
Character. Friendly name for this server. |
timeout |
Numeric. Connection timeout in seconds (default: 30). |
An MCP connection object (list with socket and tools).
## Not run: # Start server first: r mcp_server.R --port 7850 conn <- mcp_connect(port = 7850, name = "codeR") tools <- mcp_tools(conn) result <- mcp_call(conn, "read_file", list(path = "README.md")) mcp_close(conn) ## End(Not run)## Not run: # Start server first: r mcp_server.R --port 7850 conn <- mcp_connect(port = 7850, name = "codeR") tools <- mcp_tools(conn) result <- mcp_call(conn, "read_file", list(path = "README.md")) mcp_close(conn) ## End(Not run)
Spawns an MCP server process and connects to it. Requires the server script to support –port argument.
mcp_start(command, args = character(), port = NULL, name = NULL, startup_wait = 2)mcp_start(command, args = character(), port = NULL, name = NULL, startup_wait = 2)
command |
Character. Command to run the server (e.g., "r", "Rscript"). |
args |
Character vector. Arguments (path to server script). |
port |
Integer. Port for the server (default: random 7850-7899). |
name |
Character. Friendly name. |
startup_wait |
Numeric. Seconds to wait for server startup. |
An MCP connection object.
## Not run: conn <- mcp_start("Rscript", args = "mcp_server.R", port = 7850) mcp_close(conn) ## End(Not run)## Not run: conn <- mcp_start("Rscript", args = "mcp_server.R", port = 7850) mcp_close(conn) ## End(Not run)
List tools from an MCP connection
mcp_tools(conn)mcp_tools(conn)
conn |
An MCP connection object. |
List of tool definitions.
## Not run: conn <- mcp_connect(port = 7850) tools <- mcp_tools(conn) mcp_close(conn) ## End(Not run)## Not run: conn <- mcp_connect(port = 7850) tools <- mcp_tools(conn) mcp_close(conn) ## End(Not run)
Converts MCP tool definitions to the format used by Claude/OpenAI.
mcp_tools_for_api(conn)mcp_tools_for_api(conn)
conn |
An MCP connection, or list of connections. |
List of tools in API format.
## Not run: conn <- mcp_connect(port = 7850) tools <- mcp_tools_for_api(conn) mcp_close(conn) ## End(Not run)## Not run: conn <- mcp_connect(port = 7850) tools <- mcp_tools_for_api(conn) mcp_close(conn) ## End(Not run)
Wrapper for mcp_tools_for_api, retained for backwards
compatibility.
mcp_tools_for_claude(conn)mcp_tools_for_claude(conn)
conn |
An MCP connection, or list of connections. |
List of tools in API format.
## Not run: conn <- mcp_connect(host = "localhost", port = 7850) tools <- mcp_tools_for_claude(conn) mcp_close(conn) ## End(Not run)## Not run: conn <- mcp_connect(host = "localhost", port = 7850) tools <- mcp_tools_for_claude(conn) mcp_close(conn) ## End(Not run)
'llm.api' estimates 'usage$cost' from a model-price table baked into the package at release time. This returns the date that bundled table was generated.
prices_snapshot_date()prices_snapshot_date()
It does not contact the network, check for newer prices, or update the installed package. Use the date to display staleness warnings (see [prices_snapshot_stale()]) or to decide when the package maintainer should regenerate the snapshot for a future release.
The table is generated from BerriAI/litellm's 'model_prices_and_context_window.json' (https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json). Cost estimates are offline and approximate, and may differ from current provider billing. Cached-input pricing follows each provider's published model: OpenAI (https://platform.openai.com/docs/guides/prompt-caching), Moonshot (https://www.kimi.com/help/kimi-api/api-pricing), and Anthropic (https://platform.claude.com/en/docs/build-with-claude/prompt-caching).
Character scalar in 'YYYY-MM-DD' format.
[prices_snapshot_stale()], [usage_cost()]
prices_snapshot_date()prices_snapshot_date()
Convenience wrapper over [prices_snapshot_date()] for staleness alerts, so callers don't repeat the date arithmetic. Offline only; it does not check the network for newer prices.
prices_snapshot_stale(max_age_days = 90)prices_snapshot_stale(max_age_days = 90)
max_age_days |
Numeric. Age threshold in days; default 90. |
'TRUE' when the bundled snapshot is older than 'max_age_days', otherwise 'FALSE'.
[prices_snapshot_date()]
prices_snapshot_stale() prices_snapshot_stale(max_age_days = 30)prices_snapshot_stale() prices_snapshot_stale(max_age_days = 30)
S3 print method for MCP connection objects.
## S3 method for class 'mcp_connection' print(x, ...)## S3 method for class 'mcp_connection' print(x, ...)
x |
An MCP connection object. |
... |
Unused. |
x, invisibly. Called for the side effect of printing a
summary of the connection state and available tools.
## Not run: conn <- mcp_connect(port = 7850) print(conn) mcp_close(conn) ## End(Not run)## Not run: conn <- mcp_connect(port = 7850) print(conn) mcp_close(conn) ## End(Not run)
Returns the model name 'chat()' falls back to when the caller doesn't specify one. Useful for client code that wants to display the resolved model upfront (e.g., in a status line) without duplicating the lookup table.
provider_default_model(provider)provider_default_model(provider)
provider |
Character. One of '"openai"', '"anthropic"', '"moonshot"', '"ollama"'. |
Character. The default model id for that provider.
provider_default_model("anthropic") provider_default_model("moonshot")provider_default_model("anthropic") provider_default_model("moonshot")
Computes the offline cost estimate for a usage object, the same value 'chat()' and 'agent()' attach as 'usage$cost'. Reads whichever shape the provider returned (Anthropic's 'input_tokens' / 'output_tokens', or the OpenAI-compatible 'prompt_tokens' / 'completion_tokens') and accounts for prompt caching: Anthropic cache writes/reads via published multipliers, OpenAI / Moonshot cache hits at the bundled per-model 'cache_read' rate. OpenAI cache hits are read from 'prompt_tokens_details$cached_tokens' on a raw response, falling back to a flat 'cache_read_input_tokens' field as exposed by 'agent()' aggregates, so an 'agent()$usage' object recomputes to the same cost it already carries.
usage_cost(model, provider, usage)usage_cost(model, provider, usage)
model |
Character. Model id as sent to the provider. |
provider |
Character. "anthropic", "openai", "moonshot", or "ollama". |
usage |
A usage list as found in 'chat()$usage' or 'agent()$usage'. |
Costs come from the bundled price snapshot, so they are offline, approximate, and may differ from current provider billing. See [prices_snapshot_date()].
Numeric scalar (USD), or 'NA_real_' when 'usage' is 'NULL', the model isn't in the snapshot, or cache reads can't be priced.
## Not run: r <- chat("hi", model = "claude-sonnet-4-6", cache = "5m") usage_cost("claude-sonnet-4-6", "anthropic", r$usage) ## End(Not run)## Not run: r <- chat("hi", model = "claude-sonnet-4-6", cache = "5m") usage_cost("claude-sonnet-4-6", "anthropic", r$usage) ## End(Not run)