+
@@ -82,9 +121,11 @@ export default function ConsoleLogClient() {
>
{logs.length === 0 ? (
No console logs yet.
+ ) : filteredLogs.length === 0 ? (
+
No console logs for the selected user.
) : (
- {logs.map((line, i) => (
+ {filteredLogs.map((line, i) => (
{colorLine(line)}
))}
diff --git a/src/lib/consoleLogBuffer.js b/src/lib/consoleLogBuffer.js
index 9ace30fb..76356d0f 100644
--- a/src/lib/consoleLogBuffer.js
+++ b/src/lib/consoleLogBuffer.js
@@ -1,5 +1,6 @@
import { EventEmitter } from "events";
import { CONSOLE_LOG_CONFIG } from "@/shared/constants/config.js";
+import { getRequestUser } from "@/lib/requestContext";
const consoleLevels = ["log", "info", "warn", "error", "debug"];
@@ -63,12 +64,17 @@ function formatArg(arg) {
}
function appendLine(line) {
- state.logs.push(line);
+ const requestUser = getRequestUser();
+ const attributedLine = requestUser
+ ? `[USER:${requestUser.id}] [${requestUser.username}] ${line}`
+ : line;
+
+ state.logs.push(attributedLine);
const maxLines = CONSOLE_LOG_CONFIG.maxLines;
if (state.logs.length > maxLines) {
state.logs = state.logs.slice(-maxLines);
}
- state.pendingLines.push(line);
+ state.pendingLines.push(attributedLine);
if (state.pendingLines.length >= MAX_BATCH_LINES) {
if (state.flushTimer) {
clearTimeout(state.flushTimer);
diff --git a/src/lib/requestContext.js b/src/lib/requestContext.js
new file mode 100644
index 00000000..eb02331b
--- /dev/null
+++ b/src/lib/requestContext.js
@@ -0,0 +1,38 @@
+import { AsyncLocalStorage } from "node:async_hooks";
+
+if (!global._requestUserContext) {
+ global._requestUserContext = new AsyncLocalStorage();
+}
+
+const requestContext = global._requestUserContext;
+
+function normalizeUser(user) {
+ if (!user?.id || !user?.username) return null;
+ return { id: String(user.id), username: String(user.username) };
+}
+
+/**
+ * Execute an async request handler with its dashboard user available to
+ * console-log capture. AsyncLocalStorage keeps concurrent request contexts
+ * isolated while their asynchronous work is in flight.
+ */
+export function runWithRequestUser(user, handler) {
+ return requestContext.run({ user: normalizeUser(user) }, handler);
+}
+
+/**
+ * Update the user associated with the active request, for example after the
+ * selected provider connection resolves its owner.
+ */
+export function setRequestUser(user) {
+ const store = requestContext.getStore();
+ if (store) {
+ store.user = normalizeUser(user);
+ return;
+ }
+ requestContext.enterWith({ user: normalizeUser(user) });
+}
+
+export function getRequestUser() {
+ return requestContext.getStore()?.user || null;
+}
diff --git a/src/sse/handlers/chat.js b/src/sse/handlers/chat.js
index 0d3bafb1..96744c5e 100644
--- a/src/sse/handlers/chat.js
+++ b/src/sse/handlers/chat.js
@@ -10,6 +10,8 @@ import {
} from "../services/auth.js";
import { cacheClaudeHeaders } from "open-sse/utils/claudeHeaderCache.js";
import { getSettings } from "@/lib/localDb";
+import { getUserById } from "@/lib/db";
+import { runWithRequestUser, setRequestUser } from "@/lib/requestContext";
import { getModelInfo, getCombo } from "../services/model.js";
import { handleChatCore } from "open-sse/handlers/chatCore.js";
import { DEFAULT_HEADROOM_URL } from "@/lib/headroom/detect";
@@ -58,6 +60,9 @@ export async function handleChat(request, clientRawRequest = null) {
const authHeader = request.headers.get("Authorization");
const apiKey = extractApiKey(request);
const ownerId = await getApiKeyOwnerId(apiKey);
+ const requestUser = ownerId ? await getUserById(ownerId) : null;
+
+ return runWithRequestUser(requestUser, async () => {
if (authHeader && apiKey) {
const masked = log.maskKey(apiKey);
log.debug("AUTH", `API Key: ${masked}`);
@@ -135,6 +140,7 @@ export async function handleChat(request, clientRawRequest = null) {
// Single model request
return handleSingleModelChat(body, modelStr, clientRawRequest, request, apiKey, ownerId);
+ });
}
/**
@@ -226,6 +232,13 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re
return errorResponse(lastStatus || HTTP_STATUS.SERVICE_UNAVAILABLE, lastError || "All accounts unavailable");
}
+ // Requests without an API key inherit the selected provider connection's
+ // owner, matching usage-history attribution.
+ if (!ownerId && credentials._connection?.ownerId) {
+ const connectionOwner = await getUserById(credentials._connection.ownerId);
+ setRequestUser(connectionOwner);
+ }
+
// Account selection shown in the unified "▶" line (acc:...)
const refreshedCredentials = await checkAndRefreshToken(provider, credentials);