From 7f34d730dbe596064af816fc64a81156eb6a4065 Mon Sep 17 00:00:00 2001 From: David Marsh Date: Fri, 30 Jan 2026 06:57:23 -0800 Subject: [PATCH] feat(hooks): add message_received and message_sent hooks to chat completions endpoint Trigger plugin hooks for API requests through /v1/chat/completions: - message_received: fires before agent processing with prompt content, user field, model, sessionKey, and source metadata - message_sent: fires after response generation (non-streaming only) This enables plugins to react to API traffic, e.g., logging voice shortcut interactions to Discord for history/context. The hooks use the existing plugin hook system (not internal hooks), so any plugin can register handlers via api.on('message_received', ...) Note: Streaming responses do not yet fire message_sent; this could be added in a follow-up by accumulating deltas and firing at lifecycle end. --- src/gateway/openai-http.ts | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/gateway/openai-http.ts b/src/gateway/openai-http.ts index 2cbcb7c1f..5fed62376 100644 --- a/src/gateway/openai-http.ts +++ b/src/gateway/openai-http.ts @@ -5,6 +5,7 @@ import { buildHistoryContextFromEntries, type HistoryEntry } from "../auto-reply import { createDefaultDeps } from "../cli/deps.js"; import { agentCommand } from "../commands/agent.js"; import { emitAgentEvent, onAgentEvent } from "../infra/agent-events.js"; +import { getGlobalHookRunner } from "../plugins/hook-runner-global.js"; import { defaultRuntime } from "../runtime.js"; import { authorizeGatewayConnect, type ResolvedGatewayAuth } from "./auth.js"; import { @@ -200,6 +201,35 @@ export async function handleOpenAiHttpRequest( const runId = `chatcmpl_${randomUUID()}`; const deps = createDefaultDeps(); + // Trigger message_received hook for API requests (enables voice logging, etc.) + const hookRunner = getGlobalHookRunner(); + if (hookRunner?.hasHooks("message_received")) { + void hookRunner + .runMessageReceived( + { + from: user ?? "api", + content: prompt.message, + timestamp: Date.now(), + metadata: { + apiUser: user, + model, + sessionKey, + source: "chat_completions", + runId, + }, + }, + { + channelId: "api", + accountId: undefined, + conversationId: sessionKey, + }, + ) + .catch((err) => { + // Fire-and-forget, just log errors + console.error(`[openai-http] message_received hook failed: ${String(err)}`); + }); + } + if (!stream) { try { const result = await agentCommand( @@ -225,6 +255,24 @@ export async function handleOpenAiHttpRequest( .join("\n\n") : "No response from OpenClaw."; + // Trigger message_sent hook for API responses (non-streaming only for now) + if (hookRunner?.hasHooks("message_sent")) { + void hookRunner + .runMessageSent( + { + to: user ?? "api", + content, + success: true, + }, + { + channelId: "api", + accountId: undefined, + conversationId: sessionKey, + }, + ) + .catch(() => {}); + } + sendJson(res, 200, { id: runId, object: "chat.completion",