Chat Options, Conversations, and Errors

This vignette shows you how to control a chat with a model from an R script. The server is the program of LM Studio that answers the requests of R. You pick a route, the format in which R sends a request to the server. You set request options, the settings that change how the model answers. You continue a conversation and read the whole reply of the server. You also catch some errors, such as a missing server or a refused request, so that such a call does not stop your script.

vignette("getting-started") covers what comes first: installing LM Studio, starting the server, and downloading google/gemma-3-1b.

Start the server and load the model

Start the server, and load the model into memory.

library(rlmstudio)

model <- "google/gemma-3-1b"

# Start the server, and wait up to about 30 seconds for it to answer
lms_server_start(wait = 30)
#> ✔ LM Studio server started successfully on the default port.

# Load the model
lms_load(model)
#> ℹ Loading model: "google/gemma-3-1b"...
#> ✔ Model "google/gemma-3-1b" loaded and verified. [11.7s]
#> 

Three routes

LM Studio answers chat requests in three formats, and each one is a route. OpenResponses and OpenAI are formats that other programs use too. The native format is LM Studio’s own. On every route, the request goes to LM Studio on your computer. The api_type argument of lms_chat() picks the route.

The table below shows what lms_chat() supports on each route. A response id is a name that the server gives to a reply. By default, the server keeps the reply, so that a later call can continue from it. Log probabilities say how likely the model found each piece of its reply. A schema makes the reply follow a fixed shape. ?lms_chat explains the ttl argument.

api_type Continue by response id Log probabilities schema and ttl
"openresponses", the default Yes Yes No
"openai" No No Yes
"native" Yes No No

The default route covers most uses. Pick "openai" for a schema or a ttl. The OpenAI route also takes a whole conversation in a data frame, through lms_chat_openai(), as a later section shows. The native route refuses an option name that it does not know, as the last section shows. vignette("text-analysis") shows log probabilities and a schema.

Here is the same prompt on each route. A prompt is the text that you send to the model.

prompt <- "Name one color. Answer with one word."

# The default route
lms_chat(model, prompt)
#> [1] "Blue \n\nLet me know if you’d like another!"
#> attr(,"response_id")
#> [1] "resp_5cf482638f60b6dee10cb5fd6c4b254663ab888f1d48e295"

# The OpenAI route
lms_chat(model, prompt, api_type = "openai")
#> [1] "Blue."

# The native route of LM Studio
lms_chat(model, prompt, api_type = "native")
#> [1] "Blue."
#> attr(,"response_id")
#> [1] "resp_c42e41ec31f20b2124f9fc2e06ccd48f9e9ea586e5e550e7"

Each call returns the text of the reply. An attribute is a named value that R attaches to an object, and attr() reads it. On the default route and the native route, the text also carries a response_id attribute.

Request options

A request is made of fields. A field is one named value, such as the model name or the prompt. A request option is a field that changes how the model answers. lms_chat() has no argument for most of them. You add each one to the call by its name, and lms_chat() puts it in the request under that name. An option that you set to NULL is left out of the request. The LM Studio developer documentation at https://lmstudio.ai/docs/developer lists the options of each route.

The model writes its reply in tokens. A token is a short piece of text, such as a word or a part of a word. The temperature option sets how much chance goes into the choice of each token. The call below sets it to 0.

# Set the temperature to 0
lms_chat(model, prompt, temperature = 0)
#> [1] "Blue."
#> attr(,"response_id")
#> [1] "resp_c858e32caaa1abf0bb6b46a3f92492027bcf6b14b50a69cb"

The package checks a few option names. For example, it stops with an error on a stream other than FALSE or NULL, because it reads a whole reply and not one sent in parts. It also stops on instructions on the default route and on messages on the OpenAI route, because lms_chat() fills those fields itself. A misspelled name such as temprature goes to the server with no check by the package.

The server checks the names, and the routes do not check them in the same way. On the default route, the server ignores a name that it does not know. The call below misspells temperature, and it still returns a reply with no error.

# "temprature" is not an option that the server knows
lms_chat(model, prompt, temprature = 0)
#> [1] "Blue."
#> attr(,"response_id")
#> [1] "resp_e9fa3986708ece2925883ed9f7e59999867016bfe21ca658"

