@inductiv/node-red-openai-api 1.103.0-patch.1 → 6.22.0
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 +165 -100
- package/examples/responses/mcp.json +1 -1
- package/lib.js +6917 -13226
- package/locales/en-US/node.json +48 -1
- package/node.html +1513 -962
- package/node.js +194 -54
- package/package.json +8 -7
- package/src/assistants/help.html +1 -77
- package/src/audio/help.html +1 -37
- package/src/batch/help.html +3 -17
- package/src/chat/help.html +11 -89
- package/src/container-files/help.html +1 -27
- package/src/containers/help.html +8 -18
- package/src/conversations/help.html +135 -0
- package/src/conversations/methods.js +73 -0
- package/src/conversations/template.html +10 -0
- package/src/embeddings/help.html +1 -11
- package/src/evals/help.html +249 -0
- package/src/evals/methods.js +114 -0
- package/src/evals/template.html +14 -0
- package/src/files/help.html +4 -17
- package/src/fine-tuning/help.html +1 -35
- package/src/images/help.html +1 -45
- package/src/lib.js +51 -1
- package/src/messages/help.html +1 -41
- package/src/models/help.html +1 -5
- package/src/moderations/help.html +1 -5
- package/src/node.html +126 -37
- package/src/realtime/help.html +129 -0
- package/src/realtime/methods.js +45 -0
- package/src/realtime/template.html +7 -0
- package/src/responses/help.html +203 -61
- package/src/responses/methods.js +49 -16
- package/src/responses/template.html +16 -1
- package/src/runs/help.html +1 -123
- package/src/skills/help.html +183 -0
- package/src/skills/methods.js +99 -0
- package/src/skills/template.html +13 -0
- package/src/threads/help.html +1 -15
- package/src/uploads/help.html +1 -21
- package/src/vector-store-file-batches/help.html +1 -27
- package/src/vector-store-file-batches/methods.js +1 -1
- package/src/vector-store-files/help.html +1 -25
- package/src/vector-store-files/methods.js +5 -2
- package/src/vector-stores/help.html +2 -31
- package/src/videos/help.html +113 -0
- package/src/videos/methods.js +50 -0
- package/src/videos/template.html +8 -0
- package/src/webhooks/help.html +61 -0
- package/src/webhooks/methods.js +40 -0
- package/src/webhooks/template.html +4 -0
- package/test/openai-methods-mapping.test.js +1220 -0
- package/test/openai-node-auth-routing.test.js +203 -0
- package/test/service-host-editor-template.test.js +53 -0
- package/test/service-host-node.test.js +182 -0
- package/test/services.test.js +147 -0
- package/test/utils.test.js +75 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require("node:assert/strict");
|
|
4
|
+
const EventEmitter = require("node:events");
|
|
5
|
+
const test = require("node:test");
|
|
6
|
+
|
|
7
|
+
const OpenaiApi = require("../lib.js");
|
|
8
|
+
const nodeModule = require("../node.js");
|
|
9
|
+
|
|
10
|
+
function createEvaluateNodeProperty() {
|
|
11
|
+
return (value, type, node, msg, callback) => {
|
|
12
|
+
let resolvedValue = value;
|
|
13
|
+
|
|
14
|
+
if (type === "env" || type === "msg" || type === "flow" || type === "global") {
|
|
15
|
+
resolvedValue = `resolved:${type}:${value}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof callback === "function") {
|
|
19
|
+
callback(null, resolvedValue);
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return resolvedValue;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createNodeHarness() {
|
|
28
|
+
const registeredTypes = {};
|
|
29
|
+
const configNodes = new Map();
|
|
30
|
+
|
|
31
|
+
const RED = {
|
|
32
|
+
nodes: {
|
|
33
|
+
createNode: (node, config) => {
|
|
34
|
+
const emitter = new EventEmitter();
|
|
35
|
+
node.on = emitter.on.bind(emitter);
|
|
36
|
+
node.emit = emitter.emit.bind(emitter);
|
|
37
|
+
|
|
38
|
+
node.sentMessages = [];
|
|
39
|
+
node.errorMessages = [];
|
|
40
|
+
node.send = (msg) => {
|
|
41
|
+
node.sentMessages.push(msg);
|
|
42
|
+
};
|
|
43
|
+
node.error = (error, msg) => {
|
|
44
|
+
node.errorMessages.push({ error, msg });
|
|
45
|
+
};
|
|
46
|
+
node.status = () => { };
|
|
47
|
+
node.context = () => ({
|
|
48
|
+
flow: { get: () => undefined },
|
|
49
|
+
global: { get: () => undefined },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
node.credentials = config.credentials || {};
|
|
53
|
+
node.id = config.id;
|
|
54
|
+
},
|
|
55
|
+
registerType: (name, ctor) => {
|
|
56
|
+
registeredTypes[name] = ctor;
|
|
57
|
+
},
|
|
58
|
+
getNode: (id) => configNodes.get(id),
|
|
59
|
+
},
|
|
60
|
+
util: {
|
|
61
|
+
evaluateNodeProperty: createEvaluateNodeProperty(),
|
|
62
|
+
getMessageProperty: (msg, path) => {
|
|
63
|
+
if (path === "payload") {
|
|
64
|
+
return msg.payload;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return path.split(".").reduce((value, key) => {
|
|
68
|
+
if (value === undefined || value === null) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return value[key];
|
|
72
|
+
}, msg);
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
nodeModule(RED);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
OpenaiApiNode: registeredTypes["OpenAI API"],
|
|
81
|
+
ServiceHostNode: registeredTypes["Service Host"],
|
|
82
|
+
configNodes,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function runAuthRoutingCase(serviceConfig) {
|
|
87
|
+
const harness = createNodeHarness();
|
|
88
|
+
assert.ok(harness.OpenaiApiNode, "OpenAI API node should register");
|
|
89
|
+
assert.ok(harness.ServiceHostNode, "Service Host node should register");
|
|
90
|
+
|
|
91
|
+
const serviceNode = new harness.ServiceHostNode({
|
|
92
|
+
id: "service-1",
|
|
93
|
+
apiBase: "https://api.example.com/v1",
|
|
94
|
+
apiBaseType: "str",
|
|
95
|
+
secureApiKeyHeaderOrQueryName: "Authorization",
|
|
96
|
+
secureApiKeyHeaderOrQueryNameType: "str",
|
|
97
|
+
organizationId: "OPENAI_ORG_ID",
|
|
98
|
+
organizationIdType: "env",
|
|
99
|
+
secureApiKeyIsQuery: false,
|
|
100
|
+
secureApiKeyValueType: "cred",
|
|
101
|
+
credentials: {
|
|
102
|
+
secureApiKeyValue: "sk-test",
|
|
103
|
+
},
|
|
104
|
+
...serviceConfig,
|
|
105
|
+
});
|
|
106
|
+
harness.configNodes.set("service-1", serviceNode);
|
|
107
|
+
|
|
108
|
+
const apiNode = new harness.OpenaiApiNode({
|
|
109
|
+
id: "openai-1",
|
|
110
|
+
service: "service-1",
|
|
111
|
+
method: "createModelResponse",
|
|
112
|
+
property: "payload",
|
|
113
|
+
propertyType: "msg",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
apiNode.emit("input", { payload: { model: "gpt-5-nano" } });
|
|
117
|
+
|
|
118
|
+
// Allow Promise-based input handler to resolve and send.
|
|
119
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
120
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
121
|
+
|
|
122
|
+
assert.equal(apiNode.errorMessages.length, 0);
|
|
123
|
+
assert.equal(apiNode.sentMessages.length, 1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
test("applies custom auth header to OpenAI client params", async () => {
|
|
127
|
+
const originalCreateModelResponse = OpenaiApi.prototype.createModelResponse;
|
|
128
|
+
const capturedClientParams = [];
|
|
129
|
+
|
|
130
|
+
OpenaiApi.prototype.createModelResponse = async function () {
|
|
131
|
+
capturedClientParams.push({ ...this.clientParams });
|
|
132
|
+
return { ok: true };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await runAuthRoutingCase({
|
|
137
|
+
secureApiKeyHeaderOrQueryName: "X-API-Key",
|
|
138
|
+
secureApiKeyHeaderOrQueryNameType: "str",
|
|
139
|
+
secureApiKeyIsQuery: false,
|
|
140
|
+
});
|
|
141
|
+
} finally {
|
|
142
|
+
OpenaiApi.prototype.createModelResponse = originalCreateModelResponse;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
assert.equal(capturedClientParams.length, 1);
|
|
146
|
+
assert.deepEqual(capturedClientParams[0].defaultHeaders, {
|
|
147
|
+
Authorization: null,
|
|
148
|
+
"X-API-Key": "sk-test",
|
|
149
|
+
});
|
|
150
|
+
assert.equal(capturedClientParams[0].defaultQuery, undefined);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("applies query-string auth mode with typed header name", async () => {
|
|
154
|
+
const originalCreateModelResponse = OpenaiApi.prototype.createModelResponse;
|
|
155
|
+
const capturedClientParams = [];
|
|
156
|
+
|
|
157
|
+
OpenaiApi.prototype.createModelResponse = async function () {
|
|
158
|
+
capturedClientParams.push({ ...this.clientParams });
|
|
159
|
+
return { ok: true };
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
await runAuthRoutingCase({
|
|
164
|
+
secureApiKeyHeaderOrQueryName: "OPENAI_AUTH_QUERY_PARAM",
|
|
165
|
+
secureApiKeyHeaderOrQueryNameType: "env",
|
|
166
|
+
secureApiKeyIsQuery: "true",
|
|
167
|
+
});
|
|
168
|
+
} finally {
|
|
169
|
+
OpenaiApi.prototype.createModelResponse = originalCreateModelResponse;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
assert.equal(capturedClientParams.length, 1);
|
|
173
|
+
assert.deepEqual(capturedClientParams[0].defaultHeaders, {
|
|
174
|
+
Authorization: null,
|
|
175
|
+
});
|
|
176
|
+
assert.deepEqual(capturedClientParams[0].defaultQuery, {
|
|
177
|
+
"resolved:env:OPENAI_AUTH_QUERY_PARAM": "sk-test",
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("keeps OpenAI SDK default Authorization behavior when header stays Authorization", async () => {
|
|
182
|
+
const originalCreateModelResponse = OpenaiApi.prototype.createModelResponse;
|
|
183
|
+
const capturedClientParams = [];
|
|
184
|
+
|
|
185
|
+
OpenaiApi.prototype.createModelResponse = async function () {
|
|
186
|
+
capturedClientParams.push({ ...this.clientParams });
|
|
187
|
+
return { ok: true };
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await runAuthRoutingCase({
|
|
192
|
+
secureApiKeyHeaderOrQueryName: "Authorization",
|
|
193
|
+
secureApiKeyHeaderOrQueryNameType: "str",
|
|
194
|
+
secureApiKeyIsQuery: false,
|
|
195
|
+
});
|
|
196
|
+
} finally {
|
|
197
|
+
OpenaiApi.prototype.createModelResponse = originalCreateModelResponse;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
assert.equal(capturedClientParams.length, 1);
|
|
201
|
+
assert.equal(capturedClientParams[0].defaultHeaders, undefined);
|
|
202
|
+
assert.equal(capturedClientParams[0].defaultQuery, undefined);
|
|
203
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require("node:assert/strict");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const test = require("node:test");
|
|
7
|
+
|
|
8
|
+
const templatePath = path.join(__dirname, "..", "src", "node.html");
|
|
9
|
+
const template = fs.readFileSync(templatePath, "utf8");
|
|
10
|
+
|
|
11
|
+
test("service host API key editor uses native typedInput cred handling", () => {
|
|
12
|
+
assert.match(
|
|
13
|
+
template,
|
|
14
|
+
/types: \["cred", "env", "msg", "flow", "global"\],/
|
|
15
|
+
);
|
|
16
|
+
assert.match(
|
|
17
|
+
template,
|
|
18
|
+
/<input type="text" id="node-config-input-secureApiKeyValue" placeholder="" \/>/
|
|
19
|
+
);
|
|
20
|
+
assert.match(
|
|
21
|
+
template,
|
|
22
|
+
/<input\s+type="text"\s+id="node-config-input-secureApiKeyHeaderOrQueryName"\s+placeholder="Authorization"\s+\/>/
|
|
23
|
+
);
|
|
24
|
+
assert.match(
|
|
25
|
+
template,
|
|
26
|
+
/if \(inputValue && inputValue !== "__PWRD__"\) \{/
|
|
27
|
+
);
|
|
28
|
+
assert.match(
|
|
29
|
+
template,
|
|
30
|
+
/const syncApiKeyInputValue = function \(\) \{/
|
|
31
|
+
);
|
|
32
|
+
assert.match(
|
|
33
|
+
template,
|
|
34
|
+
/if \(selectedType !== "cred" && currentValue === "__PWRD__"\) \{/
|
|
35
|
+
);
|
|
36
|
+
assert.match(
|
|
37
|
+
template,
|
|
38
|
+
/apiKeyInput\.typedInput\("value", apiKeyRef\.val\(\) \|\| ""\);/
|
|
39
|
+
);
|
|
40
|
+
assert.match(
|
|
41
|
+
template,
|
|
42
|
+
/if \(selectedType === "cred" && previousApiKeyType !== "cred"\) \{/
|
|
43
|
+
);
|
|
44
|
+
assert.match(
|
|
45
|
+
template,
|
|
46
|
+
/const inputValue = apiKeyInput\.typedInput\("value"\);/
|
|
47
|
+
);
|
|
48
|
+
assert.match(
|
|
49
|
+
template,
|
|
50
|
+
/const existingCredValue = apiKeyInput\.val\(\);/
|
|
51
|
+
);
|
|
52
|
+
assert.ok(!/apiKeyInput\.prop\("type"/.test(template));
|
|
53
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require("node:assert/strict");
|
|
4
|
+
const test = require("node:test");
|
|
5
|
+
|
|
6
|
+
const nodeModule = require("../node.js");
|
|
7
|
+
|
|
8
|
+
function createServiceHostNode(evaluateNodeProperty) {
|
|
9
|
+
let ServiceHostNode;
|
|
10
|
+
|
|
11
|
+
const RED = {
|
|
12
|
+
nodes: {
|
|
13
|
+
createNode: (node, config) => {
|
|
14
|
+
node.credentials = config.credentials || {};
|
|
15
|
+
},
|
|
16
|
+
registerType: (name, ctor) => {
|
|
17
|
+
if (name === "Service Host") {
|
|
18
|
+
ServiceHostNode = ctor;
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
getNode: () => undefined,
|
|
22
|
+
},
|
|
23
|
+
util: {
|
|
24
|
+
evaluateNodeProperty,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
nodeModule(RED);
|
|
29
|
+
assert.ok(ServiceHostNode, "Service Host constructor should register");
|
|
30
|
+
return ServiceHostNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createEvaluateNodeProperty(calls, options = {}) {
|
|
34
|
+
return (value, type, node, msg, callback) => {
|
|
35
|
+
calls.push({ value, type, node, msg, hasCallback: typeof callback === "function" });
|
|
36
|
+
|
|
37
|
+
if (type === "flow" || type === "global") {
|
|
38
|
+
const resolvedValue = `resolved:${type}:${value}`;
|
|
39
|
+
if (typeof callback === "function") {
|
|
40
|
+
const deliver = options.asyncFlowGlobal ? setImmediate : (fn) => fn();
|
|
41
|
+
deliver(() => callback(null, resolvedValue));
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return resolvedValue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (type === "str" || type === "cred") {
|
|
48
|
+
if (typeof callback === "function") {
|
|
49
|
+
callback(null, value);
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const resolvedValue = `resolved:${type}:${value}`;
|
|
56
|
+
if (typeof callback === "function") {
|
|
57
|
+
callback(null, resolvedValue);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return resolvedValue;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
test("uses credential value when API key type is cred", () => {
|
|
65
|
+
const calls = [];
|
|
66
|
+
const ServiceHostNode = createServiceHostNode(createEvaluateNodeProperty(calls));
|
|
67
|
+
const node = new ServiceHostNode({
|
|
68
|
+
secureApiKeyValueType: "cred",
|
|
69
|
+
secureApiKeyValueRef: "",
|
|
70
|
+
credentials: { secureApiKeyValue: "sk-test" },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const result = node.evaluateTyped("secureApiKeyValue", {}, node);
|
|
74
|
+
|
|
75
|
+
assert.equal(result, "sk-test");
|
|
76
|
+
assert.equal(calls.length, 1);
|
|
77
|
+
assert.equal(calls[0].value, "sk-test");
|
|
78
|
+
assert.equal(calls[0].type, "cred");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("uses explicit reference value when API key type is env", () => {
|
|
82
|
+
const calls = [];
|
|
83
|
+
const ServiceHostNode = createServiceHostNode(createEvaluateNodeProperty(calls));
|
|
84
|
+
const node = new ServiceHostNode({
|
|
85
|
+
secureApiKeyValueType: "env",
|
|
86
|
+
secureApiKeyValueRef: "OPENAI_API_KEY",
|
|
87
|
+
credentials: { secureApiKeyValue: "sk-ignored" },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const result = node.evaluateTyped("secureApiKeyValue", {}, node);
|
|
91
|
+
|
|
92
|
+
assert.equal(result, "resolved:env:OPENAI_API_KEY");
|
|
93
|
+
assert.equal(calls.length, 1);
|
|
94
|
+
assert.equal(calls[0].value, "OPENAI_API_KEY");
|
|
95
|
+
assert.equal(calls[0].type, "env");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("supports legacy non-cred nodes that stored env reference in credentials", () => {
|
|
99
|
+
const calls = [];
|
|
100
|
+
const ServiceHostNode = createServiceHostNode(createEvaluateNodeProperty(calls));
|
|
101
|
+
const node = new ServiceHostNode({
|
|
102
|
+
secureApiKeyValueType: "env",
|
|
103
|
+
credentials: { secureApiKeyValue: "OPENAI_API_KEY" },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = node.evaluateTyped("secureApiKeyValue", {}, node);
|
|
107
|
+
|
|
108
|
+
assert.equal(result, "resolved:env:OPENAI_API_KEY");
|
|
109
|
+
assert.equal(calls.length, 1);
|
|
110
|
+
assert.equal(calls[0].value, "OPENAI_API_KEY");
|
|
111
|
+
assert.equal(calls[0].type, "env");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("falls back to credential value when non-cred reference is empty", () => {
|
|
115
|
+
const calls = [];
|
|
116
|
+
const ServiceHostNode = createServiceHostNode(createEvaluateNodeProperty(calls));
|
|
117
|
+
const node = new ServiceHostNode({
|
|
118
|
+
secureApiKeyValueType: "env",
|
|
119
|
+
secureApiKeyValueRef: "",
|
|
120
|
+
credentials: { secureApiKeyValue: "OPENAI_API_KEY" },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const result = node.evaluateTyped("secureApiKeyValue", {}, node);
|
|
124
|
+
|
|
125
|
+
assert.equal(result, "resolved:env:OPENAI_API_KEY");
|
|
126
|
+
assert.equal(calls.length, 1);
|
|
127
|
+
assert.equal(calls[0].value, "OPENAI_API_KEY");
|
|
128
|
+
assert.equal(calls[0].type, "env");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("supports legacy typed value stored only in credentials type field", () => {
|
|
132
|
+
const calls = [];
|
|
133
|
+
const ServiceHostNode = createServiceHostNode(createEvaluateNodeProperty(calls));
|
|
134
|
+
const node = new ServiceHostNode({
|
|
135
|
+
credentials: {
|
|
136
|
+
secureApiKeyValue: "OPENAI_API_KEY",
|
|
137
|
+
secureApiKeyValueType: "env",
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const result = node.evaluateTyped("secureApiKeyValue", {}, node);
|
|
142
|
+
|
|
143
|
+
assert.equal(result, "resolved:env:OPENAI_API_KEY");
|
|
144
|
+
assert.equal(calls.length, 1);
|
|
145
|
+
assert.equal(calls[0].value, "OPENAI_API_KEY");
|
|
146
|
+
assert.equal(calls[0].type, "env");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("supports legacy string API key values without reference field", () => {
|
|
150
|
+
const calls = [];
|
|
151
|
+
const ServiceHostNode = createServiceHostNode(createEvaluateNodeProperty(calls));
|
|
152
|
+
const node = new ServiceHostNode({
|
|
153
|
+
secureApiKeyValueType: "str",
|
|
154
|
+
credentials: { secureApiKeyValue: "sk-legacy" },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const result = node.evaluateTyped("secureApiKeyValue", {}, node);
|
|
158
|
+
|
|
159
|
+
assert.equal(result, "sk-legacy");
|
|
160
|
+
assert.equal(calls.length, 1);
|
|
161
|
+
assert.equal(calls[0].value, "sk-legacy");
|
|
162
|
+
assert.equal(calls[0].type, "str");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("evaluateTypedAsync resolves flow/global types using callback path", async () => {
|
|
166
|
+
const calls = [];
|
|
167
|
+
const ServiceHostNode = createServiceHostNode(
|
|
168
|
+
createEvaluateNodeProperty(calls, { asyncFlowGlobal: true })
|
|
169
|
+
);
|
|
170
|
+
const node = new ServiceHostNode({
|
|
171
|
+
secureApiKeyValueType: "flow",
|
|
172
|
+
secureApiKeyValueRef: "openai.key",
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const result = await node.evaluateTypedAsync("secureApiKeyValue", {}, node);
|
|
176
|
+
|
|
177
|
+
assert.equal(result, "resolved:flow:openai.key");
|
|
178
|
+
assert.equal(calls.length, 1);
|
|
179
|
+
assert.equal(calls[0].value, "openai.key");
|
|
180
|
+
assert.equal(calls[0].type, "flow");
|
|
181
|
+
assert.equal(calls[0].hasCallback, true);
|
|
182
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require("assert");
|
|
4
|
+
const { createClient } = require("../src/client");
|
|
5
|
+
const { createApi } = require("../src/index");
|
|
6
|
+
|
|
7
|
+
// Mock the OpenAI API client
|
|
8
|
+
jest.mock("../src/client", () => {
|
|
9
|
+
return {
|
|
10
|
+
createClient: jest.fn().mockReturnValue({
|
|
11
|
+
chat: {
|
|
12
|
+
completions: {
|
|
13
|
+
create: jest.fn().mockResolvedValue({ id: "chat-123", choices: [] }),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
beta: {
|
|
17
|
+
threads: {
|
|
18
|
+
create: jest.fn().mockResolvedValue({ id: "thread-123" }),
|
|
19
|
+
messages: {
|
|
20
|
+
create: jest.fn().mockResolvedValue({ id: "message-123" }),
|
|
21
|
+
list: jest
|
|
22
|
+
.fn()
|
|
23
|
+
.mockResolvedValue({ data: [{ id: "message-123" }] }),
|
|
24
|
+
retrieve: jest.fn().mockResolvedValue({ id: "message-123" }),
|
|
25
|
+
update: jest
|
|
26
|
+
.fn()
|
|
27
|
+
.mockResolvedValue({ id: "message-123", updated: true }),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
batches: {
|
|
32
|
+
create: jest.fn().mockResolvedValue({ id: "batch-123" }),
|
|
33
|
+
retrieve: jest
|
|
34
|
+
.fn()
|
|
35
|
+
.mockResolvedValue({ id: "batch-123", status: "pending" }),
|
|
36
|
+
list: jest.fn().mockResolvedValue({ data: [{ id: "batch-123" }] }),
|
|
37
|
+
cancel: jest
|
|
38
|
+
.fn()
|
|
39
|
+
.mockResolvedValue({ id: "batch-123", cancelled: true }),
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("API Services", () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
jest.clearAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("ChatService", () => {
|
|
51
|
+
it("should create chat completion", async () => {
|
|
52
|
+
const api = createApi({ apiKey: "test-key" });
|
|
53
|
+
const result = await api.chat.createChatCompletion({
|
|
54
|
+
payload: { messages: [{ role: "user", content: "Hello" }] },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
assert.strictEqual(result.id, "chat-123");
|
|
58
|
+
expect(createClient).toHaveBeenCalledWith({ apiKey: "test-key" });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("MessagesService", () => {
|
|
63
|
+
it("should create a message", async () => {
|
|
64
|
+
const api = createApi({ apiKey: "test-key" });
|
|
65
|
+
const result = await api.messages.createMessage({
|
|
66
|
+
payload: { thread_id: "thread-123", content: "Hello" },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
assert.strictEqual(result.id, "message-123");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should list messages", async () => {
|
|
73
|
+
const api = createApi({ apiKey: "test-key" });
|
|
74
|
+
const result = await api.messages.listMessages({
|
|
75
|
+
payload: { thread_id: "thread-123" },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
assert.strictEqual(result.length, 1);
|
|
79
|
+
assert.strictEqual(result[0].id, "message-123");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should get a message", async () => {
|
|
83
|
+
const api = createApi({ apiKey: "test-key" });
|
|
84
|
+
const result = await api.messages.getMessage({
|
|
85
|
+
payload: { thread_id: "thread-123", message_id: "message-123" },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert.strictEqual(result.id, "message-123");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should modify a message", async () => {
|
|
92
|
+
const api = createApi({ apiKey: "test-key" });
|
|
93
|
+
const result = await api.messages.modifyMessage({
|
|
94
|
+
payload: {
|
|
95
|
+
thread_id: "thread-123",
|
|
96
|
+
message_id: "message-123",
|
|
97
|
+
metadata: { updated: true },
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
assert.strictEqual(result.updated, true);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("BatchesService", () => {
|
|
106
|
+
it("should create a batch", async () => {
|
|
107
|
+
const api = createApi({ apiKey: "test-key" });
|
|
108
|
+
const result = await api.batches.createBatch({
|
|
109
|
+
payload: {
|
|
110
|
+
requests: [
|
|
111
|
+
{ model: "gpt-4", messages: [{ role: "user", content: "Hello" }] },
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
assert.strictEqual(result.id, "batch-123");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should retrieve a batch", async () => {
|
|
120
|
+
const api = createApi({ apiKey: "test-key" });
|
|
121
|
+
const result = await api.batches.retrieveBatch({
|
|
122
|
+
payload: { batch_id: "batch-123" },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
assert.strictEqual(result.status, "pending");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should list batches", async () => {
|
|
129
|
+
const api = createApi({ apiKey: "test-key" });
|
|
130
|
+
const result = await api.batches.listBatches({
|
|
131
|
+
payload: {},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
assert.strictEqual(result.length, 1);
|
|
135
|
+
assert.strictEqual(result[0].id, "batch-123");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should cancel a batch", async () => {
|
|
139
|
+
const api = createApi({ apiKey: "test-key" });
|
|
140
|
+
const result = await api.batches.cancelBatch({
|
|
141
|
+
payload: { batch_id: "batch-123" },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
assert.strictEqual(result.cancelled, true);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require("assert");
|
|
4
|
+
const { validation, streaming, fileHelpers } = require("../src/index").utils;
|
|
5
|
+
|
|
6
|
+
describe("Utility Functions", () => {
|
|
7
|
+
describe("Validation", () => {
|
|
8
|
+
it("should validate required parameters", () => {
|
|
9
|
+
const schema = {
|
|
10
|
+
required: ["name"],
|
|
11
|
+
properties: {
|
|
12
|
+
name: { type: "string" },
|
|
13
|
+
age: { type: "number" },
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Valid case
|
|
18
|
+
expect(() =>
|
|
19
|
+
validation.validateParams({ name: "Test" }, schema)
|
|
20
|
+
).not.toThrow();
|
|
21
|
+
|
|
22
|
+
// Invalid case - missing required field
|
|
23
|
+
expect(() => validation.validateParams({}, schema)).toThrow();
|
|
24
|
+
|
|
25
|
+
// Invalid case - wrong type
|
|
26
|
+
expect(() => validation.validateParams({ name: 123 }, schema)).toThrow();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("Streaming", () => {
|
|
31
|
+
it("should create a streaming handler", () => {
|
|
32
|
+
const onData = jest.fn();
|
|
33
|
+
const onError = jest.fn();
|
|
34
|
+
const onFinish = jest.fn();
|
|
35
|
+
|
|
36
|
+
const handler = streaming.createStreamHandler({
|
|
37
|
+
onData,
|
|
38
|
+
onError,
|
|
39
|
+
onFinish,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(handler).toHaveProperty("controller");
|
|
43
|
+
expect(handler).toHaveProperty("stream");
|
|
44
|
+
|
|
45
|
+
// Mock data event
|
|
46
|
+
const mockChunk = { choices: [{ delta: { content: "Hello" } }] };
|
|
47
|
+
handler.onChunk(mockChunk);
|
|
48
|
+
expect(onData).toHaveBeenCalledWith(mockChunk);
|
|
49
|
+
|
|
50
|
+
// Mock done event
|
|
51
|
+
handler.onDone();
|
|
52
|
+
expect(onFinish).toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("File Helpers", () => {
|
|
57
|
+
it("should determine file type from path", () => {
|
|
58
|
+
expect(fileHelpers.getFileTypeFromPath("image.png")).toBe("image");
|
|
59
|
+
expect(fileHelpers.getFileTypeFromPath("audio.mp3")).toBe("audio");
|
|
60
|
+
expect(fileHelpers.getFileTypeFromPath("document.pdf")).toBe("document");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should validate file size", () => {
|
|
64
|
+
const mockFile = { size: 1024 * 1024 }; // 1MB
|
|
65
|
+
|
|
66
|
+
// File under limit
|
|
67
|
+
expect(fileHelpers.validateFileSize(mockFile, 2 * 1024 * 1024)).toBe(
|
|
68
|
+
true
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// File exceeds limit
|
|
72
|
+
expect(fileHelpers.validateFileSize(mockFile, 512 * 1024)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|