mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 00:02:20 +03:00
Integrated into release/v3.8.36 (#3501 chatCore extraction stack 5/13)
This commit is contained in:
committed by
GitHub
parent
812f2f20a1
commit
eb1920bf91
@@ -173,6 +173,7 @@ import {
|
||||
} from "./chatCore/compressionComboPredicates.ts";
|
||||
import { emitOutputStyleTelemetry } from "./chatCore/outputStyleTelemetry.ts";
|
||||
import { writeCompressionAnalytics } from "./chatCore/compressionAnalyticsWrite.ts";
|
||||
import { runPluginOnRequestHook } from "./chatCore/pluginOnRequest.ts";
|
||||
import { recordContextEditingTelemetryHook } from "./chatCore/contextEditingTelemetry.ts";
|
||||
import { recordCompressionCacheStats } from "./chatCore/compressionCacheStats.ts";
|
||||
import { writeCavemanOutputAnalytics } from "./chatCore/cavemanOutputAnalytics.ts";
|
||||
@@ -385,50 +386,24 @@ export async function handleChatCore({
|
||||
body = injectSystemPrompt(body);
|
||||
// ── Plugin onRequest hook ──
|
||||
// Dynamic import cached by Node.js after first call — minimal overhead
|
||||
try {
|
||||
const { runOnRequest } = await import("@/lib/plugins/hooks");
|
||||
const pluginCtx = {
|
||||
requestId: traceId,
|
||||
body,
|
||||
model,
|
||||
provider,
|
||||
apiKeyInfo,
|
||||
metadata: {},
|
||||
const pluginGate = await runPluginOnRequestHook({
|
||||
requestId: traceId,
|
||||
body,
|
||||
model,
|
||||
provider,
|
||||
apiKeyInfo,
|
||||
log,
|
||||
});
|
||||
if (pluginGate.blocked) {
|
||||
return {
|
||||
success: false,
|
||||
status: 403,
|
||||
error: "Request blocked by plugin",
|
||||
response: pluginGate.response,
|
||||
};
|
||||
const pluginResult = await runOnRequest(pluginCtx);
|
||||
if (pluginResult?.blocked) {
|
||||
log?.info?.("PLUGIN", `Request blocked by plugin`);
|
||||
return {
|
||||
success: false,
|
||||
status: 403,
|
||||
error: "Request blocked by plugin",
|
||||
response: pluginResult.response
|
||||
? new Response(JSON.stringify(pluginResult.response), {
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
: new Response(
|
||||
JSON.stringify({
|
||||
error: { message: "Request blocked by plugin", type: "plugin_block" },
|
||||
}),
|
||||
{
|
||||
status: 403,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
if (pluginResult?.body) {
|
||||
body = pluginResult.body;
|
||||
}
|
||||
if (pluginResult?.metadata) {
|
||||
Object.assign(pluginCtx.metadata, pluginResult.metadata);
|
||||
}
|
||||
} catch (pluginErr) {
|
||||
log?.debug?.(
|
||||
"PLUGIN",
|
||||
`onRequest hook error (non-fatal): ${pluginErr instanceof Error ? pluginErr.message : String(pluginErr)}`
|
||||
);
|
||||
}
|
||||
if (pluginGate.body) {
|
||||
body = pluginGate.body;
|
||||
}
|
||||
|
||||
let effectiveServiceTier: EffectiveServiceTier = "standard";
|
||||
|
||||
65
open-sse/handlers/chatCore/pluginOnRequest.ts
Normal file
65
open-sse/handlers/chatCore/pluginOnRequest.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* chatCore plugin onRequest hook (Quality Gate v2 / Fase 9 — chatCore god-file decomposition,
|
||||
* #3501).
|
||||
*
|
||||
* Extracted from handleChatCore's request entry: run the registered plugin `onRequest` hooks. The
|
||||
* hook may block the request (→ the handler returns a 403), rewrite the body (→ the handler
|
||||
* reassigns `body`), or do nothing. Fail-open — a misbehaving plugin is logged and ignored. Returns
|
||||
* a discriminated result so the early-return + body reassignment stay in the handler; behaviour is
|
||||
* byte-identical to the previous inline block.
|
||||
*/
|
||||
|
||||
type LoggerLike =
|
||||
| { info?: (...args: unknown[]) => void; debug?: (...args: unknown[]) => void }
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
export type PluginOnRequestGate =
|
||||
| { blocked: true; response: Response }
|
||||
| { blocked: false; body?: unknown };
|
||||
|
||||
const JSON_HEADERS = { status: 403, headers: { "Content-Type": "application/json" } } as const;
|
||||
|
||||
export async function runPluginOnRequestHook(args: {
|
||||
requestId: string;
|
||||
body: unknown;
|
||||
model: string | null | undefined;
|
||||
provider: string | null | undefined;
|
||||
apiKeyInfo: unknown;
|
||||
log?: LoggerLike;
|
||||
}): Promise<PluginOnRequestGate> {
|
||||
try {
|
||||
const { runOnRequest } = await import("@/lib/plugins/hooks");
|
||||
const pluginCtx = {
|
||||
requestId: args.requestId,
|
||||
body: args.body,
|
||||
model: args.model,
|
||||
provider: args.provider,
|
||||
apiKeyInfo: args.apiKeyInfo,
|
||||
metadata: {},
|
||||
};
|
||||
const pluginResult = await runOnRequest(pluginCtx);
|
||||
if (pluginResult?.blocked) {
|
||||
args.log?.info?.("PLUGIN", `Request blocked by plugin`);
|
||||
const response = pluginResult.response
|
||||
? new Response(JSON.stringify(pluginResult.response), JSON_HEADERS)
|
||||
: new Response(
|
||||
JSON.stringify({
|
||||
error: { message: "Request blocked by plugin", type: "plugin_block" },
|
||||
}),
|
||||
JSON_HEADERS
|
||||
);
|
||||
return { blocked: true, response };
|
||||
}
|
||||
if (pluginResult?.metadata) {
|
||||
Object.assign(pluginCtx.metadata, pluginResult.metadata);
|
||||
}
|
||||
return { blocked: false, body: pluginResult?.body };
|
||||
} catch (pluginErr) {
|
||||
args.log?.debug?.(
|
||||
"PLUGIN",
|
||||
`onRequest hook error (non-fatal): ${pluginErr instanceof Error ? pluginErr.message : String(pluginErr)}`
|
||||
);
|
||||
return { blocked: false };
|
||||
}
|
||||
}
|
||||
73
tests/unit/chatcore-plugin-onrequest.test.ts
Normal file
73
tests/unit/chatcore-plugin-onrequest.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// Characterization of runPluginOnRequestHook — the plugin onRequest gate extracted from
|
||||
// handleChatCore's request entry (chatCore god-file decomposition, #3501). Hooks are in-memory.
|
||||
// Locks: the discriminated result — blocked (403 Response) vs body-rewrite vs pass-through — and
|
||||
// fail-open on a throwing hook.
|
||||
import { test, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { registerHook, unregisterHook } = await import("../../src/lib/plugins/hooks.ts");
|
||||
const { runPluginOnRequestHook } = await import(
|
||||
"../../open-sse/handlers/chatCore/pluginOnRequest.ts"
|
||||
);
|
||||
|
||||
const PLUGIN = "test-onrequest-plugin";
|
||||
|
||||
afterEach(() => {
|
||||
unregisterHook("onRequest", PLUGIN);
|
||||
});
|
||||
|
||||
function baseArgs(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
requestId: "req-1",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
model: "gpt-x",
|
||||
provider: "openai",
|
||||
apiKeyInfo: null,
|
||||
...overrides,
|
||||
} as Parameters<typeof runPluginOnRequestHook>[0];
|
||||
}
|
||||
|
||||
test("no registered hooks → pass-through (blocked:false, no body)", async () => {
|
||||
const gate = await runPluginOnRequestHook(baseArgs());
|
||||
assert.equal(gate.blocked, false);
|
||||
});
|
||||
|
||||
test("a blocking hook → blocked:true with a 403 JSON Response", async () => {
|
||||
registerHook("onRequest", PLUGIN, async () => ({
|
||||
blocked: true,
|
||||
response: { error: "nope" },
|
||||
}));
|
||||
const gate = await runPluginOnRequestHook(baseArgs());
|
||||
assert.equal(gate.blocked, true);
|
||||
if (!gate.blocked) return;
|
||||
assert.equal(gate.response.status, 403);
|
||||
const payload = await gate.response.json();
|
||||
assert.deepEqual(payload, { error: "nope" });
|
||||
});
|
||||
|
||||
test("a blocking hook without a response → generic plugin_block 403", async () => {
|
||||
registerHook("onRequest", PLUGIN, async () => ({ blocked: true }));
|
||||
const gate = await runPluginOnRequestHook(baseArgs());
|
||||
assert.equal(gate.blocked, true);
|
||||
if (!gate.blocked) return;
|
||||
assert.equal(gate.response.status, 403);
|
||||
const payload = (await gate.response.json()) as { error?: { type?: string } };
|
||||
assert.equal(payload.error?.type, "plugin_block");
|
||||
});
|
||||
|
||||
test("a body-rewriting hook → blocked:false with the new body", async () => {
|
||||
const rewritten = { messages: [{ role: "user", content: "rewritten" }] };
|
||||
registerHook("onRequest", PLUGIN, async () => ({ body: rewritten }));
|
||||
const gate = await runPluginOnRequestHook(baseArgs());
|
||||
assert.equal(gate.blocked, false);
|
||||
if (gate.blocked) return;
|
||||
assert.deepEqual(gate.body, rewritten);
|
||||
});
|
||||
|
||||
test("a throwing hook → fail-open pass-through (blocked:false)", async () => {
|
||||
registerHook("onRequest", PLUGIN, async () => {
|
||||
throw new Error("boom");
|
||||
});
|
||||
const gate = await runPluginOnRequestHook(baseArgs());
|
||||
assert.equal(gate.blocked, false);
|
||||
});
|
||||
Reference in New Issue
Block a user