# OCP LAN Mode — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Enable OCP to serve Claude API over LAN with flexible auth (none/shared-key/multi-key) and per-key usage tracking with a web dashboard. **Architecture:** Add LAN bind support (`0.0.0.0`), a key management layer with 3 auth modes, a SQLite-backed usage tracker, and an embedded single-file HTML dashboard. All changes in `server.mjs` + new `keys.mjs` module + CLI extensions in `ocp`. **Tech Stack:** Node.js built-in `node:crypto`, `better-sqlite3` (single native dependency) for persistent usage tracking, inline HTML/CSS/JS for dashboard. --- ## File Structure | File | Action | Responsibility | |------|--------|----------------| | `server.mjs` | Modify | Add LAN bind, auth middleware, usage recording hooks, dashboard/key API endpoints | | `keys.mjs` | Create | Key management + SQLite usage store (CRUD keys, record usage, query stats) | | `dashboard.html` | Create | Single-file embedded web dashboard (HTML+CSS+JS, served inline) | | `ocp` | Modify | Add `ocp keys`, `ocp lan`, `ocp usage --by-key` CLI commands | | `setup.mjs` | Modify | Add LAN mode config prompts during setup | | `package.json` | Modify | Add `better-sqlite3` dependency, bump version to 3.4.0 | --- ### Task 1: Add `better-sqlite3` dependency and version bump **Files:** - Modify: `package.json` - [ ] **Step 1: Install better-sqlite3** ```bash cd /Users/taodeng/.openclaw/projects/claude-proxy npm install better-sqlite3 ``` - [ ] **Step 2: Bump version to 3.4.0** In `package.json`, change: ```json "version": "3.4.0", ``` - [ ] **Step 3: Commit** ```bash git add package.json package-lock.json git commit -m "chore: add better-sqlite3 dependency, bump to v3.4.0" ``` --- ### Task 2: Create `keys.mjs` — Key management + usage store **Files:** - Create: `keys.mjs` This module handles: - SQLite database initialization (`~/.ocp/ocp.db`) - API key CRUD (create, list, revoke) - Usage recording (per-request: key, model, tokens, elapsed) - Usage queries (by key, by time range, aggregated) - [ ] **Step 1: Create keys.mjs with database init and key CRUD** ```javascript // keys.mjs — API key management and usage tracking for OCP LAN mode import Database from "better-sqlite3"; import { randomBytes } from "node:crypto"; import { join } from "node:path"; import { mkdirSync } from "node:fs"; import { homedir } from "node:os"; const OCP_DIR = join(homedir(), ".ocp"); mkdirSync(OCP_DIR, { recursive: true }); const DB_PATH = join(OCP_DIR, "ocp.db"); let db; export function getDb() { if (!db) { db = new Database(DB_PATH); db.pragma("journal_mode = WAL"); db.pragma("foreign_keys = ON"); initSchema(); } return db; } function initSchema() { db.exec(` CREATE TABLE IF NOT EXISTS api_keys ( id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT UNIQUE NOT NULL, name TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), revoked INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS usage_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, key_id INTEGER, key_name TEXT NOT NULL DEFAULT 'anonymous', model TEXT NOT NULL, prompt_chars INTEGER NOT NULL DEFAULT 0, response_chars INTEGER NOT NULL DEFAULT 0, elapsed_ms INTEGER NOT NULL DEFAULT 0, success INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY (key_id) REFERENCES api_keys(id) ); CREATE INDEX IF NOT EXISTS idx_usage_created ON usage_log(created_at); CREATE INDEX IF NOT EXISTS idx_usage_key ON usage_log(key_id); `); } // ── Key CRUD ── export function createKey(name) { const key = "ocp_" + randomBytes(24).toString("base64url"); const d = getDb(); const stmt = d.prepare("INSERT INTO api_keys (key, name) VALUES (?, ?)"); const result = stmt.run(key, name); return { id: result.lastInsertRowid, key, name }; } export function listKeys() { const d = getDb(); return d.prepare( "SELECT id, key, name, created_at, revoked FROM api_keys ORDER BY created_at DESC" ).all().map(k => ({ ...k, keyPreview: k.key.slice(0, 8) + "..." + k.key.slice(-4), })); } export function revokeKey(idOrName) { const d = getDb(); const stmt = d.prepare( "UPDATE api_keys SET revoked = 1 WHERE (id = ? OR name = ?) AND revoked = 0" ); return stmt.run(idOrName, idOrName).changes > 0; } export function validateKey(key) { const d = getDb(); const row = d.prepare( "SELECT id, name FROM api_keys WHERE key = ? AND revoked = 0" ).get(key); return row || null; } // ── Usage recording ── export function recordUsage({ keyId, keyName, model, promptChars, responseChars, elapsedMs, success }) { const d = getDb(); d.prepare(` INSERT INTO usage_log (key_id, key_name, model, prompt_chars, response_chars, elapsed_ms, success) VALUES (?, ?, ?, ?, ?, ?, ?) `).run(keyId ?? null, keyName || "anonymous", model, promptChars, responseChars, elapsedMs, success ? 1 : 0); } // ── Usage queries ── export function getUsageByKey({ since, until } = {}) { const d = getDb(); let where = "WHERE 1=1"; const params = []; if (since) { where += " AND created_at >= ?"; params.push(since); } if (until) { where += " AND created_at <= ?"; params.push(until); } return d.prepare(` SELECT key_name, COUNT(*) as requests, SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successes, SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as errors, SUM(prompt_chars) as total_prompt_chars, SUM(response_chars) as total_response_chars, SUM(elapsed_ms) as total_elapsed_ms, AVG(elapsed_ms) as avg_elapsed_ms, MIN(created_at) as first_request, MAX(created_at) as last_request FROM usage_log ${where} GROUP BY key_name ORDER BY requests DESC `).all(...params); } export function getUsageTimeline({ keyName, hours = 24 } = {}) { const d = getDb(); const since = new Date(Date.now() - hours * 3600000).toISOString(); let where = "WHERE created_at >= ?"; const params = [since]; if (keyName) { where += " AND key_name = ?"; params.push(keyName); } return d.prepare(` SELECT strftime('%Y-%m-%dT%H:00:00', created_at) as hour, COUNT(*) as requests, SUM(prompt_chars) as prompt_chars, SUM(response_chars) as response_chars, AVG(elapsed_ms) as avg_elapsed_ms FROM usage_log ${where} GROUP BY hour ORDER BY hour `).all(...params); } export function getRecentUsage(limit = 50) { const d = getDb(); return d.prepare(` SELECT key_name, model, prompt_chars, response_chars, elapsed_ms, success, created_at FROM usage_log ORDER BY created_at DESC LIMIT ? `).all(limit); } export function closeDb() { if (db) { db.close(); db = null; } } ``` - [ ] **Step 2: Verify module loads correctly** ```bash node -e "import('./keys.mjs').then(k => { console.log('OK'); k.closeDb(); })" ``` Expected: `OK` — database file created at `~/.ocp/ocp.db` - [ ] **Step 3: Commit** ```bash git add keys.mjs git commit -m "feat: add keys.mjs — API key management and SQLite usage store" ``` --- ### Task 3: Add LAN bind + auth modes to `server.mjs` **Files:** - Modify: `server.mjs:28-36` (env vars section) - Modify: `server.mjs:77-95` (configuration section) - Modify: `server.mjs:1017-1036` (HTTP server + auth middleware) - Modify: `server.mjs:1188-1206` (server.listen) Three auth modes controlled by `CLAUDE_AUTH_MODE`: - `none` — no auth (LAN trust mode) - `shared` — single `PROXY_API_KEY` (existing behavior) - `multi` — per-user keys managed via `keys.mjs` - [ ] **Step 1: Add new imports and env vars at top of server.mjs** After the existing imports (line 35), add: ```javascript import { validateKey, recordUsage, getUsageByKey, getUsageTimeline, getRecentUsage, createKey, listKeys, revokeKey, closeDb } from "./keys.mjs"; ``` Add new config vars after line 95 (`BREAKER_HALF_OPEN_MAX`): ```javascript // ── LAN mode configuration ── const BIND_ADDRESS = process.env.CLAUDE_BIND || "127.0.0.1"; const AUTH_MODE = process.env.CLAUDE_AUTH_MODE || (PROXY_API_KEY ? "shared" : "none"); // AUTH_MODE: "none" (open), "shared" (single PROXY_API_KEY), "multi" (per-user keys from keys.mjs) const ADMIN_KEY = process.env.OCP_ADMIN_KEY || ""; ``` - [ ] **Step 2: Replace auth middleware in HTTP server (lines 1027-1036)** Replace the existing Bearer token auth block with: ```javascript // ── Auth middleware ── // /health is always public. Admin endpoints require admin key in multi mode. const isPublicEndpoint = req.url === "/health"; let authKeyName = "local"; let authKeyId = null; if (!isPublicEndpoint) { const auth = req.headers["authorization"] || ""; const token = auth.startsWith("Bearer ") ? auth.slice(7) : ""; if (AUTH_MODE === "shared") { // Single shared key mode (backwards compatible) if (PROXY_API_KEY) { const tokenBuf = Buffer.from(token); const keyBuf = Buffer.from(PROXY_API_KEY); if (tokenBuf.length !== keyBuf.length || !timingSafeEqual(tokenBuf, keyBuf)) { return jsonResponse(res, 401, { error: { message: "Unauthorized: invalid or missing Bearer token", type: "auth_error" } }); } authKeyName = "shared"; } } else if (AUTH_MODE === "multi") { // Multi-key mode: validate against keys.mjs database if (!token) { return jsonResponse(res, 401, { error: { message: "Unauthorized: Bearer token required", type: "auth_error" } }); } // Check if it's the admin key if (ADMIN_KEY && token === ADMIN_KEY) { authKeyName = "admin"; } else { const keyInfo = validateKey(token); if (!keyInfo) { return jsonResponse(res, 401, { error: { message: "Unauthorized: invalid or revoked API key", type: "auth_error" } }); } authKeyName = keyInfo.name; authKeyId = keyInfo.id; } } // AUTH_MODE === "none": no auth check, allow all } // Attach auth info to request for downstream use req._authKeyName = authKeyName; req._authKeyId = authKeyId; ``` - [ ] **Step 3: Update CORS to allow LAN origins** Replace the CORS block (lines 1019-1025) with: ```javascript // Dynamic CORS: allow localhost and LAN origins const origin = req.headers["origin"] || ""; const isAllowedOrigin = /^https?:\/\/(127\.0\.0\.1|localhost|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|10\.\d+\.\d+\.\d+)(:\d+)?$/.test(origin); res.setHeader("Access-Control-Allow-Origin", isAllowedOrigin ? origin : "*"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS, PATCH"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Session-Id, X-Conversation-Id"); if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; } ``` - [ ] **Step 4: Update server.listen to use BIND_ADDRESS** Replace line 1189: ```javascript server.listen(PORT, BIND_ADDRESS, () => { const bindMsg = BIND_ADDRESS === "0.0.0.0" ? `http://0.0.0.0:${PORT} (LAN mode)` : `http://127.0.0.1:${PORT}`; console.log(`openclaw-claude-proxy v${VERSION} listening on ${bindMsg}`); ``` Add auth mode to startup log (after line 1200): ```javascript console.log(`Auth mode: ${AUTH_MODE}${AUTH_MODE === "shared" ? " (PROXY_API_KEY)" : AUTH_MODE === "multi" ? " (per-user keys)" : " (open)"}`); console.log(`Bind: ${BIND_ADDRESS}${BIND_ADDRESS === "0.0.0.0" ? " ⚠ LAN-accessible" : ""}`); ``` - [ ] **Step 5: Add usage recording to request handlers** In `handleChatCompletions` (around line 1001-1014), after successful response and in catch block, add usage recording. Wrap the try/catch: ```javascript try { const content = await callClaude(model, messages, conversationId); const id = `chatcmpl-${randomUUID()}`; completionResponse(res, id, model, content); // Record usage recordUsage({ keyId: req._authKeyId, keyName: req._authKeyName, model, promptChars: messages.reduce((a, m) => a + (typeof m.content === "string" ? m.content.length : JSON.stringify(m.content).length), 0), responseChars: content.length, elapsedMs: Date.now() - Date.now(), success: true, }); } catch (err) { recordUsage({ keyId: req._authKeyId, keyName: req._authKeyName, model, promptChars: 0, responseChars: 0, elapsedMs: 0, success: false, }); // ... existing error handling } ``` Similarly for streaming in `callClaudeStreaming`, record usage in the `proc.on("close")` handler by passing `req._authKeyName` and `req._authKeyId` through. - [ ] **Step 6: Add closeDb to graceful shutdown** In `gracefulShutdown()` function, after clearing intervals (around line 1150), add: ```javascript closeDb(); ``` - [ ] **Step 7: Commit** ```bash git add server.mjs git commit -m "feat: add LAN bind, 3-mode auth, and usage recording to server" ``` --- ### Task 4: Add key management + dashboard API endpoints to `server.mjs` **Files:** - Modify: `server.mjs` (add routes before the 404 handler) - [ ] **Step 1: Add key management API endpoints** Before the 404 handler (line 1132), add: ```javascript // ── Key management API (admin-only in multi mode) ── // Admin gate: in multi mode, key management requires admin key const isAdmin = AUTH_MODE !== "multi" || authKeyName === "admin" || BIND_ADDRESS === "127.0.0.1"; // POST /api/keys — create a new API key if (req.url === "/api/keys" && req.method === "POST") { if (!isAdmin) return jsonResponse(res, 403, { error: "Admin access required" }); let body = ""; for await (const chunk of req) body += chunk; let parsed; try { parsed = JSON.parse(body); } catch { return jsonResponse(res, 400, { error: "Invalid JSON" }); } const name = parsed.name || `key-${Date.now()}`; const newKey = createKey(name); return jsonResponse(res, 201, newKey); } // GET /api/keys — list all keys if (req.url === "/api/keys" && req.method === "GET") { if (!isAdmin) return jsonResponse(res, 403, { error: "Admin access required" }); return jsonResponse(res, 200, { keys: listKeys() }); } // DELETE /api/keys/:id — revoke a key if (req.url?.startsWith("/api/keys/") && req.method === "DELETE") { if (!isAdmin) return jsonResponse(res, 403, { error: "Admin access required" }); const idOrName = decodeURIComponent(req.url.split("/api/keys/")[1]); const revoked = revokeKey(idOrName); return jsonResponse(res, 200, { revoked, idOrName }); } // GET /api/usage — usage stats by key if (req.url?.startsWith("/api/usage") && req.method === "GET") { if (!isAdmin) return jsonResponse(res, 403, { error: "Admin access required" }); const url = new URL(req.url, `http://${BIND_ADDRESS}:${PORT}`); const since = url.searchParams.get("since"); const until = url.searchParams.get("until"); return jsonResponse(res, 200, { byKey: getUsageByKey({ since, until }), timeline: getUsageTimeline({ hours: parseInt(url.searchParams.get("hours") || "24", 10) }), recent: getRecentUsage(parseInt(url.searchParams.get("limit") || "50", 10)), }); } ``` - [ ] **Step 2: Commit** ```bash git add server.mjs git commit -m "feat: add key management and usage query API endpoints" ``` --- ### Task 5: Create embedded web dashboard **Files:** - Create: `dashboard.html` - Modify: `server.mjs` (add `/dashboard` route) The dashboard is a single HTML file served inline. It uses vanilla JS + CSS, no framework, no build step. - [ ] **Step 1: Create dashboard.html** ```html OCP Dashboard

