Compare commits

..
Author SHA1 Message Date
taodengandClaude Sonnet 4.6 68ddbe17c5 fix(setup): inject CLAUDE_BIN, OCP_ADMIN_KEY, PROXY_ANONYMOUS_KEY into service env
Gap #1 partial (CLAUDE_BIN): setup.mjs now detects `which claude` at install
time (or reads $CLAUDE_BIN) and writes CLAUDE_BIN into the service unit
EnvironmentVariables dict. Works for nvm/homebrew paths not in server.mjs's
hardcoded list, and for older server.mjs deployments.

Gap #2 (OCP_ADMIN_KEY): reads $OCP_ADMIN_KEY from the user's shell env and
conditionally injects it into the plist/systemd unit. Empty/unset → key is
omitted entirely (server.mjs treats empty string as "no admin"). Key value is
never logged; only its length is reported.

Gap #6 partial (PROXY_ANONYMOUS_KEY): reads $PROXY_ANONYMOUS_KEY and
conditionally injects it. Unset → key is omitted (anonymous access disabled).

All three keys are read via process.env at install time; no new CLI flags.
Injection status is logged before the !DRY_RUN guard so dry-run shows what
would be written.

Identified during fresh-state Round 2 testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 00:44:03 +10:00
5 changed files with 40 additions and 116 deletions
+2 -9
View File
@@ -6,8 +6,6 @@
*Open source from day one, used daily by my family, maintained on nights and weekends. If OCP saves you money too, you can [☕ buy me a coffee](https://buymeacoffee.com/dtzp555) — [full story below](#support-ocp).* *Open source from day one, used daily by my family, maintained on nights and weekends. If OCP saves you money too, you can [☕ buy me a coffee](https://buymeacoffee.com/dtzp555) — [full story below](#support-ocp).*
*If OCP saves you a setup, a ⭐ helps other folks discover it. Issue reports are even more useful — that's the highest-quality feedback this project gets.*
OCP turns your Claude Pro/Max subscription into a standard OpenAI-compatible API on localhost. Any tool that speaks the OpenAI protocol can use it — no separate API key, no extra billing. OCP turns your Claude Pro/Max subscription into a standard OpenAI-compatible API on localhost. Any tool that speaks the OpenAI protocol can use it — no separate API key, no extra billing.
``` ```
@@ -187,8 +185,7 @@ The setup script will:
1. Verify Claude CLI is installed and authenticated 1. Verify Claude CLI is installed and authenticated
2. Start the proxy on port 3456 2. Start the proxy on port 3456
3. Install auto-start (launchd on macOS, systemd on Linux) 3. Install auto-start (launchd on macOS, systemd on Linux)
4. Symlink `ocp` to `/usr/local/bin` for CLI access
After install the `ocp` CLI lives at `~/ocp/ocp`. To put it on your PATH, either symlink it manually (`ln -sf ~/ocp/ocp ~/.local/bin/ocp` if `~/.local/bin` is on your PATH, or `sudo ln -sf ~/ocp/ocp /usr/local/bin/ocp` for a system-wide symlink) or add an alias (`alias ocp=~/ocp/ocp`). Otherwise invoke it as `~/ocp/ocp <subcommand>`. The rest of this README assumes `ocp` is on your PATH.
**Single-machine use** — just set your IDE to use the proxy: **Single-machine use** — just set your IDE to use the proxy:
```bash ```bash
@@ -398,15 +395,11 @@ In `multi` mode, the admin can designate a single well-known "anonymous" key tha
**Enable**: **Enable**:
The anonymous key is wired into the service unit (launchd plist on macOS, systemd unit on Linux) at install time. Export `PROXY_ANONYMOUS_KEY` in your shell before running `setup.mjs`, and `setup.mjs` will write it into the service unit env so the auto-started proxy picks it up:
```bash ```bash
export PROXY_ANONYMOUS_KEY=ocp_public_anon # or any string of your choice export PROXY_ANONYMOUS_KEY=ocp_public_anon # or any string of your choice
node setup.mjs --bind 0.0.0.0 --auth-mode multi ocp start # or however you start the server
``` ```
If OCP is already installed without it, re-export the env var and re-run `node setup.mjs` (the installer is idempotent — it refreshes the service unit). Then `ocp restart` so the running proxy picks up the new env. Setting `PROXY_ANONYMOUS_KEY` only in your interactive shell **does not** affect the auto-started proxy — the service unit is the source of truth for its environment.
**Client side**: the anonymous key value is exposed via `GET /health` as the field `anonymousKey` (null when not set). Clients like `ocp-connect` can auto-discover and use it, so the end user doesn't need to get a personal key from the admin. **Client side**: the anonymous key value is exposed via `GET /health` as the field `anonymousKey` (null when not set). Clients like `ocp-connect` can auto-discover and use it, so the end user doesn't need to get a personal key from the admin.
**Security note**: setting this env var is an **opt-in** to public access — anyone who can reach your OCP endpoint can use it, up to any rate limits you configure. Don't enable this on internet-exposed OCP instances without additional protection. **Security note**: setting this env var is an **opt-in** to public access — anyone who can reach your OCP endpoint can use it, up to any rate limits you configure. Don't enable this on internet-exposed OCP instances without additional protection.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 222 KiB

+9 -6
View File
@@ -8,18 +8,21 @@ set -euo pipefail
PROXY="http://127.0.0.1:3456" PROXY="http://127.0.0.1:3456"
# Auth args for multi-key mode: reads from OCP_ADMIN_KEY env or ~/.ocp/admin-key file # Auth header for multi-key mode: reads from OCP_ADMIN_KEY env or ~/.ocp/admin-key file
# Using a bash array preserves word boundaries — no eval needed. _AUTH_HEADER=""
_AUTH_ARGS=()
if [[ -n "${OCP_ADMIN_KEY:-}" ]]; then if [[ -n "${OCP_ADMIN_KEY:-}" ]]; then
_AUTH_ARGS=(-H "Authorization: Bearer $OCP_ADMIN_KEY") _AUTH_HEADER="-H \"Authorization: Bearer $OCP_ADMIN_KEY\""
elif [[ -f "$HOME/.ocp/admin-key" ]]; then elif [[ -f "$HOME/.ocp/admin-key" ]]; then
_AUTH_ARGS=(-H "Authorization: Bearer $(cat "$HOME/.ocp/admin-key")") _AUTH_HEADER="-H \"Authorization: Bearer $(cat "$HOME/.ocp/admin-key")\""
fi fi
# Wrapper: curl with optional auth # Wrapper: curl with optional auth
_curl() { _curl() {
curl "${_AUTH_ARGS[@]}" "$@" if [[ -n "$_AUTH_HEADER" ]]; then
eval curl "$_AUTH_HEADER" "$@"
else
curl "$@"
fi
} }
_json() { python3 -m json.tool 2>/dev/null || cat; } _json() { python3 -m json.tool 2>/dev/null || cat; }
+2 -12
View File
@@ -582,19 +582,11 @@ print(k if k else '')
if [[ "${SHELL:-}" == */fish ]]; then if [[ "${SHELL:-}" == */fish ]]; then
echo " Note: fish shell detected. Writing to ~/.bashrc — add to fish config manually." echo " Note: fish shell detected. Writing to ~/.bashrc — add to fish config manually."
rc_files+=("$HOME/.bashrc") rc_files+=("$HOME/.bashrc")
elif $is_mac; then
# macOS: default shell since Catalina (2019) is zsh.
# Always write ~/.zshrc (create if absent — zsh tolerates an empty file).
# Only write ~/.bashrc if it already exists (don't surprise users with new files).
[[ -f "$HOME/.bashrc" ]] && rc_files+=("$HOME/.bashrc")
# zshrc: always include on macOS; create the file if it doesn't exist yet
[[ -f "$HOME/.zshrc" ]] || touch "$HOME/.zshrc"
rc_files+=("$HOME/.zshrc")
else else
# Linux / other: write to whichever rc files already exist or match current shell # Always write both on macOS (default shell is zsh but some tools source bashrc)
[[ -f "$HOME/.bashrc" || "${SHELL:-}" == */bash ]] && rc_files+=("$HOME/.bashrc") [[ -f "$HOME/.bashrc" || "${SHELL:-}" == */bash ]] && rc_files+=("$HOME/.bashrc")
[[ -f "$HOME/.zshrc" || "${SHELL:-}" == */zsh ]] && rc_files+=("$HOME/.zshrc") [[ -f "$HOME/.zshrc" || "${SHELL:-}" == */zsh ]] && rc_files+=("$HOME/.zshrc")
# If neither exists, fall back to creating one for the current shell # If neither exists, create for current shell
[[ ${#rc_files[@]} -eq 0 ]] && rc_files+=("$HOME/.${SHELL##*/}rc") [[ ${#rc_files[@]} -eq 0 ]] && rc_files+=("$HOME/.${SHELL##*/}rc")
fi fi
@@ -713,9 +705,7 @@ PYEOF
echo "" echo ""
echo " Done. Reload your shell to apply:" echo " Done. Reload your shell to apply:"
for rc_file in "${rc_files[@]}"; do
echo " source $rc_file" echo " source $rc_file"
done
} }
main "$@" main "$@"
+26 -88
View File
@@ -30,7 +30,7 @@
import { createServer } from "node:http"; import { createServer } from "node:http";
import { spawn, execFileSync } from "node:child_process"; import { spawn, execFileSync } from "node:child_process";
import { randomUUID, timingSafeEqual } from "node:crypto"; import { randomUUID, timingSafeEqual } from "node:crypto";
import { readFileSync, readdirSync, accessSync, existsSync, constants } from "node:fs"; import { readFileSync, accessSync, existsSync, constants } from "node:fs";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { homedir } from "node:os"; import { homedir } from "node:os";
@@ -41,48 +41,8 @@ const _pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf8"));
const modelsConfig = JSON.parse(readFileSync(join(__dirname, "models.json"), "utf8")); const modelsConfig = JSON.parse(readFileSync(join(__dirname, "models.json"), "utf8"));
// ── Resolve claude binary ─────────────────────────────────────────────── // ── Resolve claude binary ───────────────────────────────────────────────
// Priority: CLAUDE_BIN env > well-known paths > nvm/fnm/asdf user-local // Priority: CLAUDE_BIN env > well-known paths > which lookup
// installs > which lookup. Fail-fast if not found — never start with an // Fail-fast if not found — never start with an unresolvable binary.
// unresolvable binary.
function _listVersionDirs(parent) {
try { return readdirSync(parent); } catch { return []; }
}
function _collectNodeManagerCandidates(home) {
if (!home) return [];
const out = [];
// nvm: $HOME/.nvm/versions/node/<version>/bin/claude
const nvmRoot = join(home, ".nvm/versions/node");
for (const v of _listVersionDirs(nvmRoot)) {
out.push(join(nvmRoot, v, "bin/claude"));
}
// nvm default alias: resolve $HOME/.nvm/aliases/default if it points to a version
try {
const aliasFile = join(home, ".nvm/aliases/default");
const aliasVer = readFileSync(aliasFile, "utf8").trim();
if (aliasVer) {
const direct = join(nvmRoot, aliasVer, "bin/claude");
if (!out.includes(direct)) out.unshift(direct);
}
} catch {}
// fnm: $HOME/.fnm/node-versions/<version>/installation/bin/claude
const fnmRoot = join(home, ".fnm/node-versions");
for (const v of _listVersionDirs(fnmRoot)) {
out.push(join(fnmRoot, v, "installation/bin/claude"));
}
// asdf: $HOME/.asdf/installs/nodejs/<version>/bin/claude
const asdfRoot = join(home, ".asdf/installs/nodejs");
for (const v of _listVersionDirs(asdfRoot)) {
out.push(join(asdfRoot, v, "bin/claude"));
}
// npm prefix-relocated: $HOME/.npm-global/bin/claude
out.push(join(home, ".npm-global/bin/claude"));
return out;
}
function resolveClaude() { function resolveClaude() {
if (process.env.CLAUDE_BIN) { if (process.env.CLAUDE_BIN) {
try { try {
@@ -94,13 +54,11 @@ function resolveClaude() {
} }
} }
const home = process.env.HOME || "";
const candidates = [ const candidates = [
"/opt/homebrew/bin/claude", "/opt/homebrew/bin/claude",
"/usr/local/bin/claude", "/usr/local/bin/claude",
"/usr/bin/claude", "/usr/bin/claude",
join(home, ".local/bin/claude"), join(process.env.HOME || "", ".local/bin/claude"),
..._collectNodeManagerCandidates(home),
]; ];
for (const p of candidates) { for (const p of candidates) {
try { accessSync(p, constants.X_OK); console.warn(`[init] CLAUDE_BIN not set, resolved to ${p}`); return p; } catch {} try { accessSync(p, constants.X_OK); console.warn(`[init] CLAUDE_BIN not set, resolved to ${p}`); return p; } catch {}
@@ -114,8 +72,6 @@ function resolveClaude() {
console.error( console.error(
"FATAL: claude binary not found.\n" + "FATAL: claude binary not found.\n" +
" Set CLAUDE_BIN=/path/to/claude or ensure claude is in PATH.\n" + " Set CLAUDE_BIN=/path/to/claude or ensure claude is in PATH.\n" +
" Hint: if you use nvm/fnm/asdf, set CLAUDE_BIN to the absolute path\n" +
" shown by `which claude` in your interactive shell.\n" +
" Checked: " + candidates.join(", ") " Checked: " + candidates.join(", ")
); );
process.exit(1); process.exit(1);
@@ -202,36 +158,25 @@ const MODEL_MAP = Object.fromEntries([
const MODELS = modelsConfig.models.map(m => ({ id: m.id, name: m.displayName })); const MODELS = modelsConfig.models.map(m => ({ id: m.id, name: m.displayName }));
// ── Session management ────────────────────────────────────────────────── // ── Session management ──────────────────────────────────────────────────
// Maps namespaced session keys to Claude CLI session UUIDs. // Maps conversation IDs (from caller) to Claude CLI session UUIDs.
// Key format: "${keyName}|${conversationId}" — prevents cross-key collision
// when two callers (different API keys or anon + authenticated) use the same
// session_id string. Anonymous callers use "anon"; admin uses "admin".
// Enables --resume for multi-turn conversations, reducing token waste. // Enables --resume for multi-turn conversations, reducing token waste.
const sessions = new Map(); // `${keyName}|${conversationId}` → { uuid, messageCount, lastUsed, model } const sessions = new Map(); // conversationId → { uuid, messageCount, lastUsed, model }
// Build the namespaced key used for all sessions Map operations.
// Returns null when conversationId is falsy (one-off requests bypass session tracking).
function _sessionKey(conversationId, keyName) {
return conversationId ? `${keyName || "anon"}|${conversationId}` : null;
}
const sessionCleanupInterval = setInterval(() => { const sessionCleanupInterval = setInterval(() => {
const now = Date.now(); const now = Date.now();
for (const [id, s] of sessions) { for (const [id, s] of sessions) {
const idleMs = now - s.lastUsed; const idleMs = now - s.lastUsed;
const ageMs = s.firstSeen ? now - s.firstSeen : null; const ageMs = s.firstSeen ? now - s.firstSeen : null;
// id is "${keyName}|${conversationId}"; strip prefix for log output
const convIdShort = id.includes("|") ? id.slice(id.indexOf("|") + 1, id.indexOf("|") + 13) : id.slice(0, 12);
if (idleMs > SESSION_TTL) { if (idleMs > SESSION_TTL) {
sessions.delete(id); sessions.delete(id);
console.log(`[session] expired ${convIdShort}... (idle ${Math.round(idleMs / 60000)}m)`); console.log(`[session] expired ${id.slice(0, 12)}... (idle ${Math.round(idleMs / 60000)}m)`);
logEvent("info", "session_expired", { conversationId: convIdShort + "...", idleMs, ageMs }); logEvent("info", "session_expired", { conversationId: id.slice(0, 12) + "...", idleMs, ageMs });
} else if (ageMs !== null && ageMs > 4 * SESSION_TTL) { } else if (ageMs !== null && ageMs > 4 * SESSION_TTL) {
// #42 evidence-gathering: a session whose firstSeen is more than 4× TTL old // #42 evidence-gathering: a session whose firstSeen is more than 4× TTL old
// but whose lastUsed keeps getting bumped (never idle long enough to expire) // but whose lastUsed keeps getting bumped (never idle long enough to expire)
// is the suspected bug. Log without action so the pattern can be confirmed // is the suspected bug. Log without action so the pattern can be confirmed
// in /logs. Do NOT enforce an absolute age cap here speculatively. // in /logs. Do NOT enforce an absolute age cap here speculatively.
logEvent("warn", "session_long_lived", { conversationId: convIdShort + "...", idleMs, ageMs }); logEvent("warn", "session_long_lived", { conversationId: id.slice(0, 12) + "...", idleMs, ageMs });
} }
} }
}, 60000); }, 60000);
@@ -438,7 +383,7 @@ function getModelTier(cliModel) {
// ── Spawn claude CLI (shared setup) ───────────────────────────────────── // ── Spawn claude CLI (shared setup) ─────────────────────────────────────
// Resolves session logic, builds CLI args, spawns the process, and sets up // Resolves session logic, builds CLI args, spawns the process, and sets up
// timeouts. Returns context object or throws synchronously. // timeouts. Returns context object or throws synchronously.
function spawnClaudeProcess(model, messages, conversationId, keyName) { function spawnClaudeProcess(model, messages, conversationId) {
if (stats.activeRequests >= MAX_CONCURRENT) { if (stats.activeRequests >= MAX_CONCURRENT) {
throw new Error(`concurrency limit reached (${stats.activeRequests}/${MAX_CONCURRENT})`); throw new Error(`concurrency limit reached (${stats.activeRequests}/${MAX_CONCURRENT})`);
} }
@@ -454,11 +399,8 @@ function spawnClaudeProcess(model, messages, conversationId, keyName) {
let prompt; let prompt;
// ── Session logic ── // ── Session logic ──
// sessionKey namespaces the Map key by keyName to prevent cross-caller collision if (conversationId && sessions.has(conversationId)) {
// when two callers with different API keys share the same conversationId string. const session = sessions.get(conversationId);
const sessionKey = _sessionKey(conversationId, keyName);
if (sessionKey && sessions.has(sessionKey)) {
const session = sessions.get(sessionKey);
session.lastUsed = Date.now(); session.lastUsed = Date.now();
sessionInfo = { uuid: session.uuid, resume: true }; sessionInfo = { uuid: session.uuid, resume: true };
stats.sessionHits++; stats.sessionHits++;
@@ -469,17 +411,17 @@ function spawnClaudeProcess(model, messages, conversationId, keyName) {
: ""; : "";
session.messageCount = messages.length; session.messageCount = messages.length;
console.log(`[session] resume conv=${conversationId.slice(0, 12)}... key=${keyName || "anon"} uuid=${session.uuid.slice(0, 8)}... msgs=${messages.length} prompt_chars=${prompt.length}`); console.log(`[session] resume conv=${conversationId.slice(0, 12)}... uuid=${session.uuid.slice(0, 8)}... msgs=${messages.length} prompt_chars=${prompt.length}`);
} else if (sessionKey) { } else if (conversationId) {
const uuid = randomUUID(); const uuid = randomUUID();
const now = Date.now(); const now = Date.now();
sessions.set(sessionKey, { uuid, messageCount: messages.length, firstSeen: now, lastUsed: now, model: cliModel }); sessions.set(conversationId, { uuid, messageCount: messages.length, firstSeen: now, lastUsed: now, model: cliModel });
sessionInfo = { uuid, resume: false }; sessionInfo = { uuid, resume: false };
stats.sessionMisses++; stats.sessionMisses++;
prompt = messagesToPrompt(messages); prompt = messagesToPrompt(messages);
console.log(`[session] new conv=${conversationId.slice(0, 12)}... key=${keyName || "anon"} uuid=${uuid.slice(0, 8)}... msgs=${messages.length}`); console.log(`[session] new conv=${conversationId.slice(0, 12)}... uuid=${uuid.slice(0, 8)}... msgs=${messages.length}`);
} else { } else {
stats.oneOffRequests++; stats.oneOffRequests++;
@@ -524,11 +466,11 @@ function spawnClaudeProcess(model, messages, conversationId, keyName) {
proc.once("exit", cleanup); proc.once("exit", cleanup);
function handleSessionFailure() { function handleSessionFailure() {
if (sessionInfo?.resume && sessionKey) { if (sessionInfo?.resume && conversationId) {
console.warn(`[session] resume failed for ${conversationId.slice(0, 12)}..., removing stale session`); console.warn(`[session] resume failed for ${conversationId.slice(0, 12)}..., removing stale session`);
logEvent("warn", "session_failure", { mode: "resume", conversationId: conversationId.slice(0, 12) + "...", action: "deleted" }); logEvent("warn", "session_failure", { mode: "resume", conversationId: conversationId.slice(0, 12) + "...", action: "deleted" });
sessions.delete(sessionKey); sessions.delete(conversationId);
} else if (sessionInfo && !sessionInfo.resume && sessionKey) { } else if (sessionInfo && !sessionInfo.resume && conversationId) {
// #41 evidence-gathering: session-create failures currently leave a stale entry // #41 evidence-gathering: session-create failures currently leave a stale entry
// in the sessions map. Log without action so the staleness pattern can be // in the sessions map. Log without action so the staleness pattern can be
// confirmed in /logs before any code change. Do NOT delete here speculatively. // confirmed in /logs before any code change. Do NOT delete here speculatively.
@@ -571,11 +513,11 @@ function spawnClaudeProcess(model, messages, conversationId, keyName) {
// On-demand spawning: each request spawns a fresh `claude -p` process. // On-demand spawning: each request spawns a fresh `claude -p` process.
// No pool = no crash loops, no stale workers, no degraded states. // No pool = no crash loops, no stale workers, no degraded states.
// Stdin is written immediately so there's no 3s stdin timeout issue. // Stdin is written immediately so there's no 3s stdin timeout issue.
function callClaude(model, messages, conversationId, keyName) { function callClaude(model, messages, conversationId) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let ctx; let ctx;
try { try {
ctx = spawnClaudeProcess(model, messages, conversationId, keyName); ctx = spawnClaudeProcess(model, messages, conversationId);
} catch (err) { } catch (err) {
return reject(err); return reject(err);
} }
@@ -655,7 +597,7 @@ function callClaudeStreaming(model, messages, conversationId, res, authInfo = {}
let ctx; let ctx;
try { try {
ctx = spawnClaudeProcess(model, messages, conversationId, authInfo.keyName); ctx = spawnClaudeProcess(model, messages, conversationId);
} catch (err) { } catch (err) {
return jsonResponse(res, 500, { error: { message: err.message, type: "proxy_error" } }); return jsonResponse(res, 500, { error: { message: err.message, type: "proxy_error" } });
} }
@@ -1338,7 +1280,7 @@ async function handleChatCompletions(req, res) {
// will re-read the freshly-populated cache entry here rather than spawning. // will re-read the freshly-populated cache entry here rather than spawning.
const recheck = getCachedResponse(req._cacheHash, CACHE_TTL); const recheck = getCachedResponse(req._cacheHash, CACHE_TTL);
if (recheck) return recheck.response; if (recheck) return recheck.response;
const c = await callClaude(model, messages, conversationId, req._authKeyName); const c = await callClaude(model, messages, conversationId);
try { setCachedResponse(req._cacheHash, model, c); } catch (e) { logEvent("error", "cache_write_failed", { error: e.message }); } try { setCachedResponse(req._cacheHash, model, c); } catch (e) { logEvent("error", "cache_write_failed", { error: e.message }); }
return c; return c;
}); });
@@ -1360,7 +1302,7 @@ async function handleChatCompletions(req, res) {
// Fallback: cache disabled (CACHE_TTL=0) or no _cacheHash — original path untouched. // Fallback: cache disabled (CACHE_TTL=0) or no _cacheHash — original path untouched.
try { try {
const content = await callClaude(model, messages, conversationId, req._authKeyName); const content = await callClaude(model, messages, conversationId);
const id = `chatcmpl-${randomUUID()}`; const id = `chatcmpl-${randomUUID()}`;
completionResponse(res, id, model, content); completionResponse(res, id, model, content);
try { recordUsage({ keyId: req._authKeyId, keyName: req._authKeyName, model, promptChars, responseChars: content.length, elapsedMs: Date.now() - t0Usage, success: true }); } catch (e) { logEvent("error", "usage_record_failed", { error: e.message }); } try { recordUsage({ keyId: req._authKeyId, keyName: req._authKeyName, model, promptChars, responseChars: content.length, elapsedMs: Date.now() - t0Usage, success: true }); } catch (e) { logEvent("error", "usage_record_failed", { error: e.message }); }
@@ -1494,10 +1436,8 @@ const server = createServer(async (req, res) => {
const uptimeMs = Date.now() - START_TIME; const uptimeMs = Date.now() - START_TIME;
const sessionList = []; const sessionList = [];
for (const [id, s] of sessions) { for (const [id, s] of sessions) {
// id is "${keyName}|${conversationId}"; expose only the public-facing conversationId
const convId = id.includes("|") ? id.slice(id.indexOf("|") + 1) : id;
sessionList.push({ sessionList.push({
id: convId.slice(0, 12) + "...", id: id.slice(0, 12) + "...",
model: s.model, model: s.model,
messages: s.messageCount, messages: s.messageCount,
idleMs: Date.now() - s.lastUsed, idleMs: Date.now() - s.lastUsed,
@@ -1542,9 +1482,7 @@ const server = createServer(async (req, res) => {
if (req.url === "/sessions" && req.method === "GET") { if (req.url === "/sessions" && req.method === "GET") {
const list = []; const list = [];
for (const [id, s] of sessions) { for (const [id, s] of sessions) {
// id is "${keyName}|${conversationId}"; expose only the public-facing conversationId list.push({ id, uuid: s.uuid, model: s.model, messages: s.messageCount, lastUsed: new Date(s.lastUsed).toISOString() });
const convId = id.includes("|") ? id.slice(id.indexOf("|") + 1) : id;
list.push({ id: convId, uuid: s.uuid, model: s.model, messages: s.messageCount, lastUsed: new Date(s.lastUsed).toISOString() });
} }
return jsonResponse(res, 200, { sessions: list }); return jsonResponse(res, 200, { sessions: list });
} }