content-moderation-sdk 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/content-moderation-sdk.jpg +0 -0
- package/dist/azure.d.ts +38 -0
- package/dist/azure.d.ts.map +1 -0
- package/dist/azure.js +114 -0
- package/dist/azure.js.map +1 -0
- package/dist/core.d.ts +3 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +313 -0
- package/dist/core.js.map +1 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +77 -0
- package/dist/errors.js.map +1 -0
- package/dist/http.d.ts +9 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +87 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/mistral.d.ts +23 -0
- package/dist/mistral.d.ts.map +1 -0
- package/dist/mistral.js +81 -0
- package/dist/mistral.js.map +1 -0
- package/dist/openai.d.ts +28 -0
- package/dist/openai.d.ts.map +1 -0
- package/dist/openai.js +105 -0
- package/dist/openai.js.map +1 -0
- package/dist/testing.d.ts +18 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +44 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +26 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +229 -0
- package/dist/utils.js.map +1 -0
- package/package.json +76 -1
- package/src/azure.ts +191 -0
- package/src/core.ts +440 -0
- package/src/errors.ts +109 -0
- package/src/http.ts +94 -0
- package/src/index.ts +43 -0
- package/src/mistral.ts +124 -0
- package/src/openai.ts +162 -0
- package/src/testing.ts +85 -0
- package/src/types.ts +241 -0
- package/src/utils.ts +281 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ModerationAbortError,
|
|
3
|
+
ModerationAdapterError,
|
|
4
|
+
ModerationSdkError,
|
|
5
|
+
ModerationValidationError,
|
|
6
|
+
} from "./errors.js";
|
|
7
|
+
import type {
|
|
8
|
+
ModerationAdapterResult,
|
|
9
|
+
ModerationImageSource,
|
|
10
|
+
ModerationInput,
|
|
11
|
+
ModerationMessageRole,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
|
|
14
|
+
const MESSAGE_ROLES = new Set<ModerationMessageRole>(["system", "user", "assistant", "tool"]);
|
|
15
|
+
const BASE64_PATTERN = /^(?:[A-Za-z\d+/]{4})*(?:[A-Za-z\d+/]{2}==|[A-Za-z\d+/]{3}=)?$/;
|
|
16
|
+
|
|
17
|
+
export function assertModerationInput(input: ModerationInput): void {
|
|
18
|
+
if (!input || typeof input !== "object") {
|
|
19
|
+
throw new ModerationValidationError("Moderation input must be an object.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (input.type === "text") {
|
|
23
|
+
if (typeof input.text !== "string" || input.text.trim().length === 0) {
|
|
24
|
+
throw new ModerationValidationError("Text moderation requires non-empty text.");
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (input.type === "image") {
|
|
30
|
+
assertImageSource(input.source);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (input.type === "conversation") {
|
|
35
|
+
if (!Array.isArray(input.messages) || input.messages.length === 0) {
|
|
36
|
+
throw new ModerationValidationError("Conversation moderation requires at least one message.");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const [index, message] of input.messages.entries()) {
|
|
40
|
+
if (!message || typeof message !== "object" || !MESSAGE_ROLES.has(message.role)) {
|
|
41
|
+
throw new ModerationValidationError(
|
|
42
|
+
`Conversation message ${index} has an unsupported role.`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (typeof message.content !== "string" || message.content.trim().length === 0) {
|
|
46
|
+
throw new ModerationValidationError(
|
|
47
|
+
`Conversation message ${index} requires non-empty string content.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw new ModerationValidationError("Unsupported moderation input type.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function assertImageSource(source: ModerationImageSource): void {
|
|
58
|
+
if (!source || typeof source !== "object") {
|
|
59
|
+
throw new ModerationValidationError("Image moderation requires an image source.");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (source.type === "url") {
|
|
63
|
+
let parsed: URL;
|
|
64
|
+
try {
|
|
65
|
+
parsed = new URL(source.url);
|
|
66
|
+
} catch {
|
|
67
|
+
throw new ModerationValidationError("Image URL must be a valid HTTP(S) URL.");
|
|
68
|
+
}
|
|
69
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
70
|
+
throw new ModerationValidationError("Image URL must use HTTP or HTTPS.");
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (source.type === "base64") {
|
|
76
|
+
assertMediaType(source.mediaType);
|
|
77
|
+
if (typeof source.data !== "string") {
|
|
78
|
+
throw new ModerationValidationError("Image base64 data must be a string.");
|
|
79
|
+
}
|
|
80
|
+
const normalized = normalizeBase64(source.data);
|
|
81
|
+
if (normalized.length === 0 || !BASE64_PATTERN.test(normalized)) {
|
|
82
|
+
throw new ModerationValidationError("Image base64 data is invalid.");
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (source.type === "bytes") {
|
|
88
|
+
assertMediaType(source.mediaType);
|
|
89
|
+
if (!isBinaryData(source.data) || binaryByteLength(source.data) === 0) {
|
|
90
|
+
throw new ModerationValidationError(
|
|
91
|
+
"Image bytes must be a non-empty Uint8Array, ArrayBuffer, or Blob.",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
throw new ModerationValidationError("Unsupported image source type.");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function assertMediaType(mediaType: string): void {
|
|
101
|
+
if (typeof mediaType !== "string" || !/^image\/[A-Za-z\d.+-]+$/i.test(mediaType)) {
|
|
102
|
+
throw new ModerationValidationError('Image mediaType must look like "image/png".');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isBinaryData(value: unknown): value is Uint8Array | ArrayBuffer | Blob {
|
|
107
|
+
return value instanceof Uint8Array || value instanceof ArrayBuffer || value instanceof Blob;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function flattenConversation(input: Extract<ModerationInput, { type: "conversation" }>) {
|
|
111
|
+
return JSON.stringify(input.messages);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function normalizeHttpEndpoint(value: string, label: string): string {
|
|
115
|
+
let endpoint: URL;
|
|
116
|
+
try {
|
|
117
|
+
endpoint = new URL(value);
|
|
118
|
+
} catch {
|
|
119
|
+
throw new ModerationValidationError(`${label} must be a valid HTTP(S) URL.`);
|
|
120
|
+
}
|
|
121
|
+
if (endpoint.protocol !== "http:" && endpoint.protocol !== "https:") {
|
|
122
|
+
throw new ModerationValidationError(`${label} must use HTTP or HTTPS.`);
|
|
123
|
+
}
|
|
124
|
+
return endpoint.toString().replace(/\/+$/, "");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function normalizeBase64(value: string): string {
|
|
128
|
+
return value.replace(/\s+/g, "");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function base64ByteLength(value: string): number {
|
|
132
|
+
const normalized = normalizeBase64(value);
|
|
133
|
+
if (normalized.length === 0) return 0;
|
|
134
|
+
const padding = normalized.endsWith("==") ? 2 : normalized.endsWith("=") ? 1 : 0;
|
|
135
|
+
return (normalized.length * 3) / 4 - padding;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function binaryByteLength(value: Uint8Array | ArrayBuffer | Blob): number {
|
|
139
|
+
if (value instanceof Blob) return value.size;
|
|
140
|
+
return value.byteLength;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function imageSourceByteLength(source: ModerationImageSource): number | undefined {
|
|
144
|
+
if (source.type === "url") return undefined;
|
|
145
|
+
if (source.type === "base64") return base64ByteLength(source.data);
|
|
146
|
+
return binaryByteLength(source.data);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function imageSourceToBase64(source: Exclude<ModerationImageSource, { type: "url" }>) {
|
|
150
|
+
if (source.type === "base64") return normalizeBase64(source.data);
|
|
151
|
+
|
|
152
|
+
const bytes =
|
|
153
|
+
source.data instanceof Blob
|
|
154
|
+
? new Uint8Array(await source.data.arrayBuffer())
|
|
155
|
+
: source.data instanceof ArrayBuffer
|
|
156
|
+
? new Uint8Array(source.data)
|
|
157
|
+
: source.data;
|
|
158
|
+
return bytesToBase64(bytes);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
162
|
+
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
|
|
163
|
+
|
|
164
|
+
let binary = "";
|
|
165
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
166
|
+
return btoa(binary);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function imageSourceToDataUrl(
|
|
170
|
+
source: Exclude<ModerationImageSource, { type: "url" }>,
|
|
171
|
+
) {
|
|
172
|
+
return `data:${source.mediaType};base64,${await imageSourceToBase64(source)}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function normalizeAdapterResult<Name extends string, Raw>(
|
|
176
|
+
adapter: Name,
|
|
177
|
+
value: ModerationAdapterResult<Name, Raw>,
|
|
178
|
+
): ModerationAdapterResult<Name, Raw> {
|
|
179
|
+
if (!value || typeof value !== "object") {
|
|
180
|
+
throw new ModerationAdapterError("Adapter returned an invalid moderation result.", {
|
|
181
|
+
adapter,
|
|
182
|
+
code: "invalid_response",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (typeof value.flagged !== "boolean") {
|
|
186
|
+
throw new ModerationAdapterError('Adapter result requires a boolean "flagged" field.', {
|
|
187
|
+
adapter,
|
|
188
|
+
code: "invalid_response",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (!value.categories || typeof value.categories !== "object") {
|
|
192
|
+
throw new ModerationAdapterError('Adapter result requires a "categories" object.', {
|
|
193
|
+
adapter,
|
|
194
|
+
code: "invalid_response",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { ...value, adapter };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function toAdapterError(adapter: string, error: unknown): ModerationAdapterError {
|
|
202
|
+
if (error instanceof ModerationAdapterError) {
|
|
203
|
+
if (error.adapter === adapter) return error;
|
|
204
|
+
return new ModerationAdapterError(error.message, {
|
|
205
|
+
adapter,
|
|
206
|
+
code: error.code,
|
|
207
|
+
status: error.status,
|
|
208
|
+
retryable: error.retryable,
|
|
209
|
+
details: error.details,
|
|
210
|
+
cause: error,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (error instanceof ModerationSdkError) {
|
|
215
|
+
return new ModerationAdapterError(error.message, {
|
|
216
|
+
adapter,
|
|
217
|
+
code: error.code,
|
|
218
|
+
status: error.status,
|
|
219
|
+
retryable: error.retryable,
|
|
220
|
+
details: error.details,
|
|
221
|
+
cause: error,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return new ModerationAdapterError(
|
|
226
|
+
error instanceof Error ? error.message : "Moderation adapter failed.",
|
|
227
|
+
{ adapter, cause: error },
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function normalizeSdkError(error: unknown): ModerationSdkError {
|
|
232
|
+
if (error instanceof ModerationSdkError) return error;
|
|
233
|
+
return new ModerationSdkError(error instanceof Error ? error.message : "Moderation failed.", {
|
|
234
|
+
code: "internal_error",
|
|
235
|
+
cause: error,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function throwIfAborted(signal?: AbortSignal): void {
|
|
240
|
+
if (signal?.aborted) throw new ModerationAbortError(signal.reason);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function isAbortLike(error: unknown): boolean {
|
|
244
|
+
return (
|
|
245
|
+
error instanceof ModerationAbortError ||
|
|
246
|
+
(error instanceof DOMException && error.name === "AbortError") ||
|
|
247
|
+
(error instanceof Error && error.name === "AbortError")
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
252
|
+
throwIfAborted(signal);
|
|
253
|
+
if (!signal) return promise;
|
|
254
|
+
|
|
255
|
+
return new Promise<T>((resolve, reject) => {
|
|
256
|
+
const abort = () => reject(new ModerationAbortError(signal.reason));
|
|
257
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
258
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function sleepWithAbort(ms: number, signal?: AbortSignal): Promise<void> {
|
|
263
|
+
throwIfAborted(signal);
|
|
264
|
+
if (ms <= 0) return Promise.resolve();
|
|
265
|
+
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
const timer = setTimeout(done, ms);
|
|
268
|
+
const abort = () => {
|
|
269
|
+
clearTimeout(timer);
|
|
270
|
+
signal?.removeEventListener("abort", abort);
|
|
271
|
+
reject(new ModerationAbortError(signal?.reason));
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
function done() {
|
|
275
|
+
signal?.removeEventListener("abort", abort);
|
|
276
|
+
resolve();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
280
|
+
});
|
|
281
|
+
}
|