build: .
ports:
- "3000:3000"
+ env_file:
+ - .env
environment:
- PRESIDIO_URL=http://presidio-analyzer:3000
volumes:
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"),
})
import type { SecretsDetectionConfig } from "../config";
import type { ChatCompletionResponse, ChatMessage } from "../services/llm-client";
+import { extractTextContent } from "../utils/content";
import type { SecretsRedaction } from "./detect";
/**
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 };
...choice,
message: {
...choice.message,
- content: unredactSecrets(choice.message.content, context),
+ content:
+ typeof choice.message.content === "string"
+ ? unredactSecrets(choice.message.content, context)
+ : choice.message.content,
},
})),
};
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;
}
/**
import type { MaskingConfig } from "../config";
+import { extractTextContent } from "../utils/content";
import type { ChatCompletionResponse, ChatMessage } from "./llm-client";
import type { PIIEntity } from "./pii-detector";
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 };
...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,
},
})),
};
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;