From: mkroemer Date: Fri, 9 Jan 2026 20:06:49 +0000 (+0100) Subject: fix: Update type system for multimodal content support X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=1b8cd6b6714241effcc0368c4780d95ab6a1b7ae;p=sgasser-llm-shield.git fix: Update type system for multimodal content support - Update ChatMessage interface to accept MessageContent type - Fix masking.ts to handle multimodal content - Fix redact.ts to extract text before processing - Fix import order and formatting per biome linter --- diff --git a/docker-compose.yml b/docker-compose.yml index 64d538a..f891957 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,8 @@ services: build: . ports: - "3000:3000" + env_file: + - .env environment: - PRESIDIO_URL=http://presidio-analyzer:3000 volumes: diff --git a/src/routes/proxy.ts b/src/routes/proxy.ts index d683405..3f23175 100644 --- a/src/routes/proxy.ts +++ b/src/routes/proxy.ts @@ -18,20 +18,22 @@ import type { ChatMessage, LLMResult, } from "../services/llm-client"; -import { extractTextContent, type ContentPart } from "../utils/content"; import { logRequest, type RequestLogData } from "../services/logger"; import { unmaskResponse } from "../services/masking"; import { createUnmaskingStream } from "../services/stream-transformer"; +import { type ContentPart, extractTextContent } from "../utils/content"; // Request validation schema const ChatCompletionSchema = z .object({ messages: z .array( - z.object({ - role: z.enum(["system", "user", "assistant", "tool"]), - content: z.union([z.string(), z.array(z.any()), z.null()]).optional(), - }).passthrough(), // Allow additional fields like name, tool_calls, etc. + z + .object({ + role: z.enum(["system", "user", "assistant", "tool"]), + content: z.union([z.string(), z.array(z.any()), z.null()]).optional(), + }) + .passthrough(), // Allow additional fields like name, tool_calls, etc. ) .min(1, "At least one message is required"), }) diff --git a/src/secrets/redact.ts b/src/secrets/redact.ts index 3a2c2a3..636f2fc 100644 --- a/src/secrets/redact.ts +++ b/src/secrets/redact.ts @@ -1,5 +1,6 @@ import type { SecretsDetectionConfig } from "../config"; import type { ChatCompletionResponse, ChatMessage } from "../services/llm-client"; +import { extractTextContent } from "../utils/content"; import type { SecretsRedaction } from "./detect"; /** @@ -143,8 +144,12 @@ export function redactMessagesSecrets( const redacted = messages.map((msg, i) => { const redactions = redactionsByMessage[i] || []; - const { redacted: redactedContent } = redactSecrets(msg.content, redactions, config, context); - return { ...msg, content: redactedContent }; + const text = extractTextContent(msg.content); + const { redacted: redactedContent } = redactSecrets(text, redactions, config, context); + + // If original content was a string, return redacted string + // Otherwise return original content (arrays are handled in proxy.ts) + return { ...msg, content: typeof msg.content === "string" ? redactedContent : msg.content }; }); return { redacted, context }; @@ -218,7 +223,10 @@ export function unredactResponse( ...choice, message: { ...choice.message, - content: unredactSecrets(choice.message.content, context), + content: + typeof choice.message.content === "string" + ? unredactSecrets(choice.message.content, context) + : choice.message.content, }, })), }; diff --git a/src/services/llm-client.ts b/src/services/llm-client.ts index 8745899..451ccfd 100644 --- a/src/services/llm-client.ts +++ b/src/services/llm-client.ts @@ -1,11 +1,13 @@ import type { LocalProvider, UpstreamProvider } from "../config"; +import type { MessageContent } from "../utils/content"; /** * OpenAI-compatible message format + * Supports both text-only (content: string) and multimodal (content: array) formats */ export interface ChatMessage { role: "system" | "user" | "assistant"; - content: string; + content: MessageContent; } /** diff --git a/src/services/masking.ts b/src/services/masking.ts index fa5da21..f11cf52 100644 --- a/src/services/masking.ts +++ b/src/services/masking.ts @@ -1,4 +1,5 @@ import type { MaskingConfig } from "../config"; +import { extractTextContent } from "../utils/content"; import type { ChatCompletionResponse, ChatMessage } from "./llm-client"; import type { PIIEntity } from "./pii-detector"; @@ -117,8 +118,12 @@ export function maskMessages( const masked = messages.map((msg, i) => { const entities = entitiesByMessage[i] || []; - const { masked: maskedContent } = mask(msg.content, entities, context); - return { ...msg, content: maskedContent }; + const text = extractTextContent(msg.content); + const { masked: maskedContent } = mask(text, entities, context); + + // If original content was a string, return masked string + // Otherwise return original content (arrays are handled elsewhere) + return { ...msg, content: typeof msg.content === "string" ? maskedContent : msg.content }; }); return { masked, context }; @@ -197,7 +202,10 @@ export function unmaskResponse( ...choice, message: { ...choice.message, - content: unmask(choice.message.content, context, config), + content: + typeof choice.message.content === "string" + ? unmask(choice.message.content, context, config) + : choice.message.content, }, })), }; diff --git a/src/services/pii-detector.ts b/src/services/pii-detector.ts index 5cc9ad3..4d965b8 100644 --- a/src/services/pii-detector.ts +++ b/src/services/pii-detector.ts @@ -1,10 +1,10 @@ import { getConfig } from "../config"; +import { extractTextContent, type MessageContent } from "../utils/content"; import { getLanguageDetector, type LanguageDetectionResult, type SupportedLanguage, } from "./language-detector"; -import { extractTextContent, type MessageContent } from "../utils/content"; export interface PIIEntity { entity_type: string;