openai-api-mock 0.1.30 → 0.1.32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openai-api-mock",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "A Node.js module for mocking OpenAI API responses in a development environment",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,7 +25,9 @@
25
25
  "scripts": {
26
26
  "build": "vite build",
27
27
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
28
- "coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage"
28
+ "coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
29
+ "prepublishOnly": "npm run build"
30
+
29
31
  },
30
32
  "author": "Chiheb Nabil",
31
33
  "license": "MIT",
@@ -37,6 +39,7 @@
37
39
  "import": "./dist/index.js"
38
40
  }
39
41
  },
42
+ "files": ["dist"],
40
43
  "dependencies": {
41
44
  "@faker-js/faker": "^9.0.0",
42
45
  "nock": "^14.0.0"
@@ -1,33 +0,0 @@
1
- name: Tests
2
-
3
- on:
4
- push:
5
- branches: [ dev ]
6
- pull_request:
7
- branches: [ dev ]
8
-
9
- jobs:
10
- test:
11
- runs-on: ubuntu-latest
12
-
13
- strategy:
14
- matrix:
15
- node-version: [18.x, 20.x]
16
-
17
- steps:
18
- - uses: actions/checkout@v4
19
-
20
- - name: Use Node.js ${{ matrix.node-version }}
21
- uses: actions/setup-node@v4
22
- with:
23
- node-version: ${{ matrix.node-version }}
24
- cache: 'npm'
25
-
26
- - name: Install dependencies
27
- run: npm ci
28
-
29
- - name: Build package
30
- run: npm run build
31
-
32
- - name: Run tests
33
- run: npm test
@@ -1,47 +0,0 @@
1
- import fetch from 'node-fetch';
2
- import { mockOpenAIResponse, stopMocking } from '../src/index.js';
3
-
4
- describe('Mock Custom OpenAI endpoint', () => {
5
- let mock;
6
- beforeEach(() => {
7
- mock = mockOpenAIResponse(true, { logRequests: true });
8
- });
9
-
10
- afterEach(() => {
11
- stopMocking();
12
- });
13
-
14
- test('should mock custom endpoint', async () => {
15
- mock.addCustomEndpoint('POST', '/v1/custom/endpoint', () => {
16
- return [200, { message: 'Custom endpoint response' }];
17
- });
18
- const response = await fetch('https://api.openai.com/v1/custom/endpoint'
19
- , {
20
- method: 'POST',
21
- headers: {
22
- 'Content-Type': 'application/json',
23
- },
24
- }
25
- );
26
-
27
- const data = await response.json();
28
- expect(data).toEqual({ message: 'Custom endpoint response' });
29
- });
30
-
31
- test('should mock custom endpoint with error', async () => {
32
- mock.addCustomEndpoint('POST', '/v1/custom/endpoint', () => {
33
- return [400, { error: { message: 'Invalid request' } }];
34
- });
35
- const response = await fetch('https://api.openai.com/v1/custom/endpoint'
36
- , {
37
- method: 'POST',
38
- headers: {
39
- 'Content-Type': 'application/json',
40
- },
41
- }
42
- );
43
-
44
- const data = await response.json();
45
- expect(data).toEqual({ error: { message: 'Invalid request' } });
46
- });
47
- });
@@ -1,225 +0,0 @@
1
- import fetch from 'node-fetch';
2
- import { mockOpenAIResponse, stopMocking } from '../src/index.js';
3
- import OpenAI from 'openai';
4
-
5
- const openai = new OpenAI({ apiKey: "OPENAI_API_KEY" });
6
-
7
- describe('Mock OpenAI Chat & Image generation API', () => {
8
- beforeEach(() => {
9
- mockOpenAIResponse(true);
10
- });
11
-
12
- afterEach(() => {
13
- stopMocking();
14
- });
15
-
16
- describe('Chat Completion Tests', () => {
17
- it('should mock the chat completion with correct properties', async () => {
18
- const response = await openai.chat.completions.create({
19
- model: "gpt-3.5-turbo",
20
- messages: [
21
- { role: 'system', content: "You're an expert chef" },
22
- { role: 'user', content: "Suggest at least 5 recipes" },
23
- ]
24
- });
25
-
26
- expect(response).toHaveProperty('id');
27
- expect(response).toHaveProperty('object', 'chat.completion');
28
- expect(response).toHaveProperty('created');
29
- expect(response.model).toEqual('gpt-3.5-mock');
30
- expect(response.choices).toBeInstanceOf(Array);
31
- expect(response.choices[0]).toHaveProperty('index', 0);
32
- expect(response.choices[0]).toHaveProperty('message');
33
- expect(response.choices[0].message).toHaveProperty('role', 'assistant');
34
- expect(response.choices[0].message).toHaveProperty('content');
35
- expect(response.choices[0]).toHaveProperty('finish_reason', 'stop');
36
- expect(response).toHaveProperty('usage');
37
- expect(response.usage).toHaveProperty('prompt_tokens');
38
- expect(response.usage).toHaveProperty('completion_tokens');
39
- expect(response.usage).toHaveProperty('total_tokens');
40
- });
41
-
42
- it('should mock the streaming chat completion', async () => {
43
- const response = await openai.chat.completions.create({
44
- model: "gpt-3.5-turbo",
45
- stream: true,
46
- messages: [
47
- { role: 'user', content: "What's the biggest country in the world" },
48
- ]
49
- });
50
-
51
- let streamContent = '';
52
- for await (const part of response) {
53
- expect(part).toHaveProperty('id');
54
- expect(part).toHaveProperty('object', 'chat.completion.chunk');
55
- expect(part).toHaveProperty('created');
56
- expect(part).toHaveProperty('model', 'gpt-3.5-mock');
57
- expect(part.choices[0]).toHaveProperty('index', 0);
58
- expect(part.choices[0]).toHaveProperty('delta');
59
- if (part.choices[0]?.delta?.content) {
60
- streamContent += part.choices[0].delta.content;
61
- }
62
- }
63
- expect(streamContent).not.toBe('');
64
- });
65
-
66
- it('should mock the chat completion with function call', async () => {
67
- const response = await openai.chat.completions.create({
68
- model: "gpt-3.5-turbo",
69
- messages: [
70
- { role: 'system', content: "You're an expert chef" },
71
- { role: 'user', content: "Suggest at least 5 recipes" },
72
- ],
73
- functions: [
74
- {
75
- name: 'get_recipes',
76
- description: 'Suggest at least 5 recipes based on the ingredients you have',
77
- parameters: {
78
- type: "object",
79
- properties: {
80
- recipes: {
81
- type: "array",
82
- description: "The recipes that can be made with the ingredients",
83
- items: {
84
- type: 'object',
85
- properties: {
86
- name: { type: 'string' },
87
- ingredients: { type: 'array', items: { type: 'string' } },
88
- instructions: { type: 'string' },
89
- serving: { type: 'string' },
90
- }
91
- }
92
- },
93
- }
94
- },
95
- required: ["recipes"],
96
- },
97
- ],
98
- function_call: { name: 'get_recipes' },
99
- });
100
-
101
- expect(response.model).toEqual('gpt-3.5-mock');
102
- expect(response.choices[0]).toHaveProperty('finish_reason', 'function_call');
103
- expect(response.choices[0].message.function_call).toHaveProperty('name', 'get_recipes');
104
- expect(response.choices[0].message.function_call).toHaveProperty('arguments');
105
- const args = JSON.parse(response.choices[0].message.function_call.arguments);
106
- expect(args).toHaveProperty('recipes');
107
- expect(args.recipes).toBeInstanceOf(Array);
108
- expect(args.recipes.length).toBeGreaterThanOrEqual(5);
109
- args.recipes.forEach(recipe => {
110
- expect(recipe).toHaveProperty('name');
111
- expect(recipe).toHaveProperty('ingredients');
112
- expect(recipe).toHaveProperty('instructions');
113
- expect(recipe).toHaveProperty('serving');
114
- });
115
- });
116
-
117
- it('should mock the chat completion with tool calls', async () => {
118
- const response = await openai.chat.completions.create({
119
- model: "gpt-3.5-turbo",
120
- messages: [
121
- { role: 'system', content: "You're an expert chef" },
122
- { role: 'user', content: "Suggest at least 5 recipes and send them to my email" },
123
- ],
124
- tools: [
125
- {
126
- type: "function",
127
- function: {
128
- name: 'get_recipes',
129
- description: 'Suggest at least 5 recipes based on the ingredients you have',
130
- parameters: {
131
- type: "object",
132
- properties: {
133
- recipes: {
134
- type: "array",
135
- items: {
136
- type: 'object',
137
- properties: {
138
- name: { type: 'string' },
139
- ingredients: { type: 'array', items: { type: 'string' } },
140
- instructions: { type: 'string' },
141
- serving: { type: 'string' },
142
- }
143
- }
144
- },
145
- }
146
- },
147
- required: ["recipes"],
148
- }
149
- },
150
- {
151
- type: "function",
152
- function: {
153
- name: 'send_email',
154
- description: 'Send an email to the user with the recipes',
155
- parameters: {
156
- type: "object",
157
- properties: {
158
- email: { type: "string" },
159
- subject: { type: "string" },
160
- body: { type: "string" },
161
- }
162
- },
163
- required: ["email", "subject", "body"],
164
- }
165
- }
166
- ],
167
- tool_choice: "auto",
168
- });
169
-
170
- expect(response.model).toEqual('gpt-3.5-mock');
171
- expect(response.choices[0]).toHaveProperty('finish_reason', 'tool_calls');
172
- expect(response.choices[0].message.tool_calls).toBeInstanceOf(Array);
173
-
174
- const recipesArgs = JSON.parse(response.choices[0].message.tool_calls[0].function.arguments);
175
- expect(recipesArgs).toHaveProperty('recipes');
176
- });
177
- });
178
-
179
- describe('Image Generation Tests', () => {
180
- it('should mock image generation with correct properties', async () => {
181
- const response = await openai.images.generate({
182
- model: "dall-e-3",
183
- prompt: "A futuristic cityscape",
184
- n: 1,
185
- size: "1024x1024",
186
- quality: "hd",
187
- });
188
-
189
- expect(response).toHaveProperty('created');
190
- expect(response).toHaveProperty('data');
191
- expect(response.data).toBeInstanceOf(Array);
192
- expect(response.data.length).toBe(1);
193
- expect(response.data[0]).toHaveProperty('revised_prompt');
194
- expect(response.data[0]).toHaveProperty('url');
195
- expect(response.data[0].url).toMatch(/^http:\/\//);
196
- });
197
-
198
- it('should handle multiple image generation', async () => {
199
- const response = await openai.images.generate({
200
- model: "dall-e-3",
201
- prompt: "Abstract art in various styles",
202
- n: 3,
203
- size: "512x512",
204
- quality: "standard",
205
- });
206
-
207
- expect(response.data).toBeInstanceOf(Array);
208
- expect(response.data.length).toBe(3);
209
- response.data.forEach(image => {
210
- expect(image).toHaveProperty('url');
211
- expect(image.url).toMatch(/^http:\/\//);
212
- });
213
- });
214
- });
215
-
216
- it('should allow other requests', async () => {
217
- const response = await fetch('https://jsonplaceholder.typicode.com/todos');
218
- const data = await response.json();
219
-
220
- expect(data[0]).toHaveProperty('id');
221
- expect(data[0]).toHaveProperty('userId');
222
- expect(data[0]).toHaveProperty('title');
223
- expect(data[0]).toHaveProperty('completed');
224
- });
225
- });
@@ -1,29 +0,0 @@
1
- import fetch from 'node-fetch';
2
- import { mockOpenAIResponse, stopMocking } from '../src/index.js';
3
- import OpenAI from 'openai';
4
-
5
- const openai = new OpenAI({ apiKey: "OPENAI_API_KEY" });
6
-
7
- describe('Mock OpenAI Chat with a delay', () => {
8
-
9
- beforeEach(() => {
10
- mockOpenAIResponse(true, { latency: 1000, logRequests: true });
11
- });
12
-
13
- afterEach(() => {
14
- stopMocking();
15
- });
16
-
17
- test('should mock OpenAI Chat with a delay', async () => {
18
- const response = await openai.chat.completions.create({
19
- model: "gpt-3.5-mock",
20
- messages: [
21
- { role: 'system', content: "You're an expert chef" },
22
- { role: 'user', content: "Suggest at least 5 recipes" },
23
- ]
24
- });
25
-
26
- expect(response).toBeDefined();
27
-
28
- });
29
- });
package/jest.config.js DELETED
@@ -1,15 +0,0 @@
1
- export default {
2
- collectCoverageFrom: [
3
- 'src/**/*.js',
4
- '!**/node_modules/**',
5
- ],
6
- coverageReporters: ['text', 'lcov'],
7
- coverageThreshold: {
8
- global: {
9
- branches: 80,
10
- functions: 80,
11
- lines: 80,
12
- statements: 80
13
- }
14
- }
15
- };
package/renovate.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
- "extends": ["config:recommended", ":dependencyDashboard"]
4
- }
package/src/chat.js DELETED
@@ -1,16 +0,0 @@
1
- import { createDefaultResponse, createFunctionCallingResponse } from './utils/responseGenerators.js';
2
-
3
- function getChatResponce(requestBody) {
4
- const created = Math.floor(Date.now() / 1000);
5
-
6
- if (!requestBody.functions && !requestBody.tools) {
7
- return createDefaultResponse(created);
8
- }
9
-
10
- return createFunctionCallingResponse(requestBody, created);
11
- }
12
-
13
-
14
- export {
15
- getChatResponce,
16
- };
@@ -1,33 +0,0 @@
1
- import { Readable } from 'node:stream';
2
- import { getSteamChatObject } from './utils/responseGenerators.js'
3
-
4
- function createChatStream() {
5
- const stream = new Readable({
6
- read() { }
7
- });
8
-
9
- let count = 0;
10
- const maxCount = 5;
11
-
12
- function sendData() {
13
- setTimeout(() => {
14
- if (count < maxCount - 1) {
15
- stream.push(`data: ${getSteamChatObject()}\n\n`);
16
- count++;
17
- sendData(); // Call the function recursively until the last iteration
18
- } else if (count === maxCount - 1) {
19
- stream.push(`data: [DONE]\n\n`);
20
- stream.push(null); // End the stream after sending the data
21
- }
22
- }, 200);
23
- }
24
-
25
- sendData(); // Start sending data
26
-
27
- return stream;
28
- }
29
-
30
-
31
- export {
32
- createChatStream,
33
- };
package/src/image.js DELETED
@@ -1,24 +0,0 @@
1
- import { faker } from '@faker-js/faker';
2
-
3
- function getImageResponce(requestBody) {
4
-
5
- const n = requestBody.n || 1;
6
- const data = [];
7
- for (let i = 0; i < n; i++) {
8
- data.push({
9
- revised_prompt: faker.lorem.words(5),
10
- url: "http://via.placeholder.com/" + requestBody.size
11
- });
12
- }
13
-
14
- return {
15
- created: Math.floor(Date.now() / 1000),
16
- data: data
17
- };
18
- }
19
-
20
-
21
-
22
- export {
23
- getImageResponce,
24
- };
package/src/index.js DELETED
@@ -1,123 +0,0 @@
1
- import nock from 'nock';
2
- import { getChatResponce } from './chat.js';
3
- import { createChatStream } from './chat.stream.js';
4
- import { getImageResponce } from './image.js';
5
-
6
- const OPEN_AI_BASE_URL = 'https://api.openai.com';
7
- const CHAT_COMPLETIONS_ENDPOINT = '/v1/chat/completions';
8
- const IMAGE_GENERATIONS_ENDPOINT = '/v1/images/generations';
9
- const customScopes = []; // Track custom scopes
10
- /**
11
- * Mock OpenAI API responses
12
- * @param {boolean} force - Force mocking regardless of environment
13
- * @param {Object} options - Additional configuration options
14
- * @param {boolean} options.includeErrors - Whether to include error scenarios in mocking
15
- * @param {number} options.latency - Artificial latency in ms to simulate network delay
16
- * @param {boolean} options.logRequests - Whether to log incoming requests
17
- * @returns {Object} An object with control methods for the mocks
18
- */
19
- export function mockOpenAIResponse(force = false, options = {}) {
20
- const {
21
- includeErrors = false,
22
- latency = 0,
23
- logRequests = false
24
- } = options;
25
-
26
- const env = process.env.NODE_ENV || 'development';
27
-
28
- // Only proceed if in development or forced
29
- if (env !== 'development' && !force) {
30
- return { isActive: false, stopMocking };
31
- }
32
-
33
- // Mock chat completions endpoint
34
- const chatScope = nock(OPEN_AI_BASE_URL)
35
- .post(CHAT_COMPLETIONS_ENDPOINT)
36
- .delay(latency) // Add delay to the interceptor
37
- .reply(function (uri, requestBody) {
38
- if (logRequests) {
39
- console.log(`[openai-api-mock] Chat request:`, JSON.stringify(requestBody, null, 2));
40
- }
41
-
42
- try {
43
- // Validate minimal required fields
44
- if (!requestBody.model || !requestBody.messages || !Array.isArray(requestBody.messages)) {
45
- return [400, { error: { message: 'Invalid request. Missing required fields.' } }];
46
- }
47
-
48
- // Simulate random errors if enabled
49
- if (includeErrors && Math.random() < 0.05) {
50
- return [429, { error: { message: 'Rate limit exceeded' } }];
51
- }
52
-
53
- const isSteaming = requestBody.stream === true;
54
-
55
- if (isSteaming) {
56
- const stream = createChatStream(requestBody);
57
- return [200, stream];
58
- }
59
-
60
- return [200, getChatResponce(requestBody)];
61
- } catch (error) {
62
- console.error('[openai-api-mock] Error processing chat request:', error);
63
- return [500, { error: { message: 'Internal server error in mock' } }];
64
- }
65
- });
66
-
67
- // Mock image generations endpoint
68
- const imageScope = nock(OPEN_AI_BASE_URL)
69
- .post(IMAGE_GENERATIONS_ENDPOINT)
70
- .delay(latency) // Add delay to the interceptor
71
- .reply(function (uri, requestBody) {
72
- if (logRequests) {
73
- console.log(`[openai-api-mock] Image request:`, JSON.stringify(requestBody, null, 2));
74
- }
75
-
76
- try {
77
- // Validate minimal required fields
78
- if (!requestBody.prompt) {
79
- return [400, { error: { message: 'Invalid request. Missing prompt.' } }];
80
- }
81
-
82
- // Simulate random errors if enabled
83
- if (includeErrors && Math.random() < 0.05) {
84
- return [400, { error: { message: 'Your request was rejected as a result of our safety system.' } }];
85
- }
86
-
87
- return [200, getImageResponce(requestBody)];
88
- } catch (error) {
89
- console.error('[openai-api-mock] Error processing image request:', error);
90
- return [500, { error: { message: 'Internal server error in mock' } }];
91
- }
92
- });
93
-
94
- // Enable other network connections
95
- nock.enableNetConnect(host => host !== "api.openai.com");
96
-
97
-
98
-
99
- return {
100
- isActive: true,
101
- stopMocking,
102
-
103
- /**
104
- * Adds a custom endpoint mock
105
- * @param {string} method - HTTP method (e.g., 'GET', 'POST')
106
- * @param {string} path - Endpoint path (e.g., '/v1/custom')
107
- * @param {Function} handler - Function returning [statusCode, responseBody]
108
- */
109
- addCustomEndpoint(method, path, handler) {
110
- const methodLower = method.toLowerCase();
111
- const scope = nock(OPEN_AI_BASE_URL)
112
- [methodLower](path)
113
- .reply(handler)
114
- .persist(); // Keep the mock active indefinitely
115
- customScopes.push(scope);
116
- }
117
- };
118
- }
119
-
120
- export function stopMocking() {
121
- nock.cleanAll();
122
- customScopes.forEach(scope => scope.persist(false)); // Disable persistence
123
- }