So a misspelled option can go unnoticed. A later section shows how to see the temperature that the server used. The last section shows what the native route does with the same call.

A follow-up question

On the default route and the native route, the text of a reply carries its response id in the response_id attribute. Pass the reply itself as previous_response_id in your next call. The package sends the id from the attribute, and the server sends the earlier prompt and reply to the model with your new prompt. The id string from attr(first, "response_id") also works.

Each call below sets temperature = 0, as in the section “Request options”.

# Tell the model a fact
first <- lms_chat(
  model,
  "My favorite color is green. Reply with OK.",
  temperature = 0
)
first
#> [1] "OK."
#> attr(,"response_id")
#> [1] "resp_342a9e50660c1e6ca25912df33559f6f0c9520f5a5012d3b"

# Ask about the fact in a follow-up
lms_chat(
  model,
  "What is my favorite color? Answer with one word.",
  previous_response_id = first,
  temperature = 0
)
#> [1] "Green."
#> attr(,"response_id")
#> [1] "resp_f9f176cf59816d6b3f8c602dfcb91df493e0338c7d8b01ea"

# Ask the same question with no response id
lms_chat(
  model,
  "What is my favorite color? Answer with one word.",
  temperature = 0
)
#> [1] "Blue."
#> attr(,"response_id")
#> [1] "resp_91e29038937144cf4478592485332f092e67d5cf7a5c997d"

With the first reply as previous_response_id, the model answered from the first prompt. With no response id, the model did not see the first prompt, and its answer was a guess.

The OpenAI route has no response id. With api_type = "openai", lms_chat() stops with an error if you give previous_response_id.

A whole conversation

Conversation history is the list of the earlier messages of a chat. On the OpenAI route, a reply has no response id, so you send the whole history with each request. lms_chat() with api_type = "openai" sends a history of two messages at most: the system prompt, which is a set of instructions for the model, and your prompt. To send a longer history, call lms_chat_openai(), the function behind that route. It takes the history in its messages argument, as a data frame with one row for each message. The role column says who wrote the message, and the content column holds the text. The roles are:

You write the history yourself, so an assistant row can hold a reply that the model never gave.

history <- data.frame(
  role = c("system", "user", "assistant", "user"),
  content = c(
    "You answer in one short sentence.",
    "My favorite color is green.",
    "Green is a nice color.",
    "What is my favorite color?"
  )
)
history
#>        role                           content
#> 1    system You answer in one short sentence.
#> 2      user       My favorite color is green.
#> 3 assistant            Green is a nice color.
#> 4      user        What is my favorite color?

# Send the whole conversation
reply <- lms_chat_openai(model, messages = history, temperature = 0)
reply
#> [1] "Your favorite color is green."

To continue the conversation, add the reply and your next prompt to the data frame as new rows, and send it again. A reply can start or end with spaces or line breaks, and trimws() removes them.

# Add the reply and a new prompt as two rows
history <- rbind(
  history,
  data.frame(
    role = c("assistant", "user"),
    content = c(trimws(reply), "Write my favorite color in capital letters.")
  )
)

# Send the longer conversation
lms_chat_openai(model, messages = history, temperature = 0)
#> [1] "GREEN!"

The new prompt does not name the color, so the model read it from the history.

The raw reply

The server sends its reply in JSON, a text format for data. The raw reply is that whole reply, read into R as a list, with one element for each field. With simplify = FALSE, lms_chat() returns the raw reply in place of the text. The fields differ from route to route. On the default route, the text of the reply is inside the output field.

raw <- lms_chat(model, prompt, temperature = 0, simplify = FALSE)

# The fields of the raw reply
names(raw)
#>  [1] "id"                   "object"               "created_at"          
#>  [4] "completed_at"         "status"               "incomplete_details"  
#>  [7] "model"                "previous_response_id" "instructions"        
#> [10] "output"               "error"                "tools"               
#> [13] "tool_choice"          "truncation"           "parallel_tool_calls" 
#> [16] "text"                 "top_p"                "presence_penalty"    
#> [19] "frequency_penalty"    "top_logprobs"         "temperature"         
#> [22] "reasoning"            "usage"                "max_output_tokens"   
#> [25] "max_tool_calls"       "store"                "background"          
#> [28] "service_tier"         "metadata"             "safety_identifier"   
#> [31] "prompt_cache_key"

