feat(llm-gateway): add system prompt injection to OpenAI chat proxy
- Add injectSystemPromptOpenAI() for OpenAI messages format (role: system) - Integrate injection into createOpenAIChatProxy before upstream call - Update audit logs to track injection status - Enables brand identity override for both Anthropic and OpenAI endpoints Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a4fa4f47d6
commit
5683185a47
|
|
@ -113,6 +113,52 @@ export async function injectSystemPrompt(
|
||||||
return { system, applied: false, ruleIds: [] };
|
return { system, applied: false, ruleIds: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Inject into OpenAI messages format ───
|
||||||
|
|
||||||
|
export async function injectSystemPromptOpenAI(
|
||||||
|
messages: Array<{ role: string; content: any }>,
|
||||||
|
model: string,
|
||||||
|
apiKeyId: string,
|
||||||
|
): Promise<{ messages: Array<{ role: string; content: any }>; applied: boolean; ruleIds: string[] }> {
|
||||||
|
const allRules = await loadRules();
|
||||||
|
|
||||||
|
const applicableRules = allRules.filter(
|
||||||
|
(r) => matchesModel(r.matchModels, model) && matchesKey(r.matchKeyIds, apiKeyId),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (applicableRules.length === 0) {
|
||||||
|
return { messages, applied: false, ruleIds: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const prependRules = applicableRules.filter((r) => r.position === 'prepend');
|
||||||
|
const appendRules = applicableRules.filter((r) => r.position === 'append');
|
||||||
|
|
||||||
|
const prependText = prependRules.map((r) => r.content).join('\n\n');
|
||||||
|
const appendText = appendRules.map((r) => r.content).join('\n\n');
|
||||||
|
const ruleIds = applicableRules.map((r) => r.id);
|
||||||
|
|
||||||
|
if (!prependText && !appendText) {
|
||||||
|
return { messages, applied: false, ruleIds: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const injectedContent = [prependText, appendText].filter(Boolean).join('\n\n');
|
||||||
|
const result = [...messages];
|
||||||
|
|
||||||
|
// Find existing system message
|
||||||
|
const systemIdx = result.findIndex((m) => m.role === 'system');
|
||||||
|
if (systemIdx >= 0) {
|
||||||
|
// Merge into existing system message
|
||||||
|
const existing = typeof result[systemIdx].content === 'string' ? result[systemIdx].content : '';
|
||||||
|
const parts = [prependText, existing, appendText].filter(Boolean);
|
||||||
|
result[systemIdx] = { ...result[systemIdx], content: parts.join('\n\n') };
|
||||||
|
} else {
|
||||||
|
// Insert new system message at the beginning
|
||||||
|
result.unshift({ role: 'system', content: injectedContent });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { messages: result, applied: true, ruleIds };
|
||||||
|
}
|
||||||
|
|
||||||
export function clearInjectionRulesCache(): void {
|
export function clearInjectionRulesCache(): void {
|
||||||
cachedRules = [];
|
cachedRules = [];
|
||||||
cacheTimestamp = 0;
|
cacheTimestamp = 0;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { ApiKeyRecord } from '../types';
|
||||||
import { isModelAllowed } from '../middleware/auth';
|
import { isModelAllowed } from '../middleware/auth';
|
||||||
import { recordFromOpenAIResponse } from '../logging/usage-tracker';
|
import { recordFromOpenAIResponse } from '../logging/usage-tracker';
|
||||||
import { recordAudit } from '../logging/audit-logger';
|
import { recordAudit } from '../logging/audit-logger';
|
||||||
|
import { injectSystemPromptOpenAI } from '../injection/system-prompt-injector';
|
||||||
import { pipeSSEStream } from './stream-pipe';
|
import { pipeSSEStream } from './stream-pipe';
|
||||||
import { sanitizeOpenAIResponse, sanitizeOpenAIEmbeddingResponse, buildOpenAIStreamTransform } from './response-sanitizer';
|
import { sanitizeOpenAIResponse, sanitizeOpenAIEmbeddingResponse, buildOpenAIStreamTransform } from './response-sanitizer';
|
||||||
|
|
||||||
|
|
@ -63,7 +64,7 @@ export function createOpenAIEmbeddingsProxy(config: GatewayConfig) {
|
||||||
requestIp: clientIp,
|
requestIp: clientIp,
|
||||||
contentFiltered: false,
|
contentFiltered: false,
|
||||||
filterRuleId: null,
|
filterRuleId: null,
|
||||||
injectionApplied: false,
|
injectionApplied: injection.applied,
|
||||||
responseStatus: 502,
|
responseStatus: 502,
|
||||||
durationMs: Date.now() - startTime,
|
durationMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
|
|
@ -105,7 +106,7 @@ export function createOpenAIEmbeddingsProxy(config: GatewayConfig) {
|
||||||
requestIp: clientIp,
|
requestIp: clientIp,
|
||||||
contentFiltered: false,
|
contentFiltered: false,
|
||||||
filterRuleId: null,
|
filterRuleId: null,
|
||||||
injectionApplied: false,
|
injectionApplied: injection.applied,
|
||||||
responseStatus: upstreamResponse.status,
|
responseStatus: upstreamResponse.status,
|
||||||
durationMs,
|
durationMs,
|
||||||
});
|
});
|
||||||
|
|
@ -175,6 +176,10 @@ export function createOpenAIChatProxy(config: GatewayConfig) {
|
||||||
// Replace model for upstream
|
// Replace model for upstream
|
||||||
body.model = effectiveModel;
|
body.model = effectiveModel;
|
||||||
|
|
||||||
|
// Inject system prompt (identity, regulatory content)
|
||||||
|
const injection = await injectSystemPromptOpenAI(body.messages || [], effectiveModel, apiKeyRecord.id);
|
||||||
|
body.messages = injection.messages;
|
||||||
|
|
||||||
let upstreamResponse: Response;
|
let upstreamResponse: Response;
|
||||||
try {
|
try {
|
||||||
// openaiUpstreamUrl may already include /v1 (e.g., "https://host:8443/v1")
|
// openaiUpstreamUrl may already include /v1 (e.g., "https://host:8443/v1")
|
||||||
|
|
@ -196,7 +201,7 @@ export function createOpenAIChatProxy(config: GatewayConfig) {
|
||||||
requestIp: clientIp,
|
requestIp: clientIp,
|
||||||
contentFiltered: false,
|
contentFiltered: false,
|
||||||
filterRuleId: null,
|
filterRuleId: null,
|
||||||
injectionApplied: false,
|
injectionApplied: injection.applied,
|
||||||
responseStatus: 502,
|
responseStatus: 502,
|
||||||
durationMs: Date.now() - startTime,
|
durationMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
|
|
@ -236,7 +241,7 @@ export function createOpenAIChatProxy(config: GatewayConfig) {
|
||||||
requestIp: clientIp,
|
requestIp: clientIp,
|
||||||
contentFiltered: false,
|
contentFiltered: false,
|
||||||
filterRuleId: null,
|
filterRuleId: null,
|
||||||
injectionApplied: false,
|
injectionApplied: injection.applied,
|
||||||
responseStatus: upstreamResponse.status,
|
responseStatus: upstreamResponse.status,
|
||||||
durationMs: Date.now() - startTime,
|
durationMs: Date.now() - startTime,
|
||||||
});
|
});
|
||||||
|
|
@ -259,7 +264,7 @@ export function createOpenAIChatProxy(config: GatewayConfig) {
|
||||||
requestIp: clientIp,
|
requestIp: clientIp,
|
||||||
contentFiltered: false,
|
contentFiltered: false,
|
||||||
filterRuleId: null,
|
filterRuleId: null,
|
||||||
injectionApplied: false,
|
injectionApplied: injection.applied,
|
||||||
responseStatus: upstreamResponse.status,
|
responseStatus: upstreamResponse.status,
|
||||||
durationMs,
|
durationMs,
|
||||||
});
|
});
|
||||||
|
|
@ -282,7 +287,7 @@ export function createOpenAIChatProxy(config: GatewayConfig) {
|
||||||
requestIp: clientIp,
|
requestIp: clientIp,
|
||||||
contentFiltered: false,
|
contentFiltered: false,
|
||||||
filterRuleId: null,
|
filterRuleId: null,
|
||||||
injectionApplied: false,
|
injectionApplied: injection.applied,
|
||||||
responseStatus: upstreamResponse.status,
|
responseStatus: upstreamResponse.status,
|
||||||
durationMs,
|
durationMs,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue