modelmix 5.0.3 → 5.0.4
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/README.md +12 -0
- package/demo/moderation.js +17 -0
- package/index.d.ts +46 -0
- package/index.js +110 -1
- package/package.json +4 -5
- package/skills/modelmix/SKILL.md +4 -1
- package/test/moderation.test.js +135 -0
package/README.md
CHANGED
|
@@ -982,6 +982,18 @@ new ModelMix(args = { options: {}, config: {} })
|
|
|
982
982
|
- `toolCalls`: Array of tool calls made by the model (if any)
|
|
983
983
|
- `tokens`: Normalized token counts (`input`, `output`, `thinking`, `total`, `cached`, `cacheWrite`, `cacheWrite5m`, `cacheWrite1h`, `uncachedInput`, `cacheHitRate`), cache economics (`cacheSavings`, `cacheWritePremium`, `breakEvenHits`), plus `cost`, `costBreakdown` (USD), and `speed` (output tokens/sec)
|
|
984
984
|
- `response`: The raw API response
|
|
985
|
+
- `ModerationMix` owns moderation-only provider chains. Use `openai()` to attach OpenAI's current `omni-moderation-latest`; `raw()` exposes the results under `moderation` (`flagged`, `categories`, `category_scores`, and `category_applied_input_types`). It uses `/v1/moderations`, rejects generative providers, does not generate text, and does not support streaming. Future moderation providers can be appended as fallbacks.
|
|
986
|
+
```javascript
|
|
987
|
+
const { ModerationMix } = require('modelmix');
|
|
988
|
+
|
|
989
|
+
const { moderation: [profile] } = await ModerationMix.new()
|
|
990
|
+
.openai()
|
|
991
|
+
.addText(username)
|
|
992
|
+
.addImageFromUrl(avatarUrl)
|
|
993
|
+
.raw();
|
|
994
|
+
|
|
995
|
+
if (profile.flagged) throw new Error('Profile rejected by moderation');
|
|
996
|
+
```
|
|
985
997
|
- `stream(callback)`: Sends the message and streams the response, invoking the callback with each streamed part.
|
|
986
998
|
- `json(schemaExample, descriptions = {}, options = {})`: Forces the model to return a response in a specific JSON format.
|
|
987
999
|
- `schemaExample`: Example of the JSON structure to be returned. Top-level arrays are auto-wrapped for better LLM compatibility.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ModerationMix } from '../index.js';
|
|
2
|
+
try { process.loadEnvFile(); } catch {}
|
|
3
|
+
|
|
4
|
+
const username = 'player_name';
|
|
5
|
+
const avatarUrl = 'https://example.com/avatar.png';
|
|
6
|
+
|
|
7
|
+
const { moderation: [profileModeration] } = await ModerationMix.new()
|
|
8
|
+
.openai()
|
|
9
|
+
.addText(username)
|
|
10
|
+
.addImageFromUrl(avatarUrl)
|
|
11
|
+
.raw();
|
|
12
|
+
|
|
13
|
+
console.log({
|
|
14
|
+
allowed: !profileModeration.flagged,
|
|
15
|
+
categories: profileModeration.categories,
|
|
16
|
+
scores: profileModeration.category_scores
|
|
17
|
+
});
|
package/index.d.ts
CHANGED
|
@@ -190,9 +190,36 @@ export interface ModelMixResult {
|
|
|
190
190
|
response?: unknown;
|
|
191
191
|
assistantMessage?: ChatMessage;
|
|
192
192
|
execution?: PluginExecutionMetadata;
|
|
193
|
+
moderation?: ModerationResult[];
|
|
193
194
|
[key: string]: unknown;
|
|
194
195
|
}
|
|
195
196
|
|
|
197
|
+
export interface ModerationCategories {
|
|
198
|
+
harassment: boolean;
|
|
199
|
+
'harassment/threatening': boolean;
|
|
200
|
+
hate: boolean;
|
|
201
|
+
'hate/threatening': boolean;
|
|
202
|
+
illicit: boolean | null;
|
|
203
|
+
'illicit/violent': boolean | null;
|
|
204
|
+
'self-harm': boolean;
|
|
205
|
+
'self-harm/instructions': boolean;
|
|
206
|
+
'self-harm/intent': boolean;
|
|
207
|
+
sexual: boolean;
|
|
208
|
+
'sexual/minors': boolean;
|
|
209
|
+
violence: boolean;
|
|
210
|
+
'violence/graphic': boolean;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export type ModerationCategoryScores = Record<keyof ModerationCategories, number>;
|
|
214
|
+
export type ModerationAppliedInputTypes = Record<keyof ModerationCategories, Array<'text' | 'image'>>;
|
|
215
|
+
|
|
216
|
+
export interface ModerationResult {
|
|
217
|
+
flagged: boolean;
|
|
218
|
+
categories: ModerationCategories;
|
|
219
|
+
category_scores: ModerationCategoryScores;
|
|
220
|
+
category_applied_input_types: ModerationAppliedInputTypes;
|
|
221
|
+
}
|
|
222
|
+
|
|
196
223
|
export type ModelMixOutputMode = 'message' | 'json' | 'block' | 'raw' | 'stream';
|
|
197
224
|
|
|
198
225
|
export interface PluginExecutionMetadata {
|
|
@@ -571,7 +598,14 @@ export declare class MixCustom {
|
|
|
571
598
|
}
|
|
572
599
|
|
|
573
600
|
export declare class MixOpenAI extends MixCustom {}
|
|
601
|
+
export declare class MixModeration extends MixCustom {}
|
|
574
602
|
export declare class MixOpenAIResponses extends MixOpenAI {}
|
|
603
|
+
export declare class MixOpenAIModeration extends MixModeration {
|
|
604
|
+
static messagesToModerationInput(messages?: ChatMessage[]): Array<
|
|
605
|
+
| { type: 'text'; text: string }
|
|
606
|
+
| { type: 'image_url'; image_url: { url: string } }
|
|
607
|
+
>;
|
|
608
|
+
}
|
|
575
609
|
export declare class MixOpenAIWebSocket extends MixOpenAIResponses {}
|
|
576
610
|
export declare class MixOpenRouter extends MixOpenAI {}
|
|
577
611
|
export declare class MixKimi extends MixOpenAI {}
|
|
@@ -590,6 +624,18 @@ export declare class MixFireworks extends MixCustom {}
|
|
|
590
624
|
export declare class MixNVIDIA extends MixCustom {}
|
|
591
625
|
export declare class MixGoogle extends MixCustom {}
|
|
592
626
|
|
|
627
|
+
export declare class ModerationMix extends ModelMix {
|
|
628
|
+
constructor(setup?: Omit<ModelMixSetup, 'mix'>);
|
|
629
|
+
static new(setup?: Omit<ModelMixSetup, 'mix'>): ModerationMix;
|
|
630
|
+
new(setup?: Omit<ModelMixSetup, 'mix'>): ModerationMix;
|
|
631
|
+
attach(key: string, provider: MixModeration): this;
|
|
632
|
+
openai(args?: ModelAttachArgs): this;
|
|
633
|
+
message(): Promise<never>;
|
|
634
|
+
json(): Promise<never>;
|
|
635
|
+
block(): Promise<never>;
|
|
636
|
+
stream(): Promise<never>;
|
|
637
|
+
}
|
|
638
|
+
|
|
593
639
|
/** Normalize unified effort to integer -1 or 0..100. */
|
|
594
640
|
export function normalizeEffort(value: unknown): EffortValue;
|
|
595
641
|
|
package/index.js
CHANGED
|
@@ -2602,6 +2602,12 @@ class MixOpenAI extends MixCustom {
|
|
|
2602
2602
|
}
|
|
2603
2603
|
}
|
|
2604
2604
|
|
|
2605
|
+
class MixModeration extends MixCustom {
|
|
2606
|
+
getOptionsTools() {
|
|
2607
|
+
return {};
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2605
2611
|
class MixOpenAIResponses extends MixOpenAI {
|
|
2606
2612
|
async create({ config = {}, options = {} } = {}) {
|
|
2607
2613
|
|
|
@@ -2833,6 +2839,109 @@ class MixOpenAIResponses extends MixOpenAI {
|
|
|
2833
2839
|
}
|
|
2834
2840
|
}
|
|
2835
2841
|
|
|
2842
|
+
class MixOpenAIModeration extends MixModeration {
|
|
2843
|
+
getDefaultConfig(customConfig) {
|
|
2844
|
+
const apiKey = customConfig.apiKey || process.env.OPENAI_API_KEY;
|
|
2845
|
+
if (!apiKey) {
|
|
2846
|
+
throw new Error('OpenAI API key not found. Please provide it in config or set OPENAI_API_KEY environment variable.');
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
return super.getDefaultConfig({
|
|
2850
|
+
url: 'https://api.openai.com/v1/moderations',
|
|
2851
|
+
apiKey,
|
|
2852
|
+
...customConfig
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
async create({ config = {}, options = {} } = {}) {
|
|
2857
|
+
if (options.stream) {
|
|
2858
|
+
throw new Error('Stream is not supported for OpenAI moderation');
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
const input = MixOpenAIModeration.messagesToModerationInput(options.messages);
|
|
2862
|
+
const response = await fetchJsonResponse(this.config.url, {
|
|
2863
|
+
method: 'POST',
|
|
2864
|
+
headers: this.headers,
|
|
2865
|
+
body: JSON.stringify({ model: options.model, input })
|
|
2866
|
+
});
|
|
2867
|
+
|
|
2868
|
+
return {
|
|
2869
|
+
moderation: response.data.results,
|
|
2870
|
+
tokens: ModelMix.normalizeTokenUsage(),
|
|
2871
|
+
response: response.data
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
static messagesToModerationInput(messages = []) {
|
|
2876
|
+
const input = [];
|
|
2877
|
+
|
|
2878
|
+
for (const message of messages) {
|
|
2879
|
+
if (typeof message.content === 'string') {
|
|
2880
|
+
input.push({ type: 'text', text: message.content });
|
|
2881
|
+
continue;
|
|
2882
|
+
}
|
|
2883
|
+
if (!Array.isArray(message.content)) continue;
|
|
2884
|
+
|
|
2885
|
+
for (const content of message.content) {
|
|
2886
|
+
if (content?.type === 'text') {
|
|
2887
|
+
input.push({ type: 'text', text: content.text });
|
|
2888
|
+
} else if (content?.type === 'image') {
|
|
2889
|
+
const { media_type: mediaType, data } = content.source || {};
|
|
2890
|
+
if (!mediaType || !data) {
|
|
2891
|
+
throw new Error('OpenAI moderation images must be prepared as base64 data URLs');
|
|
2892
|
+
}
|
|
2893
|
+
input.push({
|
|
2894
|
+
type: 'image_url',
|
|
2895
|
+
image_url: { url: `data:${mediaType};base64,${data}` }
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
return input;
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
class ModerationMix extends ModelMix {
|
|
2906
|
+
static new(setup = {}) {
|
|
2907
|
+
return new ModerationMix(setup);
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
new({ options = {}, config = {} } = {}) {
|
|
2911
|
+
return new ModerationMix({
|
|
2912
|
+
options: { ...this.options, ...options },
|
|
2913
|
+
config: { ...this.config, ...config }
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
attach(key, provider) {
|
|
2918
|
+
if (!(provider instanceof MixModeration)) {
|
|
2919
|
+
throw new Error('ModerationMix only accepts moderation providers.');
|
|
2920
|
+
}
|
|
2921
|
+
return super.attach(key, provider);
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
openai({ options = {}, config = {} } = {}) {
|
|
2925
|
+
return this.attach('omni-moderation-latest', new MixOpenAIModeration({ options, config }));
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
async message() {
|
|
2929
|
+
throw new Error('ModerationMix does not generate messages. Use raw() and read result.moderation.');
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
async json() {
|
|
2933
|
+
throw new Error('ModerationMix does not generate JSON. Use raw() and read result.moderation.');
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
async block() {
|
|
2937
|
+
throw new Error('ModerationMix does not generate blocks. Use raw() and read result.moderation.');
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
async stream() {
|
|
2941
|
+
throw new Error('ModerationMix does not support streaming. Use raw().');
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2836
2945
|
class MixOpenAIWebSocket extends MixOpenAIResponses {
|
|
2837
2946
|
getDefaultConfig(customConfig) {
|
|
2838
2947
|
return super.getDefaultConfig({
|
|
@@ -4020,4 +4129,4 @@ class MixGoogle extends MixCustom {
|
|
|
4020
4129
|
}
|
|
4021
4130
|
}
|
|
4022
4131
|
|
|
4023
|
-
module.exports = { MixCustom, ModelMix, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA, normalizeEffort, applyUnifiedEffort, resolveProviderFamily };
|
|
4132
|
+
module.exports = { MixCustom, ModelMix, ModerationMix, MixModeration, MixAnthropic, MixKimi, MixMiniMax, MixMiMo, MixOpenAI, MixOpenAIResponses, MixOpenAIModeration, MixOpenAIWebSocket, MixOpenRouter, MixPerplexity, MixOllama, MixLMStudio, MixGroq, MixTogether, MixGrok, MixCerebras, MixGoogle, MixFireworks, MixNVIDIA, normalizeEffort, applyUnifiedEffort, resolveProviderFamily };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelmix",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.4",
|
|
4
4
|
"description": "🧬 Reliable interface with automatic fallback for AI LLMs.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -77,7 +77,6 @@
|
|
|
77
77
|
"test:tokens": "mocha test/tokens.test.js --timeout 10000 --require test/setup.js",
|
|
78
78
|
"test:plugins": "mocha test/plugins.test.js --timeout 10000 --require test/setup.js",
|
|
79
79
|
"test:rlm": "mocha plugins/rlm/test/**/*.test.js --timeout 10000 --require test/setup.js",
|
|
80
|
-
"test:offline": "mocha test/json.test.js test/fallback.test.js test/templates.test.js test/images.test.js test/bottleneck.test.js test/tokens.test.js test/history.test.js test/anthropic.test.js test/effort.test.js test/grok.test.js test/plugins.test.js plugins/rlm/test/**/*.test.js --timeout 10000 --require test/setup.js"
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
}
|
|
80
|
+
"test:offline": "mocha test/json.test.js test/fallback.test.js test/templates.test.js test/images.test.js test/bottleneck.test.js test/tokens.test.js test/history.test.js test/anthropic.test.js test/effort.test.js test/grok.test.js test/moderation.test.js test/plugins.test.js plugins/rlm/test/**/*.test.js --timeout 10000 --require test/setup.js"
|
|
81
|
+
}
|
|
82
|
+
}
|
package/skills/modelmix/SKILL.md
CHANGED
|
@@ -137,6 +137,9 @@ ModelMix.new({ config: { effort: 80 } })
|
|
|
137
137
|
## Available Model Shorthands
|
|
138
138
|
|
|
139
139
|
### OpenAI
|
|
140
|
+
|
|
141
|
+
Use `ModerationMix.new().openai()` with `.raw()` to classify text and images through OpenAI's Moderations endpoint. Read the results from `raw.moderation`. `ModerationMix` accepts moderation providers as ordered fallbacks, rejects generative providers, and does not generate text or support streaming.
|
|
142
|
+
|
|
140
143
|
`gpt52()` `gpt52chat()` `gpt51()` `gpt5()` `gpt5mini()` `gpt5nano()` `gpt45()` `gpt41()` `gpt41mini()` `gpt41nano()` `o3()` `o4mini()`
|
|
141
144
|
|
|
142
145
|
### Anthropic
|
|
@@ -610,7 +613,7 @@ const model = ModelMix.new({
|
|
|
610
613
|
|
|
611
614
|
## Available Provider Classes
|
|
612
615
|
|
|
613
|
-
`MixOpenAI` `MixAnthropic` `MixGoogle` `MixPerplexity` `MixGroq` `MixTogether` `MixGrok` `MixOpenRouter` `MixOllama` `MixLMStudio` `MixCustom` `MixCerebras` `MixFireworks` `MixKimi` `MixMiniMax` `MixLambda`
|
|
616
|
+
`ModerationMix` `MixModeration` `MixOpenAI` `MixOpenAIResponses` `MixOpenAIModeration` `MixAnthropic` `MixGoogle` `MixPerplexity` `MixGroq` `MixTogether` `MixGrok` `MixOpenRouter` `MixOllama` `MixLMStudio` `MixCustom` `MixCerebras` `MixFireworks` `MixKimi` `MixMiniMax` `MixLambda`
|
|
614
617
|
|
|
615
618
|
## Troubleshooting
|
|
616
619
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const { expect } = require('chai');
|
|
2
|
+
const nock = require('nock');
|
|
3
|
+
const { ModerationMix, MixModeration, MixOpenAIModeration } = require('../index.js');
|
|
4
|
+
|
|
5
|
+
describe('OpenAI moderation', () => {
|
|
6
|
+
const moderationResult = {
|
|
7
|
+
flagged: true,
|
|
8
|
+
categories: { violence: true },
|
|
9
|
+
category_scores: { violence: 0.98 },
|
|
10
|
+
category_applied_input_types: { violence: ['text', 'image'] }
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
it('registers omni-moderation-latest with openai()', () => {
|
|
14
|
+
const model = ModerationMix.new().openai({ config: { apiKey: 'test-key' } });
|
|
15
|
+
|
|
16
|
+
expect(model.models).to.have.length(1);
|
|
17
|
+
expect(model.models[0].key).to.equal('omni-moderation-latest');
|
|
18
|
+
expect(model.models[0].provider).to.be.instanceOf(MixOpenAIModeration);
|
|
19
|
+
expect(model.models[0].provider.config.url).to.equal('https://api.openai.com/v1/moderations');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('accepts an explicit API key without requiring the environment variable', () => {
|
|
23
|
+
const originalApiKey = process.env.OPENAI_API_KEY;
|
|
24
|
+
delete process.env.OPENAI_API_KEY;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const model = ModerationMix.new().openai({ config: { apiKey: 'explicit-key' } });
|
|
28
|
+
expect(model.models[0].provider.config.apiKey).to.equal('explicit-key');
|
|
29
|
+
} finally {
|
|
30
|
+
if (originalApiKey === undefined) delete process.env.OPENAI_API_KEY;
|
|
31
|
+
else process.env.OPENAI_API_KEY = originalApiKey;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('sends text and image input to the Moderations endpoint', async () => {
|
|
36
|
+
const api = nock('https://api.openai.com')
|
|
37
|
+
.post('/v1/moderations', body => {
|
|
38
|
+
expect(body).to.deep.equal({
|
|
39
|
+
model: 'omni-moderation-latest',
|
|
40
|
+
input: [
|
|
41
|
+
{ type: 'text', text: 'Check this' },
|
|
42
|
+
{
|
|
43
|
+
type: 'image_url',
|
|
44
|
+
image_url: { url: 'data:image/png;base64,aW1hZ2U=' }
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
});
|
|
48
|
+
return true;
|
|
49
|
+
})
|
|
50
|
+
.reply(200, {
|
|
51
|
+
id: 'modr-test',
|
|
52
|
+
model: 'omni-moderation-latest',
|
|
53
|
+
results: [moderationResult]
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await ModerationMix.new()
|
|
57
|
+
.openai({ config: { apiKey: 'test-key' } })
|
|
58
|
+
.addText('Check this')
|
|
59
|
+
.addImageFromUrl('data:image/png;base64,aW1hZ2U=')
|
|
60
|
+
.raw();
|
|
61
|
+
|
|
62
|
+
expect(result.moderation).to.deep.equal([moderationResult]);
|
|
63
|
+
api.done();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('exposes the complete API response through raw()', async () => {
|
|
67
|
+
const response = {
|
|
68
|
+
id: 'modr-test',
|
|
69
|
+
model: 'omni-moderation-latest',
|
|
70
|
+
results: [moderationResult]
|
|
71
|
+
};
|
|
72
|
+
const api = nock('https://api.openai.com')
|
|
73
|
+
.post('/v1/moderations')
|
|
74
|
+
.reply(200, response);
|
|
75
|
+
|
|
76
|
+
const raw = await ModerationMix.new()
|
|
77
|
+
.openai({ config: { apiKey: 'test-key' } })
|
|
78
|
+
.addText('Check this')
|
|
79
|
+
.raw();
|
|
80
|
+
|
|
81
|
+
expect(raw.moderation).to.deep.equal(response.results);
|
|
82
|
+
expect(raw.response).to.deep.equal(response);
|
|
83
|
+
expect(raw.tokens).to.include({ input: 0, output: 0, total: 0, cost: 0 });
|
|
84
|
+
api.done();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('rejects streaming because the Moderations endpoint does not support it', async () => {
|
|
88
|
+
const model = ModerationMix.new()
|
|
89
|
+
.openai({ config: { apiKey: 'test-key' } })
|
|
90
|
+
.addText('Check this');
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await model.stream(() => {});
|
|
94
|
+
throw new Error('Expected stream() to reject');
|
|
95
|
+
} catch (error) {
|
|
96
|
+
expect(error.message).to.equal('ModerationMix does not support streaming. Use raw().');
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('rejects generative providers from the moderation chain', () => {
|
|
101
|
+
const model = ModerationMix.new();
|
|
102
|
+
|
|
103
|
+
expect(() => model.gpt41nano()).to.throw(
|
|
104
|
+
'ModerationMix only accepts moderation providers.'
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('accepts additional moderation providers as fallbacks', () => {
|
|
109
|
+
class TestModeration extends MixModeration {}
|
|
110
|
+
const model = ModerationMix.new()
|
|
111
|
+
.openai({ config: { apiKey: 'test-key' } })
|
|
112
|
+
.attach('test-moderation', new TestModeration({ config: { apiKey: 'test-key' } }));
|
|
113
|
+
|
|
114
|
+
expect(model.models.map(({ key }) => key)).to.deep.equal([
|
|
115
|
+
'omni-moderation-latest',
|
|
116
|
+
'test-moderation'
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
for (const method of ['message', 'json', 'block']) {
|
|
121
|
+
it(`rejects ${method}() because moderation is not generative`, async () => {
|
|
122
|
+
const model = ModerationMix.new()
|
|
123
|
+
.openai({ config: { apiKey: 'test-key' } })
|
|
124
|
+
.addText('Check this');
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await model[method]();
|
|
128
|
+
throw new Error(`Expected ${method}() to reject`);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
expect(error.message).to.include('ModerationMix does not generate');
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
});
|