OCP Dashboard

Plan Usage

Usage by Key

KeyRequestsOKErrAvg TimeLast Request

Recent Requests

TimeKeyModelPromptResponseTimeStatus
``` - [ ] **Step 2: Add dashboard route to server.mjs** Before the 404 handler, add: ```javascript // GET /dashboard — web dashboard if (req.url === "/dashboard" && req.method === "GET") { try { const html = readFileSync(join(__dirname, "dashboard.html"), "utf8"); res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(html); } catch (err) { return jsonResponse(res, 500, { error: "Dashboard file not found" }); } return; } ``` - [ ] **Step 3: Update 404 message to include new endpoints** ```javascript jsonResponse(res, 404, { error: "Not found. Endpoints: GET /v1/models, POST /v1/chat/completions, GET /health, GET /usage, GET /status, GET /logs, GET|PATCH /settings, GET|DELETE /sessions, GET /dashboard, GET|POST|DELETE /api/keys, GET /api/usage" }); ``` - [ ] **Step 4: Commit** ```bash git add dashboard.html server.mjs git commit -m "feat: add embedded web dashboard with real-time usage display" ``` --- ### Task 6: Extend `ocp` CLI with key management and LAN commands **Files:** - Modify: `ocp` (bash CLI) - [ ] **Step 1: Add `ocp keys` command** After the `cmd_clear` section (around line 214), add: ```bash # ── keys ──────────────────────────────────────────────────────────────── cmd_keys_help() { cat <<'EOF' ocp keys — Manage API keys (multi-key auth mode) Usage: ocp keys List all keys ocp keys add Create a new key ocp keys revoke Revoke a key Examples: ocp keys add wife-laptop ocp keys add son-ipad ocp keys revoke wife-laptop EOF } cmd_keys() { case "${1:-}" in add) if [[ -z "${2:-}" ]]; then echo "Usage: ocp keys add "; return 1; fi local result result=$(curl -sf --max-time 5 -X POST "$PROXY/api/keys" \ -H "Content-Type: application/json" \ -d "{\"name\": \"$2\"}" 2>&1) || { echo "Error: proxy unreachable or unauthorized"; exit 1; } echo "$result" | python3 -c " import sys, json d = json.loads(sys.stdin.read()) if 'key' in d: print(f'✓ Key created for \"{d[\"name\"]}\"') print(f'') print(f' API Key: {d[\"key\"]}') print(f'') print(f' Copy this key now — you won\\'t see it again.') print(f' Configure in IDE: OPENAI_API_KEY={d[\"key\"]}') else: print(f'✗ {d.get(\"error\", \"Unknown error\")}') " ;; revoke) if [[ -z "${2:-}" ]]; then echo "Usage: ocp keys revoke "; return 1; fi curl -sf --max-time 5 -X DELETE "$PROXY/api/keys/$2" | python3 -c " import sys, json d = json.loads(sys.stdin.read()) if d.get('revoked'): print(f'✓ Key \"{d[\"idOrName\"]}\" revoked.') else: print(f'✗ Key not found or already revoked.') " ;; ""|--help|-h) if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then cmd_keys_help; return; fi # List keys curl -sf --max-time 5 "$PROXY/api/keys" | python3 -c " import sys, json d = json.loads(sys.stdin.read()) keys = d.get('keys', []) if not keys: print('No API keys configured.') print('Create one: ocp keys add ') else: print('API Keys') print('─────────────────────────────────────────────────') for k in keys: status = '✗ revoked' if k['revoked'] else '✓ active' print(f' {k[\"name\"]:<20} {k[\"keyPreview\"]:<20} {status} {k[\"created_at\"]}') " 2>/dev/null || { echo "Error: proxy unreachable or key management not available"; exit 1; } ;; *) echo "Unknown subcommand: $1"; cmd_keys_help; return 1 ;; esac } ``` - [ ] **Step 2: Add `ocp lan` command** ```bash # ── lan ───────────────────────────────────────────────────────────────── cmd_lan_help() { cat <<'EOF' ocp lan — Quick LAN mode setup guide Shows current network configuration and connection instructions for other devices on the same network. Usage: ocp lan EOF } cmd_lan() { local ip ip=$(ipconfig getifaddr en0 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}') local port port=$(curl -sf --max-time 5 "$PROXY/health" | python3 -c "import sys,json; h=json.loads(sys.stdin.read()); print(h.get('config',{}).get('port', '3456'))" 2>/dev/null || echo "3456") echo "OCP LAN Setup" echo "─────────────────────────────────────" echo "" echo " Your IP: $ip" echo " Port: $port" echo "" echo " For IDE users, set:" echo " OPENAI_BASE_URL=http://$ip:$port/v1" echo " OPENAI_API_KEY= (if auth enabled)" echo "" echo " Dashboard: http://$ip:$port/dashboard" echo "" # Check if currently bound to LAN if curl -sf --max-time 2 "http://$ip:$port/health" > /dev/null 2>&1; then echo " Status: ✓ LAN-accessible" else echo " Status: ✗ Not LAN-accessible (bound to localhost only)" echo "" echo " To enable LAN mode, set env var and restart:" echo " CLAUDE_BIND=0.0.0.0" echo " ocp restart" fi } ``` - [ ] **Step 3: Add `ocp usage --by-key` option** Modify `cmd_usage()` to accept `--by-key`: After the existing `cmd_usage()` function, add a check: ```bash # In the cmd_usage function, add at the top: cmd_usage() { if [[ "${1:-}" == "--by-key" ]]; then local data data=$(curl -sf --max-time 15 "$PROXY/api/usage" 2>&1) || { echo "Error: proxy unreachable or usage API not available"; exit 1; } echo "$data" | python3 -c " import sys, json d = json.loads(sys.stdin.read()) by_key = d.get('byKey', []) if not by_key: print('No usage data yet.') else: print('Usage by Key') print('─────────────────────────────────────────────────────────────────') hdr = f' {\"Key\":<20} {\"Reqs\":>5} {\"OK\":>4} {\"Err\":>4} {\"Avg Time\":>9} {\"Last Request\":<20}' print(hdr) print(' ' + '─' * (len(hdr) - 2)) for k in by_key: avg_t = f'{k[\"avg_elapsed_ms\"]/1000:.1f}s' if k['avg_elapsed_ms'] else '-' print(f' {k[\"key_name\"]:<20} {k[\"requests\"]:>5} {k[\"successes\"]:>4} {k[\"errors\"]:>4} {avg_t:>9} {k[\"last_request\"]:<20}') " return fi # ... existing cmd_usage code below ... ``` - [ ] **Step 4: Update dispatch case and help** In the `case` statement at the bottom, add: ```bash keys) cmd_keys "${1:-}" "${2:-}" ;; lan) cmd_lan ;; ``` Update `cmd_help()`: ```bash keys Manage API keys (add/list/revoke) lan LAN mode setup guide usage Plan usage limits (--by-key for per-key stats) ``` - [ ] **Step 5: Commit** ```bash git add ocp git commit -m "feat: add ocp keys, ocp lan, and ocp usage --by-key CLI commands" ``` --- ### Task 7: Update setup.mjs with LAN mode prompts **Files:** - Modify: `setup.mjs` - [ ] **Step 1: Add LAN mode env vars to launchd plist and systemd service** In `setup.mjs`, in the plist XML template (around line 351), add environment variables for LAN mode if the user chose it. Add these optional env entries to the `EnvironmentVariables` dict: ```xml CLAUDE_BIND ${BIND_ADDRESS} CLAUDE_AUTH_MODE ${AUTH_MODE_CONFIG} ``` Similarly update the systemd service template to include: ``` Environment=CLAUDE_BIND=${BIND_ADDRESS} Environment=CLAUDE_AUTH_MODE=${AUTH_MODE_CONFIG} ``` Add at the top of setup.mjs, after existing `opt()` calls: ```javascript const BIND_ADDRESS = opt("bind", "127.0.0.1"); const AUTH_MODE_CONFIG = opt("auth-mode", "none"); ``` - [ ] **Step 2: Commit** ```bash git add setup.mjs git commit -m "feat: add LAN mode config options to setup.mjs" ``` --- ### Task 8: Update README with LAN mode documentation **Files:** - Modify: `README.md` - [ ] **Step 1: Add LAN Mode section to README** Add a new section after the existing "Quick Start" section: ```markdown ## LAN Mode — Share with Family OCP can serve your entire household from a single machine. ### Enable LAN Access ```bash # Set bind address and restart export CLAUDE_BIND=0.0.0.0 ocp restart ``` Or permanently via setup: ```bash node setup.mjs --bind 0.0.0.0 --auth-mode multi ``` ### Auth Modes | Mode | Env | Description | |------|-----|-------------| | `none` | `CLAUDE_AUTH_MODE=none` | Open access (trusted network) | | `shared` | `CLAUDE_AUTH_MODE=shared` | Single key via `PROXY_API_KEY` | | `multi` | `CLAUDE_AUTH_MODE=multi` | Per-user keys with usage tracking | ### Multi-Key Setup ```bash # Create keys for family members ocp keys add wife-laptop ocp keys add son-ipad # List keys ocp keys # View per-key usage ocp usage --by-key # Revoke a key ocp keys revoke son-ipad ``` ### Family Member Setup Give your family member these settings for their IDE: ``` OPENAI_BASE_URL=http://:3456/v1 OPENAI_API_KEY=ocp_ ``` Run `ocp lan` to see your IP and connection guide. ### Web Dashboard Access the usage dashboard at: `http://:3456/dashboard` Shows real-time stats: per-key usage, request history, plan utilization, and system health. ``` - [ ] **Step 2: Update version in README** Change version references from `v3.3.1` to `v3.4.0`. - [ ] **Step 3: Commit** ```bash git add README.md git commit -m "docs: add LAN mode documentation and family sharing guide" ``` --- ### Task 9: Integration test — end-to-end verification **Files:** - No new files — manual verification steps - [ ] **Step 1: Start OCP in LAN mode with multi-key auth** ```bash cd /Users/taodeng/.openclaw/projects/claude-proxy CLAUDE_BIND=0.0.0.0 CLAUDE_AUTH_MODE=multi OCP_ADMIN_KEY=test-admin-123 node server.mjs & sleep 3 ``` - [ ] **Step 2: Verify health endpoint (public, no auth)** ```bash curl -s http://127.0.0.1:3456/health | python3 -m json.tool | head -5 ``` Expected: JSON with status "ok", version "3.4.0" - [ ] **Step 3: Create a test API key** ```bash curl -s -X POST http://127.0.0.1:3456/api/keys \ -H "Authorization: Bearer test-admin-123" \ -H "Content-Type: application/json" \ -d '{"name": "test-user"}' | python3 -m json.tool ``` Expected: JSON with `id`, `key` (starts with `ocp_`), `name` - [ ] **Step 4: Test request with key** ```bash # Save the key from step 3 KEY="" curl -s http://127.0.0.1:3456/v1/chat/completions \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" \ -d '{"model":"haiku","messages":[{"role":"user","content":"say hi"}]}' | python3 -m json.tool | head -10 ``` Expected: Chat completion response from Claude - [ ] **Step 5: Verify usage tracking** ```bash curl -s http://127.0.0.1:3456/api/usage \ -H "Authorization: Bearer test-admin-123" | python3 -m json.tool ``` Expected: Usage data showing `test-user` with 1 request - [ ] **Step 6: Test dashboard loads** ```bash curl -s http://127.0.0.1:3456/dashboard | head -5 ``` Expected: HTML starting with `` - [ ] **Step 7: Test auth rejection (no key in multi mode)** ```bash curl -s http://127.0.0.1:3456/v1/models ``` Expected: 401 error with "Bearer token required" - [ ] **Step 8: Stop test server and clean up** ```bash pkill -f "server.mjs" || true ``` - [ ] **Step 9: Commit integration test results as passing** ```bash git add -A git commit -m "test: verify LAN mode, multi-key auth, usage tracking, and dashboard" ``` --- ## Summary | Task | Description | Est. Lines | |------|-------------|-----------| | 1 | Add dependency + version bump | ~5 | | 2 | `keys.mjs` — key CRUD + usage store | ~180 | | 3 | LAN bind + auth modes in server | ~80 | | 4 | Key mgmt + usage API endpoints | ~50 | | 5 | Web dashboard (HTML+JS) | ~200 | | 6 | CLI extensions (`ocp keys/lan`) | ~150 | | 7 | Setup.mjs LAN config | ~20 | | 8 | README documentation | ~60 | | 9 | Integration testing | ~0 | | **Total** | | **~745 lines** |