Skip to content

Commit db11677

Browse files
createpjfclaude
andcommitted
feat: V2.3.0 — Brave Search, Chat window, thinking model support
New features: - Brave Search integration: real-time web search in Chat and Spotlight - Full Chat window with conversation history and sidebar - Thinking model support (DeepSeek-R1, Qwen3) with collapsible blocks - ModelSwitcher now only shows providers with active API keys - Auth token auto-loaded across all windows via Keychain Bug fixes: - Fix budget sync bug: setting key mismatch (monthly_budget → budgetMonthly) - Fix auth token not propagating to Chat/Spotlight windows Infrastructure: - Updated README with v2.3.0 features and Brave Search docs - Added search status/key management API endpoints - Search results injected as system context before LLM forwarding Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c0c9b90 commit db11677

25 files changed

+1044
-275
lines changed

README.md

Lines changed: 87 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# RouteBox
22

3-
macOS menu bar app — LLM API proxy with intelligent routing, real-time monitoring, and cost tracking.
3+
macOS menu bar app — LLM API proxy with intelligent routing, real-time monitoring, cost tracking, built-in chat, and web search.
4+
5+
**v2.3.0** · [Download DMG](https://github.com/createpjf/RouteBox/releases/latest) · MIT License
46

57
## What it does
68

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

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

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

29-
> **Tip:** [FLock API Platform](https://platform.flock.io) provides access to open-source models (Qwen3, DeepSeek, Kimi)— a good option for cost-effective routing.
34+
**Local models**: Ollama and LM Studio are auto-discovered on your network — no API key needed.
35+
36+
> **Tip:** [FLock API Platform](https://platform.flock.io) provides access to open-source models (Qwen3, DeepSeek, Kimi) — a good option for cost-effective routing.
37+
38+
## Features
39+
40+
### Three Windows
41+
42+
| Window | Shortcut | Description |
43+
|--------|----------|-------------|
44+
| **Panel** | `⌘⇧R` | Menu bar dashboard — routing, analytics, logs, settings |
45+
| **Spotlight** | `⌘⇧Space` | Quick floating window — ask a question, get an instant answer |
46+
| **Chat** | Via panel | Full chat interface with conversation history and sidebar |
47+
48+
### Dashboard Tabs
49+
50+
| Tab | What it shows |
51+
|-----|---------------|
52+
| **Dashboard** | Requests, tokens, cost, savings, traffic sparkline, provider status |
53+
| **Routing** | Strategy selector, model preferences (pin/exclude), content-aware rules |
54+
| **Logs** | Full request history with model, provider, latency, cost per request |
55+
| **Analytics** | Charts for cost trends, provider latency, model usage breakdown |
56+
| **My Usage** | Today/month usage, budget tracking, weekly trends, model breakdown |
57+
58+
### Web Search (Brave Search)
59+
60+
RouteBox can inject real-time web search results into any LLM conversation:
61+
62+
1. Go to **Settings → Web Search** and add your [Brave Search API key](https://brave.com/search/api) (free tier available)
63+
2. Toggle the 🌐 button in Chat or Spotlight to enable search for a message
64+
3. RouteBox searches the web, injects results as context, and the model responds with up-to-date information and source citations
65+
66+
### Intelligent Routing
67+
68+
| Strategy | Behavior |
69+
|----------|----------|
70+
| Smart Auto | AI picks the best route per request |
71+
| Cost First | Always pick the cheapest provider |
72+
| Speed First | Always pick the lowest latency provider |
73+
| Quality First | Always pick the best available model |
74+
75+
### Routing Rules
76+
77+
| Rule Type | Triggers when... | Example use |
78+
|-----------|-------------------|-------------|
79+
| **Alias** | Model name matches your virtual name | `route-code``deepseek-coder` |
80+
| **Code** | Request contains ≥3 code markers | Auto-route code tasks to DeepSeek |
81+
| **Long** | Message ≥8,000 characters | Auto-route long context to Gemini |
82+
| **General** | Catch-all fallback | Default model for everything else |
83+
84+
### Model Preferences
85+
86+
Pin a model to a specific provider, or exclude a provider for a model:
87+
88+
- **Pin**: `gpt-4o` → always use OpenAI (never fall back)
89+
- **Exclude**: `gpt-4o` → never use provider X
90+
91+
### Thinking Model Support
92+
93+
Models that output `<think>` blocks (DeepSeek-R1, Qwen3, etc.) are automatically handled — thinking content is hidden behind a collapsible "💭 Thinking..." section.
3094

3195
## Setup
3296

@@ -78,45 +142,11 @@ client = OpenAI(
78142
)
79143
```
80144

81-
## App Tabs
82-
83-
| Tab | What it shows |
84-
|-----|---------------|
85-
| **Dashboard** | Requests, tokens, cost, savings, traffic sparkline, provider status |
86-
| **Routing** | Strategy selector, model preferences (pin/exclude), content-aware rules |
87-
| **Logs** | Full request history with model, provider, latency, cost per request |
88-
| **Analytics** | Charts for cost trends, provider latency, model usage breakdown |
89-
90-
## Routing
91-
92-
### Strategy
93-
94-
Pick one in the Routing tab:
95-
96-
| Strategy | Behavior |
97-
|----------|----------|
98-
| Smart Auto | AI picks the best route per request |
99-
| Cost First | Always pick the cheapest provider |
100-
| Speed First | Always pick the lowest latency provider |
101-
| Quality First | Always pick the best available model |
102-
103-
### Rules
104-
105-
Create rules to route specific request types:
145+
### 4. Enable Web Search (Optional)
106146

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

114-
### Model Preferences
115-
116-
Pin a model to a specific provider, or exclude a provider for a model:
117-
118-
- **Pin**: `gpt-4o` → always use OpenAI (never fall back)
119-
- **Exclude**: `gpt-4o` → never use provider X
149+
Then toggle 🌐 in Chat or Spotlight before sending a message to include web results.
120150

121151
## Build DMG
122152

@@ -146,6 +176,7 @@ docker build -t routebox-gateway .
146176
docker run -p 3001:3001 \
147177
-e OPENAI_API_KEY=sk-... \
148178
-e ANTHROPIC_API_KEY=sk-ant-... \
179+
-e BRAVE_API_KEY=BSA-... \
149180
-v routebox-data:/data \
150181
routebox-gateway
151182
```
@@ -158,6 +189,7 @@ See [`apps/gateway/.env.example`](apps/gateway/.env.example) for all environment
158189
|---------|----------|-------|
159190
| Provider API Keys | Settings → Providers | Stored in macOS Keychain |
160191
| Monthly Budget | Settings → Budget | Alerts at 80% and 100% |
192+
| Web Search | Settings → Web Search | Brave Search API key for real-time search |
161193
| Gateway URL | Settings → Connection | Default `http://localhost:3001`, customizable |
162194
| Auth Token | Settings → Authentication | Auto-generated, stored in Keychain |
163195
| Auto-start Gateway | Settings → Gateway | On/off toggle |
@@ -168,13 +200,29 @@ See [`apps/gateway/.env.example`](apps/gateway/.env.example) for all environment
168200
| Shortcut | Action |
169201
|----------|--------|
170202
| `⌘⇧R` | Toggle panel (global) |
203+
| `⌘⇧Space` | Toggle Spotlight (global) |
171204
| `⌘C` | Copy API key |
172205
| `⌘P` | Pause/resume traffic |
206+
| `⌘⏎` | Send message (Spotlight) |
207+
| `Esc` | Close Spotlight |
208+
209+
## Architecture
210+
211+
```
212+
RouteBox/
213+
├── apps/
214+
│ ├── desktop/ Tauri v2 + React 19 (panel, spotlight, chat windows)
215+
│ │ └── src-tauri/ Rust backend (system tray, global shortcuts, keychain)
216+
│ └── gateway/ Bun + Hono (proxy, routing, analytics, search)
217+
├── package.json pnpm monorepo root
218+
└── README.md
219+
```
173220

174221
## Tech Stack
175222

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

180228
## License

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@routebox/desktop",
33
"private": true,
4-
"version": "2.0.1",
4+
"version": "2.3.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

apps/desktop/src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "routebox-desktop"
3-
version = "2.0.1"
3+
version = "2.3.0"
44
description = "RouteBox macOS Menu Bar App"
55
authors = ["RouteBox"]
66
edition = "2021"

apps/desktop/src-tauri/capabilities/default.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"global-shortcut:default",
1111
"notification:default",
1212
"store:default",
13-
"process:default"
13+
"process:default",
14+
"updater:default"
1415
]
1516
}

apps/desktop/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "RouteBox",
4-
"version": "2.0.1",
4+
"version": "2.3.0",
55
"identifier": "com.routebox.desktop",
66
"build": {
77
"beforeDevCommand": "pnpm dev",

apps/desktop/src/App.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, useCallback } from "react";
1+
import { useState, useEffect, useCallback, useRef } from "react";
22
import { Panel } from "@/components/Panel";
33
import { ErrorBoundary } from "@/components/ErrorBoundary";
44
import { HeroSection } from "@/components/HeroSection";
@@ -152,6 +152,25 @@ export function App() {
152152
}
153153
}, [connected, stats, onboardingDismissed, showSettings]);
154154