# The temperature that the server used
raw$temperature
#> [1] 0

The raw reply holds more than the text, such as the temperature that the server used. Here is the call with the misspelled option from the section “Request options”.

raw_typo <- lms_chat(model, prompt, temprature = 0, simplify = FALSE)

# The temperature that the server used
raw_typo$temperature
#> [1] 0.8

The server did not use a temperature of 0. It ignored temprature and used a temperature that the call did not set.

Errors in a script

When a call fails, R creates a condition, an object that describes the error. A condition class is a name that says what kind of error a condition is. tryCatch() can run different code for each condition class.

Two more terms come up in these errors. The host argument of each chat function is the address of the server. Its default, "http://localhost:1234", is port 1234 of your own computer. A port is a number that picks one program on a computer. With each reply, the server also sends an HTTP status, a number that says how the request went. A status of 400 or more is an error. 404 says that the server did not find what the request named, and 400 says that the request was wrong in another way.

The package uses these classes for a failed chat:

The function below calls lms_chat(). If the call works, the function returns the reply. If the call fails with one of these classes, the function prints a message and returns NA. A handler is a function that tryCatch() runs for one condition class. tryCatch() runs the first handler whose class matches, so the handler for rlmstudio_model_mismatch comes before the handler for rlmstudio_bad_response.

In a loop over many prompts, such a function keeps a failed call of these classes from stopping the loop. Other errors still stop it. For example, the package stops on a wrong argument before it sends the request, and that error has none of these classes. A server that stops during a request raises an error of the class httr2_failure, which also stops the loop.

# Chat, and return NA with a message if the call fails
chat_or_na <- function(...) {
  tryCatch(
    lms_chat(...),
    rlmstudio_no_server = function(cnd) {
      message("No server answered.")
      NA_character_
    },
    rlmstudio_api_error = function(cnd) {
      message("The server refused the call: status ", cnd$status, ".")
      if (!is.null(cnd$code)) {
        message("The error code is ", cnd$code, ".")
      }
      NA_character_
    },
    rlmstudio_model_mismatch = function(cnd) {
      message("The reply came from ", cnd$reply_model,
              ", not from ", cnd$model, ".")
      NA_character_
    },
    rlmstudio_bad_response = function(cnd) {
      message("The reply could not be read.")
      NA_character_
    }
  )
}

# A call that works returns the reply
chat_or_na(model, prompt, temperature = 0)
#> [1] "Blue."
#> attr(,"response_id")
#> [1] "resp_220281674e43b24eb1d488881bae74895a8bd66ea524b3c3"

The first call below goes to port 1, where no server runs. The other calls misspell the model name or an option.

# No server listens at this address
chat_or_na(model, prompt, host = "http://localhost:1")
#> No server answered.
#> [1] NA

# A misspelled model name on the native route
chat_or_na("google/gemma-3-1bb", prompt, api_type = "native")
#> The server refused the call: status 404.
#> The error code is model_not_found.
#> [1] NA

# The same misspelled model name on the default route
chat_or_na("google/gemma-3-1bb", prompt)
#> The reply came from google/gemma-3-1b, not from google/gemma-3-1bb.
#> [1] NA

# A misspelled option on the native route
chat_or_na(model, prompt, api_type = "native", temprature = 0)
#> The server refused the call: status 400.
#> The error code is unrecognized_keys.
#> [1] NA

The native route refused the misspelled model name and the misspelled option. The code unrecognized_keys says that the request held a field name that the server does not know.

On the default route, with one model loaded as here, the server did not refuse the misspelled model name. It answered with the model that was loaded, and the package raised rlmstudio_model_mismatch when it read the reply.

Clean up

When you are done, unload the model to free its memory, and stop the server. lms_server_stop() stops the server even if it ran before this vignette.

# Remove the model from memory
lms_unload(model)
#> ℹ Unloading model: "google/gemma-3-1b"...
#> ✔ Model "google/gemma-3-1b" unloaded successfully. [525ms]
#> 

# Stop the local server
lms_server_stop()
#> ✔ LM Studio server stopped successfully.