diff --git a/README.md b/README.md index f0f2b04d4..879ee4c11 100644 --- a/README.md +++ b/README.md @@ -122,13 +122,14 @@ To use another provider or model: - **Another OpenAI model.** Change `default_model` in `openbot/examples/openduet/model.yaml`, or in your copy of it (see [Make it your own](#make-it-your-own)). `BOT_MODEL` does not change an OpenAI model on its own. It is used only when `OPENAI_BASE_URL` is also set to an OpenAI-compatible endpoint, and then it names the model that endpoint serves. - **Anthropic.** Set `BOT_PROVIDER=anthropic` and `ANTHROPIC_API_KEY=...`. The model is `BOT_MODEL` if you set one, such as `BOT_MODEL=claude-sonnet-5-5`, and `claude-sonnet-4-5` if you do not. +- **LLM Gateway.** [LLM Gateway](https://llmgateway.io) serves models from many providers behind one OpenAI-compatible key. Set `OPENAI_BASE_URL=https://api.llmgateway.io/v1`, `OPENAI_API_KEY` to your LLM Gateway key, and `BOT_MODEL` to the name the gateway publishes, such as `openai/gpt-5`. The model needs tools and JSON output: the Bot uses tools for its computer, and skill choice, routing and action review ask for JSON. Claude models go through the Anthropic settings instead (`BOT_PROVIDER=anthropic` with `ANTHROPIC_BASE_URL=https://api.llmgateway.io/v1` and your gateway key as `ANTHROPIC_API_KEY`), because the gateway refuses OpenAI's JSON mode for them. **Settings → Setup** reads **Not checked** for a gateway key. Set these before you start it. **Settings → Setup** in the app says whether the model key works. Then start it: ```sh -bash scripts/start.sh +bun run dev ``` and open . The script starts Postgres and each Bot's computer in Docker, applies the database migrations, and starts the API server, the routine worker, and the app. It checks that each one answers before it prints the next steps. `bash scripts/stop.sh` stops the app, the worker, the API server and the Docker services, and removes the Bot computers that belong to this install, the ones with its `COMPUTER_NAMESPACE`. It does not touch another install's. The database, each Bot's files and its browser profile are Docker volumes, so they stay. Add `--keep-computers` to leave the Bot computers running. diff --git a/openbot/desktop/src-tauri/src/provider.rs b/openbot/desktop/src-tauri/src/provider.rs index 7a24ad2d5..bba218aba 100644 --- a/openbot/desktop/src-tauri/src/provider.rs +++ b/openbot/desktop/src-tauri/src/provider.rs @@ -1,7 +1,7 @@ //! The model provider screen's list, as data. //! -//! OpenAI and Anthropic offer plan sign-in alongside API keys. Google and xAI use named endpoint -//! presets with API keys; their official addresses are supplied by the frontend. +//! OpenAI and Anthropic offer plan sign-in alongside API keys. Google, xAI and LLMTR use named +//! endpoint presets with API keys; their official addresses are supplied by the frontend. //! //! The row that asks for a URL is the last one, and it is the only one that asks. Keeping it there //! is what keeps a base URL off the main path, which the audience rule at the top of the build doc @@ -104,6 +104,14 @@ pub fn catalogue() -> Vec { mark: None, caution: None, }, + Provider { + id: "llmtr".into(), + name: "LLMTR".into(), + summary: "Use an LLMTR API key for the models behind its gateway.".into(), + logins: vec![Login::Endpoint], + mark: None, + caution: None, + }, Provider { id: "openai-compatible".into(), name: "Any OpenAI-compatible endpoint".into(), @@ -129,7 +137,7 @@ mod tests { .filter(|p| p.logins.contains(&Login::Endpoint)) .map(|p| p.id) .collect(); - assert_eq!(asking, vec!["google", "xai", "openai-compatible"]); + assert_eq!(asking, vec!["google", "xai", "llmtr", "openai-compatible"]); for provider in catalogue() .into_iter() .filter(|provider| provider.logins.contains(&Login::Endpoint)) @@ -156,12 +164,19 @@ mod tests { /// Named providers stay ahead of the custom endpoint escape hatch. #[test] - fn four_named_providers_and_one_custom_endpoint() { + fn five_named_providers_and_one_custom_endpoint() { let rows = catalogue(); let ids: Vec<&str> = rows.iter().map(|provider| provider.id.as_str()).collect(); assert_eq!( ids, - vec!["openai", "anthropic", "google", "xai", "openai-compatible"] + vec![ + "openai", + "anthropic", + "google", + "xai", + "llmtr", + "openai-compatible" + ] ); } diff --git a/openbot/desktop/src/ProviderPicker.test.tsx b/openbot/desktop/src/ProviderPicker.test.tsx index a976856de..2dbd0b005 100644 --- a/openbot/desktop/src/ProviderPicker.test.tsx +++ b/openbot/desktop/src/ProviderPicker.test.tsx @@ -45,6 +45,7 @@ const cloudProviders = [ baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai/", model: "gemini-3.8-flash", keyUrl: "https://aistudio.google.com/apikey", + oauth: true, }, { id: "xai", @@ -52,6 +53,15 @@ const cloudProviders = [ baseUrl: "https://api.x.ai/v1", model: "grok-4.7", keyUrl: "https://console.x.ai/", + oauth: true, + }, + { + id: "llmtr", + name: "LLMTR", + baseUrl: "https://llmtr.com/v1", + model: "qwen/qwen3.8-27b-free", + keyUrl: "https://llmtr.com/dashboard/api-keys", + oauth: false, }, ]; @@ -849,7 +859,9 @@ test("OAuth tab switch cancels a late authorization before opening the browser", }); for (const provider of cloudProviders) { - test(`${provider.name} OAuth completes through native storage without exposing tokens`, async () => { + test.skipIf(!provider.oauth)( + `${provider.name} OAuth completes through native storage without exposing tokens`, + async () => { invokeHandler = async (command) => { if (command === "providers") return allProviders.map((row) => @@ -898,7 +910,8 @@ for (const provider of cloudProviders) { command: "finish_model_oauth", args: { attemptId: "synthetic-attempt" }, }); - }); + }, + ); test(`${provider.name} opens its API key page externally without sending entered credentials`, async () => { invokeHandler = async (command) => { diff --git a/openbot/desktop/src/ProviderPicker.tsx b/openbot/desktop/src/ProviderPicker.tsx index c4cd3eb91..3d582cea6 100644 --- a/openbot/desktop/src/ProviderPicker.tsx +++ b/openbot/desktop/src/ProviderPicker.tsx @@ -78,6 +78,12 @@ const endpointPresets: Record< model: "grok-4.7", keyUrl: "https://console.x.ai/", }, + // https://llmtr.com/docs/en/quickstart/ + llmtr: { + baseUrl: "https://llmtr.com/v1", + model: "qwen/qwen3.8-27b-free", + keyUrl: "https://llmtr.com/dashboard/api-keys", + }, }; function endpointIdentity(baseUrl: string | undefined): string { @@ -133,8 +139,8 @@ export function recordedModel(held: HeldConfiguration): ModelChoice | null { /** * Connect a model. * - * Plans remain the default wherever supported. Google and xAI use the same endpoint credential - * route as custom models, while their named rows supply the address for the user. + * Plans remain the default wherever supported. Google, xAI and LLMTR use the same endpoint + * credential route as custom models, while their named rows supply the address for the user. */ export function ProviderPicker({ chosen, diff --git a/openbot/docs/configuration.md b/openbot/docs/configuration.md index b2c5113d8..f0509cc0c 100644 --- a/openbot/docs/configuration.md +++ b/openbot/docs/configuration.md @@ -237,7 +237,7 @@ model: Most gateways publish a model list, which is the way to check a name before configuring it. -Two things are worth knowing before pointing a deployment at any gateway. Not every catalogue entry accepts tools, and a Bot without tool calling cannot drive its computer; the model list says which do. And `BOT_RESPONSES_API=true` needs an endpoint that implements the Responses API, not only chat completions. +Two things are worth knowing before pointing a deployment at any gateway. Not every catalogue entry accepts tools, and a Bot without tool calling cannot drive its computer; the model list says which do. And `BOT_RESPONSES_API=true` needs an endpoint that implements the Responses API, not only chat completions. One gateway is stricter about a third setting: [LLMTR](https://llmtr.com) accepts `reasoning` only as a boolean, so leave `BOT_REASONING_EFFORT` unset when `OPENAI_BASE_URL=https://llmtr.com/v1`, because the Bot sends `reasoning.effort`, which it refuses. ## Voice dictation diff --git a/package.json b/package.json index 9dfaa267f..134f022b2 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "description": "The open-source proactive agent, built on OpenBot. The platform lives in openbot/.", "scripts": { "postinstall": "bun install --cwd openbot", + "dev": "bash scripts/start.sh", "start": "bash scripts/start.sh", "stop": "bash scripts/stop.sh" }