Skip to content
Merged
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
126 changes: 87 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# RouteBox

macOS menu bar app — LLM API proxy with intelligent routing, real-time monitoring, and cost tracking.
macOS menu bar app — LLM API proxy with intelligent routing, real-time monitoring, cost tracking, built-in chat, and web search.

**v2.3.0** · [Download DMG](https://github.com/createpjf/RouteBox/releases/latest) · MIT License

## What it does

Expand All @@ -9,9 +11,12 @@ RouteBox runs a local OpenAI-compatible proxy (`http://localhost:3001/v1`). You
1. **Routes** requests to the best provider based on your rules (cheapest, fastest, or smartest)
2. **Tracks** tokens, cost, latency, and savings in real-time
3. **Manages** multiple provider API keys securely in macOS Keychain
4. **Chat** directly with any model via the built-in Chat window or Spotlight
5. **Search** the web in real-time with Brave Search integration

```
Your App → RouteBox (localhost:3001) → OpenAI / Anthropic / Google / DeepSeek / MiniMax / Kimi / FLock.io
↑ Brave Search results injected as context
```

## Supported Providers
Expand All @@ -26,7 +31,66 @@ Your App → RouteBox (localhost:3001) → OpenAI / Anthropic / Google / Dee
| [Kimi](https://kimi.ai) | Kimi K2.5, Kimi K2, Moonshot | [platform.moonshot.ai](https://platform.moonshot.ai/) |
| [FLock.io](https://flock.io) | Qwen3-235B, Qwen3-30B, DeepSeek-V3.2, Kimi K2.5 | [platform.flock.io](https://platform.flock.io) |

> **Tip:** [FLock API Platform](https://platform.flock.io) provides access to open-source models (Qwen3, DeepSeek, Kimi)— a good option for cost-effective routing.
**Local models**: Ollama and LM Studio are auto-discovered on your network — no API key needed.

> **Tip:** [FLock API Platform](https://platform.flock.io) provides access to open-source models (Qwen3, DeepSeek, Kimi) — a good option for cost-effective routing.
## Features

### Three Windows

| Window | Shortcut | Description |
|--------|----------|-------------|
| **Panel** | `⌘⇧R` | Menu bar dashboard — routing, analytics, logs, settings |
| **Spotlight** | `⌘⇧Space` | Quick floating window — ask a question, get an instant answer |
| **Chat** | Via panel | Full chat interface with conversation history and sidebar |

### Dashboard Tabs

| Tab | What it shows |
|-----|---------------|
| **Dashboard** | Requests, tokens, cost, savings, traffic sparkline, provider status |
| **Routing** | Strategy selector, model preferences (pin/exclude), content-aware rules |
| **Logs** | Full request history with model, provider, latency, cost per request |
| **Analytics** | Charts for cost trends, provider latency, model usage breakdown |
| **My Usage** | Today/month usage, budget tracking, weekly trends, model breakdown |

### Web Search (Brave Search)

RouteBox can inject real-time web search results into any LLM conversation:

1. Go to **Settings → Web Search** and add your [Brave Search API key](https://brave.com/search/api) (free tier available)
2. Toggle the 🌐 button in Chat or Spotlight to enable search for a message
3. RouteBox searches the web, injects results as context, and the model responds with up-to-date information and source citations

### Intelligent Routing

| Strategy | Behavior |
|----------|----------|
| Smart Auto | AI picks the best route per request |
| Cost First | Always pick the cheapest provider |
| Speed First | Always pick the lowest latency provider |
| Quality First | Always pick the best available model |

### Routing Rules

| Rule Type | Triggers when... | Example use |
|-----------|-------------------|-------------|
| **Alias** | Model name matches your virtual name | `route-code``deepseek-coder` |
| **Code** | Request contains ≥3 code markers | Auto-route code tasks to DeepSeek |
| **Long** | Message ≥8,000 characters | Auto-route long context to Gemini |
| **General** | Catch-all fallback | Default model for everything else |

### Model Preferences

Pin a model to a specific provider, or exclude a provider for a model:

- **Pin**: `gpt-4o` → always use OpenAI (never fall back)
- **Exclude**: `gpt-4o` → never use provider X

### Thinking Model Support

Models that output `<think>` blocks (DeepSeek-R1, Qwen3, etc.) are automatically handled — thinking content is hidden behind a collapsible "💭 Thinking..." section.

## Setup

Expand Down Expand Up @@ -78,45 +142,11 @@ client = OpenAI(
)
```

## App Tabs

| Tab | What it shows |
|-----|---------------|
| **Dashboard** | Requests, tokens, cost, savings, traffic sparkline, provider status |
| **Routing** | Strategy selector, model preferences (pin/exclude), content-aware rules |
| **Logs** | Full request history with model, provider, latency, cost per request |
| **Analytics** | Charts for cost trends, provider latency, model usage breakdown |

## Routing

### Strategy

Pick one in the Routing tab:

| Strategy | Behavior |
|----------|----------|
| Smart Auto | AI picks the best route per request |
| Cost First | Always pick the cheapest provider |
| Speed First | Always pick the lowest latency provider |
| Quality First | Always pick the best available model |

### Rules

Create rules to route specific request types:
### 4. Enable Web Search (Optional)

| Rule Type | Triggers when... | Example use |
|-----------|-------------------|-------------|
| **Alias** | Model name matches your virtual name | `route-code``deepseek-coder` |
| **Code** | Request contains ≥3 code markers | Auto-route code tasks to DeepSeek |
| **Long** | Message ≥8,000 characters | Auto-route long context to Gemini |
| **General** | Catch-all fallback | Default model for everything else |
Go to **Settings → Web Search** → Paste your Brave Search API key → Save.

### Model Preferences

Pin a model to a specific provider, or exclude a provider for a model:

- **Pin**: `gpt-4o` → always use OpenAI (never fall back)
- **Exclude**: `gpt-4o` → never use provider X
Then toggle 🌐 in Chat or Spotlight before sending a message to include web results.

## Build DMG

Expand Down Expand Up @@ -146,6 +176,7 @@ docker build -t routebox-gateway .
docker run -p 3001:3001 \
-e OPENAI_API_KEY=sk-... \
-e ANTHROPIC_API_KEY=sk-ant-... \
-e BRAVE_API_KEY=BSA-... \
-v routebox-data:/data \
routebox-gateway
```
Expand All @@ -158,6 +189,7 @@ See [`apps/gateway/.env.example`](apps/gateway/.env.example) for all environment
|---------|----------|-------|
| Provider API Keys | Settings → Providers | Stored in macOS Keychain |
| Monthly Budget | Settings → Budget | Alerts at 80% and 100% |
| Web Search | Settings → Web Search | Brave Search API key for real-time search |
| Gateway URL | Settings → Connection | Default `http://localhost:3001`, customizable |
| Auth Token | Settings → Authentication | Auto-generated, stored in Keychain |
| Auto-start Gateway | Settings → Gateway | On/off toggle |
Expand All @@ -168,13 +200,29 @@ See [`apps/gateway/.env.example`](apps/gateway/.env.example) for all environment
| Shortcut | Action |
|----------|--------|
| `⌘⇧R` | Toggle panel (global) |
| `⌘⇧Space` | Toggle Spotlight (global) |
| `⌘C` | Copy API key |
| `⌘P` | Pause/resume traffic |
| `⌘⏎` | Send message (Spotlight) |
| `Esc` | Close Spotlight |

## Architecture

```
RouteBox/
├── apps/
│ ├── desktop/ Tauri v2 + React 19 (panel, spotlight, chat windows)
│ │ └── src-tauri/ Rust backend (system tray, global shortcuts, keychain)
│ └── gateway/ Bun + Hono (proxy, routing, analytics, search)
├── package.json pnpm monorepo root
└── README.md
```

## Tech Stack

- **Desktop**: Tauri v2 (Rust) + React 19 + TypeScript + Tailwind CSS v4
- **Gateway**: Bun + Hono + bun:sqlite
- **Search**: Brave Search API
- **Design**: SF Pro, frosted glass (macOS native)

## License
Expand Down
7 changes: 5 additions & 2 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@routebox/desktop",
"private": true,
"version": "1.2.0",
"version": "2.3.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -23,7 +23,10 @@
"lucide-react": "^0.460.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"recharts": "^2.15.0"
"react-markdown": "^9.1.0",
"recharts": "^2.15.0",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "routebox-desktop"
version = "1.2.0"
version = "2.3.0"
description = "RouteBox macOS Menu Bar App"
authors = ["RouteBox"]
edition = "2021"
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"global-shortcut:default",
"notification:default",
"store:default",
"process:default"
"process:default",
"updater:default"
]
}
58 changes: 57 additions & 1 deletion apps/desktop/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::keychain;
use tauri::Manager;
use tauri::{Emitter, Manager};
use tauri_plugin_positioner::{Position, WindowExt};
use arboard::Clipboard;
use std::sync::Mutex;
Expand Down Expand Up @@ -70,6 +70,62 @@ pub fn toggle_panel_internal(app: &tauri::AppHandle) -> Result<(), String> {
Ok(())
}

// ── Spotlight toggle ────────────────────────────────────────────────────────

#[tauri::command]
pub async fn toggle_spotlight(app: tauri::AppHandle) -> Result<(), String> {
toggle_spotlight_internal(&app)
}

pub fn toggle_spotlight_internal(app: &tauri::AppHandle) -> Result<(), String> {
if let Some(window) = app.get_webview_window("spotlight") {
if window.is_visible().unwrap_or(false) {
window.hide().map_err(|e| e.to_string())?;
} else {
window.center().map_err(|e| e.to_string())?;
window.show().map_err(|e| e.to_string())?;
window.set_focus().map_err(|e| e.to_string())?;
}
}
Ok(())
}

// ── Chat window ─────────────────────────────────────────────────────────────

#[tauri::command]
pub async fn open_chat(app: tauri::AppHandle) -> Result<(), String> {
if let Some(window) = app.get_webview_window("chat") {
window.show().map_err(|e| e.to_string())?;
window.set_focus().map_err(|e| e.to_string())?;
}
Ok(())
}

// ── Clipboard read ──────────────────────────────────────────────────────────

#[tauri::command]
pub async fn read_clipboard() -> Result<String, String> {
let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?;
clipboard.get_text().map_err(|e| e.to_string())
}

// ── Clipboard action (read + emit event + show spotlight) ───────────────────

#[tauri::command]
pub async fn clipboard_action(app: tauri::AppHandle, action: String) -> Result<(), String> {
clipboard_action_sync(&app, &action)
}

/// Synchronous version for use from shortcut handlers (no async runtime needed)
pub fn clipboard_action_sync(app: &tauri::AppHandle, action: &str) -> Result<(), String> {
let mut clipboard = Clipboard::new().map_err(|e| e.to_string())?;
let text = clipboard.get_text().unwrap_or_default();
app.emit("spotlight-action", serde_json::json!({ "action": action, "text": text }))
.map_err(|e: tauri::Error| e.to_string())?;
toggle_spotlight_internal(app)?;
Ok(())
}

// ── Gateway process management ──────────────────────────────────────────────

#[tauri::command]
Expand Down
59 changes: 52 additions & 7 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ pub fn run() {
window.on_window_event(move |event| {
if let tauri::WindowEvent::Focused(false) = event {
let win = w.clone();
// Small delay: if focus returns within 200ms (e.g. DevTools, HMR),
// don't hide. This prevents the panel flickering in dev mode.
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(200));
if !win.is_focused().unwrap_or(true) {
Expand All @@ -41,27 +39,70 @@ pub fn run() {
});
}

// Register global hotkey: Cmd+Shift+R
// Hide spotlight when it loses focus
if let Some(window) = app.get_webview_window("spotlight") {
let w = window.clone();
window.on_window_event(move |event| {
if let tauri::WindowEvent::Focused(false) = event {
let win = w.clone();
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(200));
if !win.is_focused().unwrap_or(true) {
let _ = win.hide();
}
});
}
});
}

// Register global hotkeys
#[cfg(desktop)]
{
use tauri_plugin_global_shortcut::{
Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState,
};

let shortcut =
let shortcut_panel =
Shortcut::new(Some(Modifiers::META | Modifiers::SHIFT), Code::KeyR);
let shortcut_spotlight =
Shortcut::new(Some(Modifiers::META | Modifiers::SHIFT), Code::KeyX);
let shortcut_translate =
Shortcut::new(Some(Modifiers::META | Modifiers::SHIFT), Code::KeyT);
let shortcut_summarize =
Shortcut::new(Some(Modifiers::META | Modifiers::SHIFT), Code::KeyS);
let shortcut_explain =
Shortcut::new(Some(Modifiers::META | Modifiers::SHIFT), Code::KeyE);

let handle = app.handle().clone();
let sc_panel = shortcut_panel.clone();
let sc_spotlight = shortcut_spotlight.clone();
let sc_translate = shortcut_translate.clone();
let sc_summarize = shortcut_summarize.clone();

app.handle().plugin(
tauri_plugin_global_shortcut::Builder::new()
.with_handler(move |_app, _shortcut, event| {
.with_handler(move |_app, shortcut, event| {
if event.state() == ShortcutState::Pressed {
let _ = commands::toggle_panel_internal(&handle);
if *shortcut == sc_panel {
let _ = commands::toggle_panel_internal(&handle);
} else if *shortcut == sc_spotlight {
let _ = commands::toggle_spotlight_internal(&handle);
} else if *shortcut == sc_translate {
let _ = commands::clipboard_action_sync(&handle, "translate");
} else if *shortcut == sc_summarize {
let _ = commands::clipboard_action_sync(&handle, "summarize");
} else {
let _ = commands::clipboard_action_sync(&handle, "explain");
}
}
})
.build(),
)?;
app.global_shortcut().register(shortcut)?;
app.global_shortcut().register(shortcut_panel)?;
app.global_shortcut().register(shortcut_spotlight)?;
app.global_shortcut().register(shortcut_translate)?;
app.global_shortcut().register(shortcut_summarize)?;
app.global_shortcut().register(shortcut_explain)?;
}

Ok(())
Expand All @@ -73,6 +114,10 @@ pub fn run() {
commands::copy_to_clipboard,
commands::show_notification,
commands::toggle_panel,
commands::toggle_spotlight,
commands::open_chat,
commands::read_clipboard,
commands::clipboard_action,
commands::spawn_gateway,
commands::stop_gateway,
commands::is_gateway_running,
Expand Down
Loading
Loading