Changelog
Source:NEWS.md
rlmstudio (development version)
lms_load()now checks the shape of a status-200 load reply before it reads it. The reply must be a JSON object whosestatusis the string"loaded". Withecho_load_config = TRUE, itsload_configmust also be a JSON object, and the call returns it as a list. Any other reply aborts withrlmstudio_bad_response, and the message names the field that broke the rule. Fields are read by their exact names. Before,{}and a status such as"pending"aborted withrlmstudio_api_errorand status 200. A body such as"x"failed with an unclassed error. A field such asstatusXwas read in place ofstatus. Withecho_load_config = TRUE, a reply with noload_configreturnedNULL.lms_download()now checks the shape of a status-200 reply before it reads it. The reply must be a JSON object whosestatusis a string. If the status is"already_downloaded", the call returns"already_downloaded"invisibly, as before. Otherwise the reply’sjob_idmust be a string, and the call returns it. Any other reply aborts withrlmstudio_bad_response, and the message names the field that broke the rule. The call no longer returnsTRUE. Before,{},[], andnullreturnedTRUE, ajob_idof1returned1L, and ajob_idthat was an array returned a list.lms_download_status()now checks the shape of a status-200 reply before it reads it. The reply must be a JSON object whosejob_idandstatusare strings. Itstotal_size_bytes,downloaded_bytes, andbytes_per_secondmust each be a number, or absent, ornull. Any other reply aborts withrlmstudio_bad_response, and the message names the field that broke the rule. Before,{}returned an empty status object, and printing it failed with “EXPR must be a length 1 vector”.print()on a download status now reads each field by its exact name. Before, a field such asbytes_per_secondXwas read in place of an absentbytes_per_second. A string there madeprint()fail with “non-numeric argument to binary operator”.print()also shows the status as text now. Before, braces in the status were run as R code, so a status of"{1 + 1}"printed as2.list_models()now checks the shape of a status-200 model list before it reads it. A model list that breaks a rule aborts withrlmstudio_bad_response, and the message names the field or entry that broke it. The body must be a JSON object whosemodelsis an array. Each model must be a JSON object whosetypeandkeyare strings and whoseloaded_instancesis an array. Asize_bytesmust be a number when it is present and notnull. Each loaded instance must be a JSON object whoseidis a string that is not empty or blank. Fields are read by their exact names. The check covers every entry, also the entries that thetypeandloadedfilters drop.lms_unload_all(), andlms_load()withoutforce = TRUE, raise the condition throughlist_models()and send no other request. Before,{}gave an empty data frame, a field such asmodelsXwas read in place ofmodels, and some bad fields, such as a missingkeyortype, were read without error. Some other bodies failed with a base R error, such as “$ operator is invalid for atomic vectors” or “missing value where TRUE/FALSE needed”.list_models()no longer fails on a model list with no models in it, which is what a server with nothing downloaded returns. It returns an empty data frame and, unlessquiet = TRUE, says that no models were found on the host. Before, it failed with “missing value where TRUE/FALSE needed”. Through it,lms_unload_all()failed the same way, and so didlms_load()withoutforce = TRUE. Nowlms_unload_all()reports that no models are loaded, andlms_load()loads the model.lms_unload_all()now reads each loaded instance by itsidfield alone. Before, it read anidentifierfield first, thenid, then the first field, and it skipped an id that wasNAor empty.lms_server_ready()now returnsTRUEonly for a model list thatlist_models()can read. It applies the same rules. Before, it accepted any array of JSON objects undermodels, such as models without atypeor akey.A reply body is now read as JSON text and nothing else. Before, the package fetched a body whose text was a URL that starts with
http://orhttps://. It read a body whose text was the path of an existing file from disk. For a file that held a valid reply,list_models(),lms_embed(), and the chat functions returned the content of that file. If the reply header saidapplication/json,lms_load(),lms_download(), andlms_download_status()did the same. Now such a body does not parse, so the functions that read a status-200 body abort withrlmstudio_bad_response, andlms_server_ready()returnsFALSE. An error body of that kind is shown as its own text in therlmstudio_api_errormessage.A status-200 body that does not parse as JSON now aborts with
rlmstudio_bad_responseinlist_models(),lms_load(),lms_download(), andlms_download_status().lms_unload_all()raises it throughlist_models(), and so doeslms_load()unlessforce = TRUE. The condition’sstatusis200L. After a label, the message says what the message of the chat functions says. The label is the function’s own. If the model list is the body that fails,lms_unload_all()andlms_load()show the label oflist_models(), “API List Failed”. Before, such a body failed with an unclassed error from httr2 or jsonlite.lms_load(),lms_download(), andlms_download_status()now read a status-200 body by its content, not by itsContent-Typeheader. Valid JSON undertext/plainnow gives the same result as underapplication/json. Before, it failed with an unclassed httr2 error.lms_server_ready()now returnsTRUEfor a model list sent astext/plain, where before it returnedFALSE. Anrlmstudio_api_errorwhose JSON body comes astext/plainnow shows the error text from the body, where before it showed the whole body.A status-200 reply body that does not parse as JSON now aborts with
rlmstudio_bad_responseinlms_chat_native(),lms_chat_openresponses(), andlms_chat_openai(). Examples are an HTML page from a proxy, JSON text that stops part way, and an empty body. The abort happens withsimplify = TRUEand withsimplify = FALSE. The condition’sstatusis200L. The message says that the body did not parse as JSON. It also says that something other than LM Studio can be answering on the host. It does not hold the body text. On the OpenAI route, the condition’scontentandfinish_reasonfields areNULL. Before, such a body failed with an unclassed error from httr2 or jsonlite. Inlms_chat_batch(), that error ended the batch and lost every reply so far. Now that input fails alone, and the batch stores the failure and warns once, as for other failed inputs.The three chat functions now parse a status-200 body by its content, not by its
Content-Typeheader. Valid JSON undertext/plainnow gives the same result as underapplication/json. Before, it failed with an unclassed httr2 error.lms_embed()already worked this way, and its behavior does not change.With
api_type = "openresponses"orapi_type = "openai"andformat = "data.frame",lms_chat_batch()now returns four more columns at the end of the data frame:response_id,input_tokens,total_output_tokens, andreasoning_output_tokens. They carry the names of the first four native columns, so batches from the three routes share them.response_idis the reply’sid. The counts come from the reply’susageobject. The OpenResponses route readsinput_tokens,output_tokens, andoutput_tokens_details.reasoning_tokens. The OpenAI route readsprompt_tokens,completion_tokens, andcompletion_tokens_details.reasoning_tokens. The columns are there withlogprobs = TRUEand with aschematoo.response_idis character, and the counts are double. If a field is absent or is not one value of the column type, its cell isNA, with no warning. Ifusageis not a JSON object, all three count cells areNA. The row of an input that failed holdsNAin all four columns. Apart from the bare-value change below, the answer columns, the vector and list formats, and single calls return what they did before.A 200 response body can be a bare JSON value, such as
5,"s", ortrue. Withsimplify = TRUE, such a body now aborts withrlmstudio_bad_responseinlms_chat_native(),lms_chat_openresponses(), andlms_chat_openai(). The message says that the body is not a JSON object. Before, such a body failed with the base R error “subscript out of bounds”. Inlms_chat_batch(), that error ended the batch and lost every reply so far. Now the batch stores the failure and goes on. Withsimplify = FALSE, the body comes back unchanged, as before.With
api_type = "native"andformat = "data.frame",lms_chat_batch()now returns seven more columns at the end of the data frame. The first isresponse_id, the id of the reply on the server. The other six come from thestatsobject of the reply. They areinput_tokens,total_output_tokens,reasoning_output_tokens,tokens_per_second,time_to_first_token_seconds, andmodel_load_time_seconds.response_idis character, and the other six are double. If a field is absent or has the wrong type, its cell isNA, with no warning. The answer inoutputis kept. The row of an input that failed holdsNAin all seven columns. If every input failed, the columns are still there. The other routes, the other formats, and single calls return what they did before.With
simplify = TRUEandlogprobs = TRUE,lms_chat_openresponses()now checks thelogprobsvalue of each"output_text"part before it builds the data frame. The value must be an array of JSON objects. In each object,tokenmust be a string andlogprobmust be a number. Itstop_logprobsmust be an array of objects whosetokenandlogprobfollow the same two rules. Each of these fields, and thelogprobsvalue itself, can also benullor absent. Anullstep or candidate breaks a rule. A value that breaks a rule aborts withrlmstudio_bad_response, and the message names the first rule it breaks. The parts, the steps of a part, and the candidates of a step are checked one at a time, in order. Within a step,token,logprob, and the array test oftop_logprobscome before the candidates. A badtextin any part is reported before anylogprobsfault. Before, a value such as[5]failed with the bare R error “$ operator is invalid for atomic vectors”. Parts of other types, such as a refusal, are not checked. Withlogprobs = FALSE, the call returns the text as before.lms_chat_batch()stores this failure as it stores other failed inputs.The fields of each step and candidate are now read by their exact names. Before, a step with a
tokenXfield and notokenfield gave the value oftokenXas its token. Now it givesNA, as anullor absent field does. A readablelogprobsvalue gives the same data frame as before.lms_chat_native()andlms_chat_openresponses()now read the answer from the reply items of type"message". Before, they read the first item of theoutputarray. A reasoning model puts a reasoning item first, so the call returned the reasoning in place of the answer. The text of every message item is now pasted together in order, and reasoning items, tool calls, and other items are skipped. On the OpenResponses route, the text comes from each part of type"output_text", and a part of another type, such as a refusal, is skipped. Withlogprobs = TRUE, the log probabilities come from every such part in order.With
simplify = TRUE, those two functions now abort withrlmstudio_bad_responsewhen a reply holds no readable answer text. That covers anoutputfield that is missing, empty, or not an array, and a reply with no message item, such as one that holds only reasoning or a tool call. It also covers message text that is not one string. Before, some of these replies failed with a bare R error such as “subscript out of bounds”. Others returnedNULLor a value that was not a string.simplify = FALSEstill returns the body unchanged.With
simplify = TRUE,lms_chat_openai()now aborts withrlmstudio_bad_responsewhen the reply content is not one string. That includesnullcontent, which a reply that holds only a tool call has. The condition carries the content and the finish reason in itscontentandfinish_reasonfields. Before,nullcontent returnedNULL, and withlogprobs = TRUEit failed with an unclassed error. Content such as a number or an array was returned as it was.lms_chat_batch()stores these failures as it stores other failed inputs, and warns once. Withformat = "data.frame"andsimplify = FALSE, it now aborts before it sends any request. Before, it sent every request first and then aborted, and the replies were lost.lms_chat_openai()has a newschemaargument for structured output. Give it a JSON Schema written as a named list. The request then asks the server for a reply that matches the schema. Withsimplify = TRUE, the call returns the reply parsed into an R value, for examplelist(score = 3L), rather than a string. Withsimplify = FALSEorlogprobs = TRUE, the reply stays a string. A plain vector of length one is sent as a single value. Write a JSON array of one item as a list, such asrequired = list("score").lms_chat()takesschematoo and passes it tolms_chat_openai(). It needsapi_type = "openai", because LM Studio documents structured output on that endpoint only. Any otherapi_typewith aschemaaborts before the request.lms_chat_batch()passesschemaon through.... Whensimplify = TRUEandlogprobs = FALSE, the reply is parsed. In that case,format = "list"returns one parsed reply per input. Withformat = "data.frame", theoutputcolumn is a list of parsed replies. Withformat = "vector", the call warns and returns the list.A
schemamust be a named list, an empty list, orNULL. Any other value aborts before the request, and so does aschemagiven together with aresponse_formatin.... An empty object nested in the schema, such asproperties, is writtensetNames(list(), character()), becauselist()is sent as the empty array[].The reply is parsed only with a
schema,simplify = TRUE, andlogprobs = FALSE. There, reply content that is not one string of valid JSON aborts with the condition classrlmstudio_bad_response.lms_chat_openai()now raises it, aslms_embed()does, andlms_chat()passes it on. The condition carries the reply content in acontentfield and the server’s finish reason in afinish_reasonfield. You can therefore read what the model wrote without a second request. If the finish reason is"length", the message says that the token limit cut the reply off and namesmax_tokens.With
simplify = TRUE, a chat completions response fromlms_chat_openai()whosechoicesfield is missing, empty, or not an array, or whose first element is not a JSON object with amessageobject in it, also aborts withrlmstudio_bad_response, with or without aschema. Before, an emptychoiceslist failed with the bare errorsubscript out of bounds. Without aschemaand withlogprobs = FALSE, a missingchoicesfield returnedNULL. The condition’scontentandfinish_reasonfields areNULL. Without aschema,lms_chat_batch()now stores such a response as a failed input, as the next entry says. Before, a missingchoicesfield gaveNULLfor that input. Withformat = "vector", the result was then shorter than the input.lms_chat_batch()no longer stops at an input that fails withrlmstudio_api_errororrlmstudio_bad_response, whatever its settings. It goes on to the next input. In a returned list, and in theoutputlist-column that aschemagives, the element for that input holds the condition. Where the result is text, the element holdsNA. That is the case forformat = "vector"when it returns a vector, and for a data frame whose replies are not parsed. In such a data frame, thelogprobscolumn holdsNULLfor a failed input. A reply with no readable answer text, such as one whose content isnull, fails in the same way, so the result stays as long asinputs. Withformat = "data.frame"andlogprobs = TRUE, thelogprobscolumn is now always there, even when no reply carried log probabilities. Before, it was left out in that case. The call gives one warning that names the count and the positions of the failed inputs.quiet = TRUEdoes not silence it. Where the result holdsNA, the warning namesformat = "list"as the way to keep the conditions.An
rlmstudio_no_serverfrom one input still abortslms_chat_batch(), but the replies so far are no longer lost. The condition carries them in a newresultsfield, with any failures stored so far. It is a list as long asinputs, withNULLfrom the lost input on. This covers a server that the check before each input finds gone. A connection that fails during a request still aborts with anhttr2_failureerror and noresultsfield.lms_server_start()now waits for the REST API to answer before it returns. The CLI reports success as soon as it asks for the server. The REST API accepts a request some time after that. A script that called the API on the next line therefore reported a missing server on a healthy machine. The call now askslms_server_ready()again and again until it reportsTRUE. It pauses a quarter of a second between requests. Once the wait passes it starts no new request. After the firstTRUEit sends no further request.A new
waitargument onlms_server_start()sets how many seconds that lasts. It defaults to 10. It is not a hard cap. No new request starts once the wait passes, but a request already in flight is allowed one second to finish. With nohostand noport, the call first asks the CLI which port the server uses, and that read runs before the wait starts to count. Withwait = 0the call sends no readiness request and returns as soon as the CLI does, which is what every earlier version did. Awaitthat is not one number, zero or more, aborts before the CLI runs.NULL, a string, a vector of two,NA,NaN,Inf, and a negative number all abort. The message names the argument and the rule the value broke.A wait that runs out raises a warning rather than an error, and the call still returns the CLI exit code. Aborting cannot undo a server that already started. The warning names the host it asked and the
waitargument. Therlmstudio.quietoption does not silence it.A new
hostargument onlms_server_start()says where to look for the server that was started. It does not change where the CLI starts it, which onlyportdoes. When you give nohost, the host ishttp://localhost:plus theportyou gave. When you give neither, the port thatlms_server_status(json = TRUE)reports is read instead. A call that reaches that last case and finds no usable port sends no readiness request. It raises its own warning instead. That warning names the failed port read, thehostargument, and theportargument.A new
tokenargument onlms_server_start()is sent on the readiness request. It serves a server that requires authentication. Withtoken = NULL, the request reads therlmstudio.tokenoption and then theRLMSTUDIO_API_TOKENenvironment variable.lms_server_start()now checkshostandtokenbefore the CLI runs. A fault in either one therefore aborts before any server starts. Atokenthat is not one character string and notNULLaborts. Ahostthat the readiness request cannot be built from also aborts. The message nameshostand quotes the reason from httr2 or curl. Some examples are a vector of two strings,NA, an empty string, and a number. A URL with a space in it and"localhost:1234", which lackshttp://, abort too. Both checks also run withwait = 0.If the readiness check aborts during the wait,
lms_server_start()raises one warning and returns the CLI exit code. The warning names the host and quotes the abort message.The help page of
lms_server_ready()now names the call faults that abort it rather than returningFALSE. There are six. AhostofNULL. Ahostof more than one string. Ahostthat is a characterNA. Ahostthat cannot be parsed as a URL. Atimeoutbelow one millisecond. Atimeoutthat is not one number. The page says which package each message comes from. The httr2 messages nameurlandsecondsrather thanhostandtimeout, and the page says so. The curl message for ahostthat cannot be parsed as a URL names no argument. It names the parse reason instead. The page also says that those six are not the whole list, because anyhostthat is not one string aborts the same way. Thetokenfault the page already named is unchanged.A model name or a job id that the server cannot use now aborts before the request goes out. The message names the argument and the rule the value broke. The check covers
modelonlms_chat(),lms_chat_batch(),lms_chat_native(),lms_chat_openai(),lms_chat_openresponses(),lms_download(),lms_embed(),lms_load(), andlms_unload(). It coversjob_idonlms_download_status(). Each of these must be one name, given as a single string. These all abort: a vector of two names, an empty character vector,NA, an empty string, and a string of whitespace only. So do a one-by-one matrix,NULL, and any value that is not a character string. Before, eight of these functions had no check onmodelat all. A vector of two names went to those as a JSON array of two. The error that came back named neither the argument nor the mistake.lms_load()was the one exception. If a model was already loaded, it raised a bare R error from its already-loaded test instead.A missing value inside a text argument now aborts.
lms_embed(input)andlms_chat_batch(inputs)must hold at least one value and noNA. Onlms_chat(),lms_chat_native(), andlms_chat_openresponses(), a characterinputmust hold noNA. A value of any other type still passes through to the server on those three. The structured input form that the OpenResponses endpoint accepts therefore still works. Two things stay unchecked.lms_chat_openai(messages)takes a list, which no rule here reaches. On the three chat wrappers above, a characterinputof length zero still goes out as an empty JSON array. Before, anNAwent out as JSONnull. The server then answered with an error or a count that named neither theNAnor its position.Two of these messages changed text.
lms_download()andlms_download_status()used to say “You must provide a valid model identifier or URL.” and “You must provide a valid job_id.”. Those two checks also raised a bare R error, rather than their own message, when the argument held more than one value.A new function,
lms_embed(), turns text into the vectors that an embedding model produces for it. Give it a loaded embedding model and a character vector of texts. The whole vector goes out in one request to the server. By default the return is a numeric matrix. It has one row per input text and one column per embedding dimension, so it goes straight todist()orprcomp(). The matrix carries no row or column names, so rows pair with inputs by position. The row at each position holds the vector that the response reported for the text at that position, whatever order the server answered in. Withsimplify = FALSEthe return is the parsed response body instead. Atokenargument works as it does on the other functions that reach the REST API. The...argument forwards any other field to the request body.lms_embed()reads the response before it builds the matrix, and aborts rather than returning a matrix it cannot trust. It aborts on each of these. The response body is not JSON at all. The response body is a plain value rather than an object or an array. The response carries no list of vectors, which is also what a body sent as a bare array reports, because such a body can carry no named field. The list of vectors is a plain value rather than a list. The list of vectors arrived as a JSON object rather than an array. It carries a different number of vectors than there were texts. A vector carries no position, or a position that is not a whole number. Two vectors carry the same position. A position falls outside the range of the texts. A vector is not a list of numbers. A vector is an empty list. An entry in the list of vectors is not an object at all. The vectors are of unequal length. These aborts carry the new condition classrlmstudio_bad_responseand astatusfield holding the HTTP status as an integer. Every message but one namessimplify = FALSE, which returns the body unchanged so you can read what arrived. The exception is a body that is not JSON: the parse runs beforesimplifyis read, so that message instead tells you to check what is answering on the host.The help pages now document a third condition class.
rlmstudio_bad_responsehas its own section on therlmstudio-conditionspage and its own alias, so?rlmstudio_bad_responsereaches it. The same section appears on thelms_embed()page. The class means a response that the server did not report as a failure and that the package still cannot read. That is a different thing fromrlmstudio_api_error.A new function,
lms_server_ready(), reports whether a host answers as an LM Studio server you can use. It sends one GET request to the model list endpoint and returnsTRUEonly for an HTTP 200 whose body carries a list of models. An empty list counts, because a fresh install has nothing downloaded and its server still works. Every other answer returnsFALSE: a refused connection, a listener that never replies, a rejected token, a failed status, and a body that is not a model list. The function raises nothing of its own for any of these, so a caller can branch on the value directly. Atimeoutargument sets how many seconds to wait, and defaults to 2. Atokenargument works as it does on the other functions that reach the REST API. Atokenthat is not one character string and notNULLstill aborts.The help page for the error conditions now says what the server check actually reads. Functions that call the REST API open a TCP connection to the host and port. Any process holding that port accepts the connection, so
rlmstudio_no_serveris not raised even though no LM Studio server is there. The call then fails later as anrlmstudio_api_erroror as a raw parse error. The page nameslms_server_ready()as the stronger test. The same section appears on the help page of every exported function that can raise the condition.Both vignettes now ask whether the server answers before they call it. A host that does not answer makes them skip their REST examples. Before, a server that was not usable made the vignette fail to build.
The package can now authenticate to an LM Studio server that requires an API token. Eleven functions take a
tokenargument. They arelist_models(),lms_load(),lms_unload(),lms_unload_all(),lms_download(),lms_download_status(),lms_chat(),lms_chat_batch(),lms_chat_openresponses(),lms_chat_openai(), andlms_chat_native(). Each one sends the value as a bearer token in theAuthorizationheader of every request it makes. Whentokenis not given, the package reads therlmstudio.tokenoption, and then theRLMSTUDIO_API_TOKENenvironment variable. When none of the three holds a value, the request carries noAuthorizationheader. A new help topic,rlmstudio_token, is the source. On the nine of these functions that take...,tokensits after the dots. It is therefore matched by name alone, and no existing argument moved position.A printed request shows the
Authorizationheader as<REDACTED>rather than showing the token. The package never puts the token into the message of a failed REST call. That message does repeat the text the server sent. An R backtrace also repeats your own calling line. Neither one is under the package’s control.A
tokenargument that is not one character string and notNULLnow aborts. Before, a value such as a vector of two strings was discarded without a message. The call then fell through to the option and the environment variable.A REST response with HTTP status 401 or 403 now adds a hint to the abort. If the request carried no token, the hint names
RLMSTUDIO_API_TOKENand thetokenargument. If the request carried a token, the hint says that the server rejected it. Other statuses gain no hint.The help pages now document the error condition classes that this package raises. A new help topic,
rlmstudio-conditions, is the source. It names the situation that raisesrlmstudio_no_serverand the situation that raisesrlmstudio_api_error. It states that anrlmstudio_api_errorcondition carries astatusfield, which holds the HTTP status as an integer. It shows how to catch each class withtryCatch(). The same two sections now appear on the help page of every exported function that can raise one of these classes. The pages oflms_chat(),lms_chat_batch(), andlms_unload_all()also name the function that they reach the abort through.Every failed REST response now aborts through one path. This affects
lms_load(),lms_unload(),lms_download(),lms_download_status(),lms_chat_openresponses(),lms_chat_openai(), andlms_chat_native(). All seven now report the same message text for the same response body. Every one of these aborts carries the condition classrlmstudio_api_error. It also carries astatusfield, which holds the HTTP status as an integer.lms_unload_all()andlms_chat_batch()call these functions, so their failures change in the same way. The aborts fromlms_download()andlms_download_status()no longer print the call that raised them. The other five already suppressed it.Some failure messages changed as a result. A JSON body whose
errorfield is a plain string now reports that string. Before,lms_load(),lms_unload(),lms_download(), andlms_download_status()printed the whole raw body, andlms_chat_openresponses()andlms_chat_openai()printed the status. A JSON body that carries no readable message now reportsHTTP Status <n>. Before, such a body gave the raw body text, a fragment of theerrorobject, or a crash, depending on the function and the shape. A body that is not JSON and holds text now reports that text at all seven functions. Before, the three chat functions printed the status instead. An empty body still reportsHTTP Status <n>. Iflms_load()gets a successful response that does not report the model as loaded, it now reports the response body.Response bodies that used to crash now abort with a message. Before, all seven functions raised a raw R error on at least one body shape. An
errorfield holding an empty object, an empty array, or an array of strings raised errors such asargument is of length zero. Atlms_load(),lms_unload(),lms_download(), andlms_download_status(), an empty response body raisedCan't retrieve empty body.list_models()now reports a failed REST response through that same path. Before, it raised the raw httr2 error, such asHTTP 400 Bad Request. It now aborts with the messageAPI List Failed:followed by the text read out of the response body. The abort carries the condition classrlmstudio_api_error. It also carries astatusfield, which holds the HTTP status as an integer. If the status is 400 or above and the body carries no readable message, the abort reportsHTTP Status <n>. Below status 400 it reports the body text instead. The call now aborts on every status other than 200. Before, only status 400 and above raised an error, so a status such as 201 or 302 reached the parser.The server probe now honors the
hostargument. This affectslist_models(),lms_load(),lms_unload(),lms_unload_all(),lms_download(),lms_download_status(),lms_chat_openai(),lms_chat_openresponses(),lms_chat_native(), andlms_chat_batch(). Before, the probe always triedlocalhost:1234, so a server on another hostname or port was reported as not running.If the server is not running,
list_models(),lms_download(), andlms_download_status()now abort instead of returning an empty data frame orNULL. Every server-down abort carries the condition classrlmstudio_no_server.has_lms()now uses the same lookup aslms_path(): theRLMSTUDIO_LMS_PATHenvironment variable, then the systemPATH, then common installation directories. Before, it checked only thePATH, so the two functions did not always agree.