155+
// Auto-check for updates on startup (once, 5s delay)
156+
const updateCheckedRef = useRef(false);
157+
useEffect(() => {
158+
if (updateCheckedRef.current || gatewayState !== "running") return;
159+
updateCheckedRef.current = true;
160+
const timer = setTimeout(async () => {
161+
try {
162+
const { check } = await import("@tauri-apps/plugin-updater");
163+
const update = await check();
164+
if (update) {
165+
showToast(`New version available! Open Settings → About to update.`, "info", 8000);
166+
}
167+
} catch {
168+
// Silent — update check is best-effort
169+
}
170+
}, 5000);
171+
return () => clearTimeout(timer);
172+
}, [gatewayState, showToast]);
173+
155174
const handleDismissOnboarding = useCallback(async () => {
156175
setShowOnboarding(false);
157176
setOnboardingDismissed(true);

apps/desktop/src/components/ProviderKeyManager.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export function ProviderKeyManager({ onProvidersChanged }: ProviderKeyManagerPro
1919
const [successProvider, setSuccessProvider] = useState<string | null>(null);
2020
const [editingLocalUrl, setEditingLocalUrl] = useState<string | null>(null);
2121
const [localUrlInput, setLocalUrlInput] = useState("");
22+
const [localApiKeyInput, setLocalApiKeyInput] = useState("");
2223
const [refreshingLocal, setRefreshingLocal] = useState<string | null>(null);
2324

2425
const fetchRegistry = useCallback(async () => {
@@ -85,18 +86,20 @@ export function ProviderKeyManager({ onProvidersChanged }: ProviderKeyManagerPro
8586
setSaving(true);
8687
setError(null);
8788
try {
88-
const updated = await api.setLocalProviderUrl(name, localUrlInput.trim());
89+
const apiKeyArg = localApiKeyInput.trim() || undefined;
90+
const updated = await api.setLocalProviderUrl(name, localUrlInput.trim(), apiKeyArg);
8991
setLocalProviders((prev) =>
9092
prev.map((lp) => lp.name === name ? { ...lp, ...updated } : lp)
9193
);
9294
setEditingLocalUrl(null);
9395
setLocalUrlInput("");
96+
setLocalApiKeyInput("");
9497
} catch (err) {
9598
setError(err instanceof Error ? err.message : "Failed to update URL");
9699
} finally {
97100
setSaving(false);
98101
}
99-
}, [localUrlInput]);
102+
}, [localUrlInput, localApiKeyInput]);
100103

101104
if (loading) {
102105
return (
@@ -142,6 +145,7 @@ export function ProviderKeyManager({ onProvidersChanged }: ProviderKeyManagerPro
142145
if (!isEditingUrl) {
143146
setEditingLocalUrl(lp.name);
144147
setLocalUrlInput(lp.baseUrl);
148+
setLocalApiKeyInput("");
145149
setError(null);
146150
}
147151
}}
@@ -154,6 +158,11 @@ export function ProviderKeyManager({ onProvidersChanged }: ProviderKeyManagerPro
154158
/>
155159
<span className="text-[13px] text-text-primary flex-1">{lp.name}</span>
156160
<div className="flex items-center gap-1.5">
161+
{lp.hasApiKey && (
162+
<span title="API key configured">
163+
<Key size={10} strokeWidth={1.75} className="text-text-tertiary/60" />
164+
</span>
165+
)}
157166
{lp.isOnline ? (
158167
<span className="text-[11px] text-text-tertiary">
159168
{lp.modelCount} model{lp.modelCount !== 1 ? "s" : ""}
@@ -188,13 +197,14 @@ export function ProviderKeyManager({ onProvidersChanged }: ProviderKeyManagerPro
188197
setLocalUrlInput(e.target.value);
189198
setError(null);
190199
}}
191-
placeholder={`Base URL (e.g. http://localhost:11434/v1)`}
200+
placeholder={`Base URL (e.g. http://192.168.1.100:1234/v1)`}
192201
className="input input-mono flex-1 !h-8 !text-[11px]"
193202
autoFocus
194203
onKeyDown={(e) => {
195204
if (e.key === "Enter") handleSaveLocalUrl(lp.name);
196205
if (e.key === "Escape") {
197206
setEditingLocalUrl(null);
207+
setLocalApiKeyInput("");
198208
setError(null);
199209
}
200210
}}
@@ -214,6 +224,21 @@ export function ProviderKeyManager({ onProvidersChanged }: ProviderKeyManagerPro
214224
)}
215225
</button>
216226
</div>
227+
<input
228+
type="password"
229+
value={localApiKeyInput}
230+
onChange={(e) => setLocalApiKeyInput(e.target.value)}
231+
placeholder={lp.hasApiKey ? "API Key (configured — leave blank to keep)" : "API Key (optional)"}
232+
className="input input-mono w-full !h-8 !text-[11px] mt-1.5"
233+
onKeyDown={(e) => {
234+
if (e.key === "Enter") handleSaveLocalUrl(lp.name);
235+
if (e.key === "Escape") {
236+
setEditingLocalUrl(null);
237+
setLocalApiKeyInput("");
238+
setError(null);
239+
}
240+
}}
241+
/>
217242
{error && editingLocalUrl === lp.name && (
218243
<div className="flex items-center gap-1.5 mt-1.5">
219244
<AlertCircle size={12} strokeWidth={1.75} className="text-accent-red shrink-0" />

0 commit comments

Comments
 (0)