mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-15 03:32:21 +03:00
feat(plugins): expose client request headers in plugin context (#9570)
This commit is contained in:
1
changelog.d/features/9570-plugin-context-headers.md
Normal file
1
changelog.d/features/9570-plugin-context-headers.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(plugins):** expose client request headers in plugin onRequest/onResponse context (#9570)
|
||||
@@ -490,6 +490,7 @@ export async function handleChatCore({
|
||||
model,
|
||||
provider,
|
||||
apiKeyInfo,
|
||||
headers: clientRawRequest?.headers,
|
||||
log,
|
||||
});
|
||||
if (pluginGate.blocked) {
|
||||
@@ -4608,6 +4609,7 @@ export async function handleChatCore({
|
||||
model,
|
||||
provider,
|
||||
apiKeyInfo,
|
||||
headers: clientRawRequest?.headers,
|
||||
response: { status: 200, data: translatedResponse },
|
||||
});
|
||||
|
||||
@@ -4998,6 +5000,7 @@ export async function handleChatCore({
|
||||
model,
|
||||
provider,
|
||||
apiKeyInfo,
|
||||
headers: clientRawRequest?.headers,
|
||||
response: { status: 200, streamed: true },
|
||||
});
|
||||
|
||||
|
||||
@@ -10,13 +10,10 @@
|
||||
*/
|
||||
|
||||
type LoggerLike =
|
||||
| { info?: (...args: unknown[]) => void; debug?: (...args: unknown[]) => void }
|
||||
| null
|
||||
| undefined;
|
||||
{ info?: (...args: unknown[]) => void; debug?: (...args: unknown[]) => void } | null | undefined;
|
||||
|
||||
export type PluginOnRequestGate =
|
||||
| { blocked: true; response: Response }
|
||||
| { blocked: false; body?: unknown };
|
||||
{ blocked: true; response: Response } | { blocked: false; body?: unknown };
|
||||
|
||||
const JSON_HEADERS = { status: 403, headers: { "Content-Type": "application/json" } } as const;
|
||||
|
||||
@@ -26,6 +23,7 @@ export async function runPluginOnRequestHook(args: {
|
||||
model: string | null | undefined;
|
||||
provider: string | null | undefined;
|
||||
apiKeyInfo: unknown;
|
||||
headers?: Record<string, string | string[] | undefined>;
|
||||
log?: LoggerLike;
|
||||
}): Promise<PluginOnRequestGate> {
|
||||
try {
|
||||
@@ -36,6 +34,7 @@ export async function runPluginOnRequestHook(args: {
|
||||
model: args.model,
|
||||
provider: args.provider,
|
||||
apiKeyInfo: args.apiKeyInfo,
|
||||
headers: args.headers,
|
||||
metadata: {},
|
||||
};
|
||||
const pluginResult = await runOnRequest(pluginCtx);
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function runPluginOnResponseHook(args: {
|
||||
model: string | null | undefined;
|
||||
provider: string | null | undefined;
|
||||
apiKeyInfo: unknown;
|
||||
headers?: Record<string, string | string[] | undefined>;
|
||||
response: PluginOnResponsePayload;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
@@ -35,6 +36,7 @@ export async function runPluginOnResponseHook(args: {
|
||||
model: args.model,
|
||||
provider: args.provider,
|
||||
apiKeyInfo: args.apiKeyInfo,
|
||||
headers: args.headers,
|
||||
metadata: {},
|
||||
},
|
||||
args.response
|
||||
|
||||
@@ -40,6 +40,7 @@ export const BUILTIN_EVENTS = [
|
||||
"onActivate",
|
||||
"onDeactivate",
|
||||
"onUninstall",
|
||||
"onStreamComplete",
|
||||
] as const;
|
||||
|
||||
export type BuiltinEvent = (typeof BUILTIN_EVENTS)[number];
|
||||
@@ -251,6 +252,35 @@ export interface Plugin {
|
||||
onActivate?: (payload: unknown) => Promise<void> | void;
|
||||
onDeactivate?: (payload: unknown) => Promise<void> | void;
|
||||
onUninstall?: (payload: unknown) => Promise<void> | void;
|
||||
onStreamComplete?: (payload: PluginOnStreamCompletePayload) => Promise<void> | void;
|
||||
}
|
||||
|
||||
// ── onStreamComplete event types ──
|
||||
|
||||
export type PluginOnStreamCompletePayload = {
|
||||
status: number;
|
||||
usage?: {
|
||||
prompt_tokens?: number;
|
||||
completion_tokens?: number;
|
||||
reasoning_tokens?: number;
|
||||
cache_read_input_tokens?: number;
|
||||
cache_creation_input_tokens?: number;
|
||||
};
|
||||
timing?: {
|
||||
latencyMs: number;
|
||||
ttft?: number;
|
||||
};
|
||||
model?: string;
|
||||
provider?: string;
|
||||
errorCode?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Run onStreamComplete hooks — fire-and-forget notification with usage/timing data.
|
||||
* Called when an SSE stream is fully consumed and usage/timing data is available.
|
||||
*/
|
||||
export async function runOnStreamComplete(payload: PluginOnStreamCompletePayload): Promise<void> {
|
||||
await emitHook("onStreamComplete", payload);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,9 +6,8 @@ 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 { runPluginOnRequestHook } =
|
||||
await import("../../open-sse/handlers/chatCore/pluginOnRequest.ts");
|
||||
|
||||
const PLUGIN = "test-onrequest-plugin";
|
||||
|
||||
@@ -32,6 +31,31 @@ test("no registered hooks → pass-through (blocked:false, no body)", async () =
|
||||
assert.equal(gate.blocked, false);
|
||||
});
|
||||
|
||||
test("headers passed to the hook are visible in PluginContext", async () => {
|
||||
let capturedCtx: Record<string, unknown> | undefined;
|
||||
registerHook("onRequest", "test-ctx-headers", async (ctx: Record<string, unknown>) => {
|
||||
capturedCtx = ctx;
|
||||
return {};
|
||||
});
|
||||
const testHeaders = { "x-trace-id": "abc-123", "x-request-id": "req-456" };
|
||||
const gate = await runPluginOnRequestHook(baseArgs({ headers: testHeaders }));
|
||||
assert.equal(gate.blocked, false);
|
||||
assert.ok(capturedCtx, "expected the hook to be invoked");
|
||||
assert.deepEqual(capturedCtx!.headers, testHeaders);
|
||||
});
|
||||
|
||||
test("no headers arg → backward compatible (undefined in ctx)", async () => {
|
||||
let capturedCtx: Record<string, unknown> | undefined;
|
||||
registerHook("onRequest", "test-ctx-noheaders", async (ctx: Record<string, unknown>) => {
|
||||
capturedCtx = ctx;
|
||||
return {};
|
||||
});
|
||||
const gate = await runPluginOnRequestHook(baseArgs());
|
||||
assert.equal(gate.blocked, false);
|
||||
assert.ok(capturedCtx, "expected the hook to be invoked");
|
||||
assert.equal(capturedCtx!.headers, undefined);
|
||||
});
|
||||
|
||||
test("a blocking hook → blocked:true with a 403 JSON Response", async () => {
|
||||
registerHook("onRequest", PLUGIN, async () => ({
|
||||
blocked: true,
|
||||
|
||||
Reference in New Issue
Block a user