--- title: "Language models (your own key) and the signed services document" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Language models (your own key) and the signed services document} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} # the vignette runs in full in GitHub CI and for the package tarball (NOT_CRAN), on r-universe # (MY_UNIVERSE), on the package website (IN_PKGDOWN) and in an interactive session; when CRAN # re-builds it in its check, none of these is set and only this chunk runs if (!interactive() && !any(nzchar(Sys.getenv(c("NOT_CRAN", "MY_UNIVERSE", "IN_PKGDOWN"))))) { knitr::knit_exit() } # the one-time index record of this vignette stays out of the reader's configuration directory options(rmoriebricklayer.xmssidxfile = tempfile()) knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(rmoriebricklayer) ``` `llmask()` sends a question to a language model and returns the answer. You bring the model: a key for a provider you already use, a model on your own machine, or a server of your own. Nothing is sent anywhere you did not set up, and the package runs no model service of its own. This vignette explains the routes, how the model is given the packages' help, and what the package will and will not do on the network. ## The route order 1. **An endpoint of your own**: `MORIE_LLM_BASE_URL` (with `MORIE_LLM_API_KEY` and `MORIE_LLM_MODEL`), any OpenAI-compatible server (LM Studio, vLLM, llama.cpp's `llama-server`, a company gateway). Taken when configured, not probed. 2. **A provider whose key is set**, in the order `llmprvd()` lists them: OpenAI, Anthropic, Google Gemini, OpenRouter, Groq, Together AI, Fireworks AI, DeepSeek, Mistral, xAI, Cerebras, Perplexity, Hugging Face, OpenCode Zen, Cohere and NVIDIA. Each reads its usual variable (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, ...) or a key saved with `llmcfg(anthropic.key = ...)`; the model is `MORIE__MODEL` or `.model`. Anthropic's Messages API and Gemini's `generateContent` are spoken natively; the others are OpenAI-compatible. 3. **Ollama** at `OLLAMA_HOST` (default `localhost:11434`), on this machine or another, once its server is running (`ollama serve`, the Ollama app, or the system service its Linux installer sets up) and has a model (`ollama pull NAME`), or Ollama's own cloud API (`ollama.url = https://ollama.com` with your `ollama.key`); `OLLAMA_HOST=off` disables the probe. Its first listed model is the default. `route = "anthropic"` (or `rmbl config set route anthropic`) takes one route only. ```{r language-model-routes-providers} llmprvd()[, c("id", "label", "key_env")] ``` `llmstat()` reports what is available from this machine. With the local route off the call reaches no network: ```{r language-model-routes-status} old <- Sys.getenv("OLLAMA_HOST", unset = NA) Sys.setenv(OLLAMA_HOST = "off") llmstat() if (is.na(old)) Sys.unsetenv("OLLAMA_HOST") else Sys.setenv(OLLAMA_HOST = old) ``` ## Asking, with the packages' help at hand ```{r language-model-routes-ask, eval = FALSE} llmask("How do I estimate a weighted proportion with bootstrap replicate weights?") llmask(paste(c("Summarise this table:", capture.output(print(head(mtcars)))), collapse = "\n"), syspro = "Answer in two sentences.", docs = FALSE) ``` With `docs = TRUE` (the default) the model is told every function of the MORIE packages installed here (rmoriebricklayer, and rmoriedata and rmorie when present), read from their installed help, and gets five tools it can call: `search_docs` (a search over every help page), `get_help`, `get_examples`, `list_datasets` and `get_vignette`. It reads the pages it needs before it writes code. A model that does not take tools gets the pages nearest the question in its prompt instead. The tools only read help; nothing the model says is run. How much of the help goes into the prompt follows what the model can take in: * a provider's model, a model Ollama serves from its cloud (`minimax-m3:cloud` reports a 512,000-token context) and a remote server of your own get the whole index of functions, full help pages and up to six rounds of lookups; * a model Ollama runs on this machine works in Ollama's context window (4,096 tokens unless the server was started with `OLLAMA_CONTEXT_LENGTH`), so it gets the functions nearest the question, shorter pages and fewer rounds; a server of your own on this machine gets a middle size; * `MORIE_LLM_CONTEXT` (tokens) says what your model takes, whatever the route. Every size can still reach every function: `search_docs` and `get_help` look up any page. `agentbndl()` asks over the same routes, with the bundle task added, and with no route it returns the setup hint (`rmbl bundle` exits 1). ## The signed services document The data service's addresses and its sign-in service are **not** hard-coded in the package. They come from a document at `https://rmorie.com/.well-known/morie-services.json`, signed with ML-DSA-44 under a key pinned in the package, so the endpoints can change without a release while no one but the project can change them. A copy ships with the package as the fallback and the floor: ```{r language-model-routes-services} s <- svcs(offline = TRUE) s$data$mode attr(s, "source") ``` What the verifier enforces, each with a test: * the signature must verify under the pinned key, or the document is ignored; * a validly signed but **older** document than the bundled copy is refused: a retired document in a cache (or served live) can never pin the endpoint a key would be sent to; * the cache serves only when it is newer than the bundled document, and an older cache is deleted; * a mirror without a `.json` suffix finds its `.sig`; `MORIE_SERVICES_URL` names a mirror. ## What the package will not do on the network The 0.5.8 to 0.5.10 reviews closed every bypass of the transport and these rules now hold for every byte the package sends or receives: * An endpoint of your own is either the loopback host, a literal private LAN address (`10/8`, `172.16/12`, `192.168/16`, `fc00::/7`, for an LM Studio or vLLM box on your own network) over plain http, or a public https address. Link-local addresses and host names over plain http are refused, and a redirect from a remote origin into the loopback interface is refused. * Request headers do not follow a redirect to another origin (scheme, host or effective port) unless they are content negotiation; `Authorization`, `X-Api-Key` and the rest stay with the first origin. * **Keys are redacted by value**: a gateway's error text has the key that was sent, any `Bearer` token and any `sk-` key removed before it becomes an R condition. * Bytes a server sent are parsed as text, never as a second address. * A download under 64 bytes/s for 30 s is ended. A model call is not: a model may think for a minute before its first byte, so a call has one deadline, the `timeout` (120 s), a conversation with help-page lookups three times that, and Ctrl-C ends the wait at once without sending the prompt again. An in-memory reply past 16 MiB is dropped; the services document and JSON fetches cap at 1 MiB and 16 MiB. * The sign-in service's `verification_uri` is opened in a browser only when it is a public https address. ## The data key Signing in is for the curated tables at `data.rmorie.com` only; it is never a language model. ```{r language-model-routes-login, eval = FALSE} dlogin(token = "") dlogin() # or: GitHub device flow / emailed code dlogout() # forget the key ``` The key is stored with mode 0600 in the user's configuration directory and never written anywhere else. `rmbl doctor` says whether one is stored. ## From the command line The same routes serve the CLI installed by `instcli()`: ```sh rmoriebricklayer config setup # pick a provider, paste its key (saved privately), pick a model rmoriebricklayer doctor # the routes, and the one ask takes rmoriebricklayer models --route anthropic rmoriebricklayer ask "what does cpsbndl() cover?" rmoriebricklayer ask --route ollama --model gemma3:4b "how do I verify a capsule?" rmoriebricklayer login --token # the data key: paste it when asked; it never sits in the shell history ``` ## Asking about the Indigenous tables With a route set up, a question can name the package's Indigenous data; the model answers with the functions to call (this needs a model, so it is not run here): ```{r language-model-routes-indigenous-llm, eval = FALSE} llmask(paste("How do I compare perceived mental health of First Nations,", "Metis and Inuit adults with non-Indigenous adults, using", "indiex('health_indicators')?")) ``` ## The providers in a picture The routes above are drawn from the provider table `llmprvd()`. The bars count the providers by the shape of request each one takes; every OpenAI-compatible provider shares one request builder. ```{r language-model-routes-providers-plot, fig.width = 6, fig.height = 4} pv <- llmprvd() barplot(table(pv$api), col = blpal()[1], ylab = "providers", main = "Providers by request shape") ```