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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sophisticatedalterego
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img alt="Content Moderation SDK — Moderate text, images, and conversations without provider lock-in" src="./content-moderation-sdk.jpg" width="820" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
One TypeScript SDK for moderating text, images, and conversations without coupling application code to one provider.
|
|
6
|
+
|
|
7
|
+
- Swappable OpenAI, Azure AI Content Safety, and Mistral adapters
|
|
8
|
+
- Provider-native category names, decisions, scores, severities, and raw responses
|
|
9
|
+
- Fail-fast capability validation across the complete fallback route
|
|
10
|
+
- Retries inside one adapter and fallback across adapters
|
|
11
|
+
- Typed lifecycle hooks and test adapters
|
|
12
|
+
- No runtime dependencies, telemetry, persistence, or content logging
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install content-moderation-sdk
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The SDK is server-side only and supports Node.js 20+ and Bun 1.1+. The package is ESM-only. Keep provider API keys out of browser code.
|
|
21
|
+
|
|
22
|
+
## Quickstart
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { createModerationClient } from "content-moderation-sdk";
|
|
26
|
+
import { openai } from "content-moderation-sdk/openai";
|
|
27
|
+
|
|
28
|
+
const moderation = createModerationClient({
|
|
29
|
+
adapters: [
|
|
30
|
+
openai({
|
|
31
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
32
|
+
}),
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = await moderation.moderate({
|
|
37
|
+
type: "text",
|
|
38
|
+
text: "Content supplied by a user",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (result.flagged) {
|
|
42
|
+
console.log(result.categories);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Inputs
|
|
47
|
+
|
|
48
|
+
One `moderate()` call handles one unit: text, an image, or a text-only
|
|
49
|
+
conversation.
|
|
50
|
+
|
|
51
|
+
### Text
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
await moderation.moderate({
|
|
55
|
+
type: "text",
|
|
56
|
+
text: "A forum post",
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Images
|
|
61
|
+
|
|
62
|
+
Images can be a public HTTP(S) URL, base64, or bytes:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
await moderation.moderate({
|
|
66
|
+
type: "image",
|
|
67
|
+
source: {
|
|
68
|
+
type: "url",
|
|
69
|
+
url: "https://example.com/upload.png",
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await moderation.moderate({
|
|
74
|
+
type: "image",
|
|
75
|
+
source: {
|
|
76
|
+
type: "bytes",
|
|
77
|
+
data: imageBytes,
|
|
78
|
+
mediaType: "image/png",
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
OpenAI and Azure support images. Mistral does not, so a fallback route containing
|
|
84
|
+
Mistral is invalid for image input and fails before any provider is called.
|
|
85
|
+
|
|
86
|
+
### Conversations
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
await moderation.moderate({
|
|
90
|
+
type: "conversation",
|
|
91
|
+
messages: [
|
|
92
|
+
{ role: "system", content: "You are a support assistant." },
|
|
93
|
+
{ role: "user", content: "The user's prompt" },
|
|
94
|
+
{ role: "assistant", content: "The assistant's response" },
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Mistral receives structured conversations natively. OpenAI and Azure receive a
|
|
100
|
+
deterministic JSON serialization of the messages as text. Results expose
|
|
101
|
+
`mode: "native" | "flattened"` so this transformation is never hidden.
|
|
102
|
+
|
|
103
|
+
## Adapters
|
|
104
|
+
|
|
105
|
+
| Adapter | Text | Image | Conversation |
|
|
106
|
+
| ----------------------- | ------ | ------------------------------- | ------------ |
|
|
107
|
+
| OpenAI | Native | Native with `omni-moderation-*` | Flattened |
|
|
108
|
+
| Azure AI Content Safety | Native | Native | Flattened |
|
|
109
|
+
| Mistral | Native | Unsupported | Native |
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { azure } from "content-moderation-sdk/azure";
|
|
113
|
+
import { mistral } from "content-moderation-sdk/mistral";
|
|
114
|
+
|
|
115
|
+
const moderation = createModerationClient({
|
|
116
|
+
adapters: [
|
|
117
|
+
azure({
|
|
118
|
+
endpoint: process.env.AZURE_CONTENT_SAFETY_ENDPOINT!,
|
|
119
|
+
apiKey: process.env.AZURE_CONTENT_SAFETY_KEY!,
|
|
120
|
+
threshold: 4,
|
|
121
|
+
}),
|
|
122
|
+
mistral({
|
|
123
|
+
apiKey: process.env.MISTRAL_API_KEY!,
|
|
124
|
+
}),
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Azure uses four severity levels and flags a category at severity 4 or above by
|
|
130
|
+
default. Configure `threshold: 2 | 4 | 6` for a different policy. OpenAI uses
|
|
131
|
+
its top-level provider decision; Mistral is flagged when any native category is
|
|
132
|
+
flagged.
|
|
133
|
+
|
|
134
|
+
## Results
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
type ModerationResult = {
|
|
138
|
+
adapter: string;
|
|
139
|
+
flagged: boolean;
|
|
140
|
+
inputType: "text" | "image" | "conversation";
|
|
141
|
+
mode: "native" | "flattened";
|
|
142
|
+
categories: Record<
|
|
143
|
+
string,
|
|
144
|
+
{
|
|
145
|
+
flagged: boolean | null;
|
|
146
|
+
score?: number;
|
|
147
|
+
severity?: { value: number; max: number };
|
|
148
|
+
inputTypes?: ("text" | "image")[];
|
|
149
|
+
}
|
|
150
|
+
>;
|
|
151
|
+
id?: string;
|
|
152
|
+
model?: string;
|
|
153
|
+
raw?: unknown;
|
|
154
|
+
};
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Category keys and numeric values remain provider-native. A probability-like
|
|
158
|
+
score from one provider is not comparable to an Azure severity or another
|
|
159
|
+
provider's score. Use `raw` when provider-specific response fields matter.
|
|
160
|
+
|
|
161
|
+
## Validation, retries, and fallback
|
|
162
|
+
|
|
163
|
+
`validate()` runs common, capability, and adapter-specific checks for every
|
|
164
|
+
candidate route without sending content:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
await moderation.validate(input, {
|
|
168
|
+
adapter: "openai",
|
|
169
|
+
fallback: { adapters: ["azure"] },
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Retries stay on the current adapter. Fallback begins after that adapter reaches
|
|
174
|
+
a terminal error:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const moderation = createModerationClient({
|
|
178
|
+
adapters: [primary, backup],
|
|
179
|
+
retry: {
|
|
180
|
+
maxAttempts: 3,
|
|
181
|
+
},
|
|
182
|
+
fallback: {
|
|
183
|
+
adapters: ["backup"],
|
|
184
|
+
shouldFallback(error) {
|
|
185
|
+
return error.status !== 401;
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The default is one attempt and no fallback. Provider calls can still be billed
|
|
192
|
+
when a response is lost, so use `shouldFallback` when duplicate cost matters.
|
|
193
|
+
An `AbortSignal` stops the active request, retry backoff, and later fallbacks.
|
|
194
|
+
|
|
195
|
+
## Errors
|
|
196
|
+
|
|
197
|
+
All SDK errors extend `ModerationSdkError` and expose `code`, `retryable`, and,
|
|
198
|
+
when available, `adapter`, `status`, and `details`. They serialize to actionable
|
|
199
|
+
JSON, including the error message, while omitting stack traces and causes:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { ModerationSdkError } from "content-moderation-sdk";
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
return Response.json(await moderation.moderate(input));
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (error instanceof ModerationSdkError) {
|
|
208
|
+
return Response.json(error, {
|
|
209
|
+
status: error.code === "validation_error" ? 400 : (error.status ?? 500),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Batches
|
|
217
|
+
|
|
218
|
+
`moderateMany()` runs sequentially and returns one ordered settled result per
|
|
219
|
+
item:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
const results = await moderation.moderateMany([
|
|
223
|
+
{ input: { type: "text", text: "First" } },
|
|
224
|
+
{ input: { type: "text", text: "Second" } },
|
|
225
|
+
]);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Custom adapters
|
|
229
|
+
|
|
230
|
+
Adapters are plain objects with declared capabilities:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import type { ModerationAdapter } from "content-moderation-sdk";
|
|
234
|
+
|
|
235
|
+
const custom: ModerationAdapter<"custom"> = {
|
|
236
|
+
name: "custom",
|
|
237
|
+
capabilities: {
|
|
238
|
+
text: true,
|
|
239
|
+
image: false,
|
|
240
|
+
conversation: "flattened",
|
|
241
|
+
},
|
|
242
|
+
async moderate(input, context) {
|
|
243
|
+
// Forward context.signal to network work.
|
|
244
|
+
return {
|
|
245
|
+
adapter: "custom",
|
|
246
|
+
flagged: false,
|
|
247
|
+
categories: {},
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`validate`, when implemented, must be deterministic and perform no network
|
|
254
|
+
work. The client calls it for every candidate before the first moderation
|
|
255
|
+
request.
|
|
256
|
+
|
|
257
|
+
## Testing
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import { memoryAdapter } from "content-moderation-sdk/testing";
|
|
261
|
+
|
|
262
|
+
const memory = memoryAdapter();
|
|
263
|
+
const moderation = createModerationClient({ adapters: [memory] });
|
|
264
|
+
|
|
265
|
+
await moderation.moderate({ type: "text", text: "test input" });
|
|
266
|
+
expect(memory.raw?.requests).toHaveLength(1);
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`memoryAdapter` stores inputs intentionally for assertions. Production
|
|
270
|
+
adapters do not store input. `failingAdapter` is also available for retry and
|
|
271
|
+
fallback tests.
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
MIT
|
|
Binary file
|
package/dist/azure.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ModerationAdapter } from "./types.js";
|
|
2
|
+
export type AzureModerationCategory = "Hate" | "SelfHarm" | "Sexual" | "Violence";
|
|
3
|
+
export type AzureSeverityThreshold = 2 | 4 | 6;
|
|
4
|
+
export type AzureCategoryAnalysis = {
|
|
5
|
+
category: AzureModerationCategory | (string & {});
|
|
6
|
+
severity: number;
|
|
7
|
+
};
|
|
8
|
+
export type AzureBlocklistMatch = {
|
|
9
|
+
blocklistName: string;
|
|
10
|
+
blocklistItemId: string;
|
|
11
|
+
blocklistItemText: string;
|
|
12
|
+
};
|
|
13
|
+
export type AzureTextModerationResponse = {
|
|
14
|
+
categoriesAnalysis: AzureCategoryAnalysis[];
|
|
15
|
+
blocklistsMatch?: AzureBlocklistMatch[];
|
|
16
|
+
};
|
|
17
|
+
export type AzureImageModerationResponse = {
|
|
18
|
+
categoriesAnalysis: AzureCategoryAnalysis[];
|
|
19
|
+
};
|
|
20
|
+
export type AzureModerationResponse = AzureTextModerationResponse | AzureImageModerationResponse;
|
|
21
|
+
export type AzureOptions = {
|
|
22
|
+
endpoint: string;
|
|
23
|
+
apiKey: string;
|
|
24
|
+
apiVersion?: string;
|
|
25
|
+
threshold?: AzureSeverityThreshold;
|
|
26
|
+
categories?: readonly AzureModerationCategory[];
|
|
27
|
+
blocklistNames?: readonly string[];
|
|
28
|
+
haltOnBlocklistHit?: boolean;
|
|
29
|
+
headers?: Readonly<Record<string, string>>;
|
|
30
|
+
fetch?: typeof fetch;
|
|
31
|
+
};
|
|
32
|
+
export type AzureAdapter = ModerationAdapter<"azure", {
|
|
33
|
+
endpoint: string;
|
|
34
|
+
apiVersion: string;
|
|
35
|
+
threshold: AzureSeverityThreshold;
|
|
36
|
+
}, AzureModerationResponse>;
|
|
37
|
+
export declare function azure(options: AzureOptions): AzureAdapter;
|
|
38
|
+
//# sourceMappingURL=azure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"azure.d.ts","sourceRoot":"","sources":["../src/azure.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,iBAAiB,EAIlB,MAAM,YAAY,CAAC;AAMpB,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,UAAU,CAAC;AAClF,MAAM,MAAM,sBAAsB,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAE/C,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,uBAAuB,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;IAClD,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,kBAAkB,EAAE,qBAAqB,EAAE,CAAC;IAC5C,eAAe,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACzC,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,kBAAkB,EAAE,qBAAqB,EAAE,CAAC;CAC7C,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,2BAA2B,GAAG,4BAA4B,CAAC;AAEjG,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,UAAU,CAAC,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAChD,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3C,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,iBAAiB,CAC1C,OAAO,EACP;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,sBAAsB,CAAA;CAAE,EAC3E,uBAAuB,CACxB,CAAC;AAEF,wBAAgB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,YAAY,CA6FzD"}
|
package/dist/azure.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ModerationAdapterError, ModerationValidationError } from "./errors.js";
|
|
2
|
+
import { requestJson } from "./http.js";
|
|
3
|
+
import { imageSourceByteLength, imageSourceToBase64, normalizeHttpEndpoint } from "./utils.js";
|
|
4
|
+
const AZURE_MAX_TEXT_CODE_POINTS = 10_000;
|
|
5
|
+
const AZURE_MAX_IMAGE_BYTES = 4 * 1024 * 1024;
|
|
6
|
+
export function azure(options) {
|
|
7
|
+
const endpoint = normalizeHttpEndpoint(options.endpoint, "Azure endpoint");
|
|
8
|
+
if (typeof options.apiKey !== "string" || options.apiKey.trim().length === 0) {
|
|
9
|
+
throw new ModerationValidationError("Azure apiKey is required.");
|
|
10
|
+
}
|
|
11
|
+
const apiVersion = options.apiVersion ?? "2024-09-01";
|
|
12
|
+
const threshold = options.threshold ?? 4;
|
|
13
|
+
if (typeof apiVersion !== "string" || apiVersion.trim().length === 0) {
|
|
14
|
+
throw new ModerationValidationError("Azure apiVersion must be a non-empty string.");
|
|
15
|
+
}
|
|
16
|
+
if (threshold !== 2 && threshold !== 4 && threshold !== 6) {
|
|
17
|
+
throw new ModerationValidationError("Azure threshold must be 2, 4, or 6.");
|
|
18
|
+
}
|
|
19
|
+
const fetcher = options.fetch ?? fetch;
|
|
20
|
+
return {
|
|
21
|
+
name: "azure",
|
|
22
|
+
capabilities: {
|
|
23
|
+
text: true,
|
|
24
|
+
image: true,
|
|
25
|
+
conversation: "flattened",
|
|
26
|
+
},
|
|
27
|
+
raw: { endpoint, apiVersion, threshold },
|
|
28
|
+
validate(input) {
|
|
29
|
+
if (input.type === "text" && [...input.text].length > AZURE_MAX_TEXT_CODE_POINTS) {
|
|
30
|
+
throw new ModerationValidationError(`Azure text input cannot exceed ${AZURE_MAX_TEXT_CODE_POINTS} Unicode code points.`, { adapter: "azure", limit: AZURE_MAX_TEXT_CODE_POINTS });
|
|
31
|
+
}
|
|
32
|
+
if (input.type === "image") {
|
|
33
|
+
const bytes = imageSourceByteLength(input.source);
|
|
34
|
+
if (bytes !== undefined && bytes > AZURE_MAX_IMAGE_BYTES) {
|
|
35
|
+
throw new ModerationValidationError("Azure image input cannot exceed 4 MB.", {
|
|
36
|
+
adapter: "azure",
|
|
37
|
+
limit: AZURE_MAX_IMAGE_BYTES,
|
|
38
|
+
actual: bytes,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
async moderate(input, context) {
|
|
44
|
+
if (input.type === "conversation") {
|
|
45
|
+
throw new ModerationValidationError("Azure receives conversations through the client's flattened text path.");
|
|
46
|
+
}
|
|
47
|
+
const isText = input.type === "text";
|
|
48
|
+
const body = await requestJson({
|
|
49
|
+
adapter: "azure",
|
|
50
|
+
provider: "Azure AI Content Safety",
|
|
51
|
+
fetcher,
|
|
52
|
+
url: `${endpoint}/contentsafety/${isText ? "text" : "image"}:analyze?api-version=${encodeURIComponent(apiVersion)}`,
|
|
53
|
+
init: {
|
|
54
|
+
method: "POST",
|
|
55
|
+
signal: context.signal,
|
|
56
|
+
headers: {
|
|
57
|
+
"Ocp-Apim-Subscription-Key": options.apiKey,
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
...options.headers,
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(isText ? textPayload(input, options) : await imagePayload(input, options)),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
if (!Array.isArray(body.categoriesAnalysis)) {
|
|
65
|
+
throw new ModerationAdapterError("Azure returned an invalid moderation response.", {
|
|
66
|
+
adapter: "azure",
|
|
67
|
+
code: "invalid_response",
|
|
68
|
+
details: body,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const categories = toCategoryAssessments(body.categoriesAnalysis, threshold);
|
|
72
|
+
const blocklistsMatch = "blocklistsMatch" in body && Array.isArray(body.blocklistsMatch)
|
|
73
|
+
? body.blocklistsMatch
|
|
74
|
+
: [];
|
|
75
|
+
return {
|
|
76
|
+
adapter: "azure",
|
|
77
|
+
flagged: blocklistsMatch.length > 0 ||
|
|
78
|
+
Object.values(categories).some((category) => category.flagged === true),
|
|
79
|
+
categories,
|
|
80
|
+
raw: body,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function textPayload(input, options) {
|
|
86
|
+
return {
|
|
87
|
+
text: input.text,
|
|
88
|
+
outputType: "FourSeverityLevels",
|
|
89
|
+
...(options.categories ? { categories: options.categories } : {}),
|
|
90
|
+
...(options.blocklistNames ? { blocklistNames: options.blocklistNames } : {}),
|
|
91
|
+
...(options.haltOnBlocklistHit !== undefined
|
|
92
|
+
? { haltOnBlocklistHit: options.haltOnBlocklistHit }
|
|
93
|
+
: {}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function imagePayload(input, options) {
|
|
97
|
+
return {
|
|
98
|
+
image: input.source.type === "url"
|
|
99
|
+
? { blobUrl: input.source.url }
|
|
100
|
+
: { content: await imageSourceToBase64(input.source) },
|
|
101
|
+
outputType: "FourSeverityLevels",
|
|
102
|
+
...(options.categories ? { categories: options.categories } : {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function toCategoryAssessments(analyses, threshold) {
|
|
106
|
+
return Object.fromEntries(analyses.map(({ category, severity }) => [
|
|
107
|
+
category,
|
|
108
|
+
{
|
|
109
|
+
flagged: severity >= threshold,
|
|
110
|
+
severity: { value: severity, max: 6 },
|
|
111
|
+
},
|
|
112
|
+
]));
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=azure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"azure.js","sourceRoot":"","sources":["../src/azure.ts"],"names":[],"mappings":"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"}
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ModerationAdapter, ModerationClient, ModerationClientOptions } from "./types.js";
|
|
2
|
+
export declare function createModerationClient<const TAdapters extends readonly ModerationAdapter[]>(options: ModerationClientOptions<TAdapters>): ModerationClient<TAdapters>;
|
|
3
|
+
//# sourceMappingURL=core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAEV,iBAAiB,EAEjB,gBAAgB,EAChB,uBAAuB,EAYxB,MAAM,YAAY,CAAC;AA6BpB,wBAAgB,sBAAsB,CAAC,KAAK,CAAC,SAAS,SAAS,SAAS,iBAAiB,EAAE,EACzF,OAAO,EAAE,uBAAuB,CAAC,SAAS,CAAC,GAC1C,gBAAgB,CAAC,SAAS,CAAC,CA0J7B"}
|