mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
chatCore #3501: extract runPluginOnResponseHook. Integrated into release/v3.8.35.
This commit is contained in:
committed by
GitHub
parent
8a28a3ae5b
commit
2566f44f60
@@ -170,6 +170,7 @@ import { recordCompressionCacheStats } from "./chatCore/compressionCacheStats.ts
|
||||
import { writeCavemanOutputAnalytics } from "./chatCore/cavemanOutputAnalytics.ts";
|
||||
import { scheduleQuotaShareConsumption } from "./chatCore/quotaShareConsumption.ts";
|
||||
import { emitRequestGamificationEvent } from "./chatCore/gamificationEvent.ts";
|
||||
import { runPluginOnResponseHook } from "./chatCore/pluginOnResponse.ts";
|
||||
import {
|
||||
appendNonStreamingSseTerminalSignal,
|
||||
type NonStreamingSseTerminalState,
|
||||
@@ -4205,15 +4206,7 @@ export async function handleChatCore({
|
||||
await emitRequestGamificationEvent({ apiKeyId: apiKeyInfo?.id, model, provider });
|
||||
|
||||
// ── Plugin onResponse hook (fire-and-forget) ──
|
||||
try {
|
||||
const { runOnResponse } = await import("@/lib/plugins/hooks");
|
||||
runOnResponse(
|
||||
{ requestId: traceId, body, model, provider, apiKeyInfo, metadata: {} },
|
||||
{ status: 200 }
|
||||
).catch(() => {});
|
||||
} catch (_) {
|
||||
/* plugin onResponse optional */
|
||||
}
|
||||
await runPluginOnResponseHook({ requestId: traceId, body, model, provider, apiKeyInfo });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
34
open-sse/handlers/chatCore/pluginOnResponse.ts
Normal file
34
open-sse/handlers/chatCore/pluginOnResponse.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* chatCore plugin onResponse hook (Quality Gate v2 / Fase 9 — chatCore god-file decomposition,
|
||||
* #3501).
|
||||
*
|
||||
* Extracted from handleChatCore's streaming finalization: runs the registered plugin `onResponse`
|
||||
* hooks for a completed (status 200) response. Fire-and-forget and fail-open — the inner run is
|
||||
* not awaited and both the dynamic import and the run swallow their own errors, so a misbehaving
|
||||
* plugin never affects the response. Behaviour is byte-identical to the previous inline block.
|
||||
*/
|
||||
|
||||
export async function runPluginOnResponseHook(args: {
|
||||
requestId: string;
|
||||
body: unknown;
|
||||
model: string | null | undefined;
|
||||
provider: string | null | undefined;
|
||||
apiKeyInfo: unknown;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const { runOnResponse } = await import("@/lib/plugins/hooks");
|
||||
runOnResponse(
|
||||
{
|
||||
requestId: args.requestId,
|
||||
body: args.body,
|
||||
model: args.model,
|
||||
provider: args.provider,
|
||||
apiKeyInfo: args.apiKeyInfo,
|
||||
metadata: {},
|
||||
},
|
||||
{ status: 200 }
|
||||
).catch(() => {});
|
||||
} catch (_) {
|
||||
/* plugin onResponse optional */
|
||||
}
|
||||
}
|
||||
73
tests/unit/chatcore-plugin-onresponse.test.ts
Normal file
73
tests/unit/chatcore-plugin-onresponse.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// Characterization of runPluginOnResponseHook — the plugin onResponse hook extracted from
|
||||
// handleChatCore's streaming finalization (chatCore god-file decomposition, #3501). Hooks are
|
||||
// in-memory (no DB). Locks: a registered onResponse hook receives the request context + status-200
|
||||
// response, and the helper is fire-and-forget / fail-open (no registered hooks → no-op).
|
||||
import { test, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { registerHook, unregisterHook } = await import("../../src/lib/plugins/hooks.ts");
|
||||
const { runPluginOnResponseHook } = await import(
|
||||
"../../open-sse/handlers/chatCore/pluginOnResponse.ts"
|
||||
);
|
||||
|
||||
async function waitFor(pred: () => boolean, timeoutMs = 2000): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline && !pred()) {
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
}
|
||||
}
|
||||
|
||||
after(() => {
|
||||
unregisterHook("onResponse", "test-onresponse-plugin");
|
||||
});
|
||||
|
||||
test("no registered hooks → resolves without throwing (no-op)", async () => {
|
||||
await assert.doesNotReject(
|
||||
runPluginOnResponseHook({
|
||||
requestId: "req-noop",
|
||||
body: { messages: [] },
|
||||
model: "gpt-x",
|
||||
provider: "openai",
|
||||
apiKeyInfo: null,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("registered onResponse hook receives the request context and status-200 response", async () => {
|
||||
let captured: Record<string, unknown> | undefined;
|
||||
registerHook("onResponse", "test-onresponse-plugin", async (ctx: Record<string, unknown>) => {
|
||||
captured = ctx;
|
||||
return {};
|
||||
});
|
||||
|
||||
await runPluginOnResponseHook({
|
||||
requestId: "req-42",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
model: "gpt-4o",
|
||||
provider: "openai",
|
||||
apiKeyInfo: { id: "key-1" },
|
||||
});
|
||||
|
||||
await waitFor(() => captured !== undefined);
|
||||
assert.ok(captured, "expected the onResponse hook to be invoked");
|
||||
assert.equal(captured!.requestId, "req-42");
|
||||
assert.equal(captured!.model, "gpt-4o");
|
||||
assert.equal(captured!.provider, "openai");
|
||||
assert.deepEqual(captured!.response, { status: 200 });
|
||||
});
|
||||
|
||||
test("a throwing hook never rejects the caller (fail-open)", async () => {
|
||||
registerHook("onResponse", "test-onresponse-plugin", async () => {
|
||||
throw new Error("boom");
|
||||
});
|
||||
await assert.doesNotReject(
|
||||
runPluginOnResponseHook({
|
||||
requestId: "req-throw",
|
||||
body: {},
|
||||
model: "m",
|
||||
provider: "p",
|
||||
apiKeyInfo: null,
|
||||
})
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
});
|
||||
Reference in New Issue
Block a user