Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <http://localhost:3010>. 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.
Expand Down
25 changes: 20 additions & 5 deletions openbot/desktop/src-tauri/src/provider.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -104,6 +104,14 @@ pub fn catalogue() -> Vec<Provider> {
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(),
Expand All @@ -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))
Expand All @@ -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"
]
);
}

Expand Down
17 changes: 15 additions & 2 deletions openbot/desktop/src/ProviderPicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,23 @@ const cloudProviders = [
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai/",
model: "gemini-3.8-flash",
keyUrl: "https://aistudio.google.com/apikey",
oauth: true,
},
{
id: "xai",
name: "xAI",
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,
},
];

Expand Down Expand Up @@ -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) =>
Expand Down Expand Up @@ -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) => {
Expand Down
10 changes: 8 additions & 2 deletions openbot/desktop/src/ProviderPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion openbot/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down