weflayr 0.2.0 → 0.3.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/package.json +2 -2
- package/src/index.js +2 -2
- package/src/instrument.js +89 -50
- package/src/providers/anthropic-ai-sdk.js +63 -0
- package/tests/index.test.js +191 -0
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weflayr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Weflayr Node.js SDK — instrument any LLM client via JS Proxy",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "node --test tests/"
|
|
7
|
+
"test": "node --test tests/index.test.js"
|
|
8
8
|
},
|
|
9
9
|
"keywords": ["llm", "observability", "weflayr"],
|
|
10
10
|
"author": "Weflayr <contact@weflayr.com>",
|
package/src/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require('dotenv').config();
|
|
4
4
|
|
|
5
|
-
const { configure, weflayr_instrument } = require('./instrument');
|
|
5
|
+
const { configure, weflayr_instrument, send_event } = require('./instrument');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
*
|
|
@@ -26,4 +26,4 @@ function weflayr_setup(settings, { enabled = true } = {}) {
|
|
|
26
26
|
configure(intakeUrl, clientId, clientSecret, settings);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
module.exports = { weflayr_setup, weflayr_instrument };
|
|
29
|
+
module.exports = { weflayr_setup, weflayr_instrument, send_event };
|
package/src/instrument.js
CHANGED
|
@@ -14,7 +14,7 @@ function weflayr_instrument(target) {
|
|
|
14
14
|
if (!_config) return target;
|
|
15
15
|
|
|
16
16
|
if (typeof target === 'function') {
|
|
17
|
-
const methodConfig = _config.settings.methods.find(m => m.call === target.name);
|
|
17
|
+
const methodConfig = (_config.settings.methods || []).find(m => m.call === target.name);
|
|
18
18
|
if (!methodConfig) return target;
|
|
19
19
|
return _wrapFn(target, target.name, methodConfig);
|
|
20
20
|
}
|
|
@@ -35,7 +35,7 @@ function _makeProxy(target, pathPrefix) {
|
|
|
35
35
|
const fullPath = pathPrefix ? `${pathPrefix}.${prop}` : prop;
|
|
36
36
|
|
|
37
37
|
if (typeof value === 'function') {
|
|
38
|
-
const methodConfig = _config.settings.methods.find(m => m.call === fullPath);
|
|
38
|
+
const methodConfig = (_config.settings.methods || []).find(m => m.call === fullPath);
|
|
39
39
|
if (methodConfig) {
|
|
40
40
|
return _wrapFn(value.bind(obj), fullPath, methodConfig);
|
|
41
41
|
}
|
|
@@ -57,16 +57,19 @@ function _wrapFn(fn, methodName, methodConfig) {
|
|
|
57
57
|
const startTime = Date.now();
|
|
58
58
|
const { settings } = _config;
|
|
59
59
|
const eventId = randomUUID();
|
|
60
|
+
// For multi-arg methods (e.g. convert(voiceId, request)), use the last arg as the
|
|
61
|
+
// request object so middleware and content policy see the options, not the id.
|
|
62
|
+
const requestArgs = cleanArgs[cleanArgs.length - 1];
|
|
60
63
|
|
|
61
64
|
if (settings.event_mode !== 'light') {
|
|
62
65
|
const beforeMiddlewareData = methodConfig.middleware
|
|
63
|
-
? methodConfig.middleware(
|
|
66
|
+
? methodConfig.middleware(requestArgs, null) || {}
|
|
64
67
|
: {};
|
|
65
68
|
_sendEvent('before', {
|
|
66
69
|
event_id: eventId,
|
|
67
70
|
method: methodName,
|
|
68
71
|
tags,
|
|
69
|
-
args: _applyContentPolicy(
|
|
72
|
+
args: _applyContentPolicy(requestArgs, settings),
|
|
70
73
|
...beforeMiddlewareData,
|
|
71
74
|
});
|
|
72
75
|
}
|
|
@@ -76,18 +79,18 @@ function _wrapFn(fn, methodName, methodConfig) {
|
|
|
76
79
|
|
|
77
80
|
if (_isAsyncIterable(result)) {
|
|
78
81
|
_sendEvent('stream_start', { event_id: eventId, method: methodName, tags });
|
|
79
|
-
return _wrapStream(result, methodName, tags,
|
|
82
|
+
return _wrapStream(result, methodName, tags, requestArgs, settings, startTime, methodConfig, eventId);
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
const middlewareData = methodConfig.middleware
|
|
83
|
-
? methodConfig.middleware(
|
|
86
|
+
? methodConfig.middleware(requestArgs, result) || {}
|
|
84
87
|
: {};
|
|
85
88
|
|
|
86
89
|
_sendEvent('after', {
|
|
87
90
|
event_id: eventId,
|
|
88
91
|
method: methodName,
|
|
89
92
|
tags,
|
|
90
|
-
args: _applyContentPolicy(
|
|
93
|
+
args: _applyContentPolicy(requestArgs, settings),
|
|
91
94
|
response: _applyContentPolicy(_toPlain(result), settings),
|
|
92
95
|
elapsed_ms: Date.now() - startTime,
|
|
93
96
|
...middlewareData,
|
|
@@ -95,13 +98,13 @@ function _wrapFn(fn, methodName, methodConfig) {
|
|
|
95
98
|
|
|
96
99
|
return result;
|
|
97
100
|
} catch (err) {
|
|
98
|
-
_sendEvent('after', {
|
|
101
|
+
await _sendEvent('after', {
|
|
99
102
|
event_id: eventId,
|
|
100
103
|
method: methodName,
|
|
101
104
|
tags,
|
|
102
|
-
args: _applyContentPolicy(
|
|
103
|
-
error: err.message,
|
|
104
|
-
status_code: err.status,
|
|
105
|
+
args: _applyContentPolicy(requestArgs, settings),
|
|
106
|
+
error: err.message ?? 'unknown',
|
|
107
|
+
status_code: err.status ?? err.statusCode ?? 'unknown',
|
|
105
108
|
elapsed_ms: Date.now() - startTime,
|
|
106
109
|
});
|
|
107
110
|
throw err;
|
|
@@ -111,16 +114,27 @@ function _wrapFn(fn, methodName, methodConfig) {
|
|
|
111
114
|
|
|
112
115
|
async function* _wrapStream(stream, methodName, tags, requestArgs, settings, startTime, methodConfig, eventId) {
|
|
113
116
|
let lastChunk = null;
|
|
117
|
+
const accumulator = methodConfig.streamMiddleware ? methodConfig.streamMiddleware() : null;
|
|
114
118
|
|
|
115
119
|
try {
|
|
116
120
|
for await (const chunk of stream) {
|
|
117
121
|
lastChunk = chunk;
|
|
122
|
+
if (accumulator && accumulator.onChunk(chunk)) {
|
|
123
|
+
_sendEvent('stream_pending', {
|
|
124
|
+
event_id: eventId,
|
|
125
|
+
method: methodName,
|
|
126
|
+
tags,
|
|
127
|
+
elapsed_ms: Date.now() - startTime,
|
|
128
|
+
...accumulator.finalize(),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
118
131
|
yield chunk;
|
|
119
132
|
}
|
|
120
133
|
|
|
121
|
-
const middlewareData =
|
|
122
|
-
?
|
|
123
|
-
: {}
|
|
134
|
+
const middlewareData = accumulator
|
|
135
|
+
? accumulator.finalize()
|
|
136
|
+
: (methodConfig.middleware && lastChunk ? methodConfig.middleware(requestArgs, lastChunk) || {}
|
|
137
|
+
: {});
|
|
124
138
|
|
|
125
139
|
_sendEvent('after', {
|
|
126
140
|
event_id: eventId,
|
|
@@ -132,12 +146,13 @@ async function* _wrapStream(stream, methodName, tags, requestArgs, settings, sta
|
|
|
132
146
|
...middlewareData,
|
|
133
147
|
});
|
|
134
148
|
} catch (err) {
|
|
135
|
-
_sendEvent('after', {
|
|
149
|
+
await _sendEvent('after', {
|
|
136
150
|
event_id: eventId,
|
|
137
151
|
method: methodName,
|
|
138
152
|
tags,
|
|
139
153
|
args: _applyContentPolicy(requestArgs, settings),
|
|
140
|
-
error: err.message,
|
|
154
|
+
error: err.message ?? 'unknown',
|
|
155
|
+
status_code: err.status ?? err.statusCode ?? 'unknown',
|
|
141
156
|
elapsed_ms: Date.now() - startTime,
|
|
142
157
|
});
|
|
143
158
|
throw err;
|
|
@@ -163,6 +178,7 @@ function _isAsyncIterable(value) {
|
|
|
163
178
|
|
|
164
179
|
function _toPlain(value) {
|
|
165
180
|
if (!value) return value;
|
|
181
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array || value instanceof ArrayBuffer) return null;
|
|
166
182
|
if (typeof value.toJSON === 'function') return value.toJSON();
|
|
167
183
|
return value;
|
|
168
184
|
}
|
|
@@ -177,15 +193,14 @@ function _applyContentPolicy(data, settings) {
|
|
|
177
193
|
return data;
|
|
178
194
|
}
|
|
179
195
|
|
|
180
|
-
if (settings.content_policy === '
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
_deleteField(clone, field);
|
|
196
|
+
if (settings.content_policy === 'ignore') {
|
|
197
|
+
for (const field of (settings.ignore_fields || [])) {
|
|
198
|
+
_deleteField(clone, field);
|
|
199
|
+
}
|
|
200
|
+
return clone;
|
|
186
201
|
}
|
|
187
202
|
|
|
188
|
-
return clone;
|
|
203
|
+
return _pickFields(clone, settings.allow_fields || []);
|
|
189
204
|
}
|
|
190
205
|
|
|
191
206
|
function _pickFields(obj, allowFields) {
|
|
@@ -234,15 +249,27 @@ function _deleteField(obj, path) {
|
|
|
234
249
|
}
|
|
235
250
|
}
|
|
236
251
|
|
|
252
|
+
const _debug = process.env.WEFLAYR_DEBUG === 'true';
|
|
253
|
+
|
|
237
254
|
function _sendEvent(eventType, data) {
|
|
238
255
|
const { intakeUrl, clientId, clientSecret } = _config;
|
|
239
256
|
const endpoint = `${intakeUrl.replace(/\/$/, '')}/${clientId}/`;
|
|
240
257
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
258
|
+
let body;
|
|
259
|
+
try {
|
|
260
|
+
body = JSON.stringify({
|
|
261
|
+
event_type: eventType,
|
|
262
|
+
timestamp: new Date().toISOString(),
|
|
263
|
+
...data,
|
|
264
|
+
});
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.warn(`[weflayr] could not serialize event "${eventType}":`, err.message);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (_debug) {
|
|
271
|
+
console.debug(`[weflayr] → ${eventType}`, JSON.parse(body));
|
|
272
|
+
}
|
|
246
273
|
|
|
247
274
|
let parsed;
|
|
248
275
|
try {
|
|
@@ -252,30 +279,42 @@ function _sendEvent(eventType, data) {
|
|
|
252
279
|
}
|
|
253
280
|
|
|
254
281
|
const lib = parsed.protocol === 'https:' ? https : http;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
res
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
282
|
+
return new Promise((resolve) => {
|
|
283
|
+
const req = lib.request({
|
|
284
|
+
hostname: parsed.hostname,
|
|
285
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
286
|
+
path: parsed.pathname,
|
|
287
|
+
method: 'POST',
|
|
288
|
+
headers: {
|
|
289
|
+
'Content-Type': 'application/json',
|
|
290
|
+
'Content-Length': Buffer.byteLength(body),
|
|
291
|
+
'Authorization': `Bearer ${clientSecret}`,
|
|
292
|
+
},
|
|
293
|
+
}, res => {
|
|
294
|
+
res.resume();
|
|
295
|
+
if (_debug) {
|
|
296
|
+
console.debug(`[weflayr] ← ${eventType} status ${res.statusCode}`);
|
|
297
|
+
}
|
|
298
|
+
if (res.statusCode >= 400) {
|
|
299
|
+
console.warn(`[weflayr] intake returned ${res.statusCode} for event "${eventType}"`);
|
|
300
|
+
}
|
|
301
|
+
resolve();
|
|
302
|
+
});
|
|
271
303
|
|
|
272
|
-
|
|
273
|
-
|
|
304
|
+
req.on('error', (err) => {
|
|
305
|
+
console.warn(`[weflayr] send error for event "${eventType}":`, err.message);
|
|
306
|
+
resolve();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
req.write(body);
|
|
310
|
+
req.end();
|
|
274
311
|
});
|
|
312
|
+
}
|
|
275
313
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
314
|
+
// Send an arbitrary event to the intake. Requires configure() to have been called first.
|
|
315
|
+
function send_event(eventType, data) {
|
|
316
|
+
if (!_config) return;
|
|
317
|
+
return _sendEvent(eventType, data);
|
|
279
318
|
}
|
|
280
319
|
|
|
281
|
-
module.exports = { configure, weflayr_instrument };
|
|
320
|
+
module.exports = { configure, weflayr_instrument, send_event };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Non-streaming: extract usage from the completed message response.
|
|
4
|
+
function middleware(request, response) {
|
|
5
|
+
if (!response) return { model: request?.model ?? null };
|
|
6
|
+
return {
|
|
7
|
+
model: response.model ?? request?.model ?? null,
|
|
8
|
+
input_tokens: response.usage?.input_tokens ?? null,
|
|
9
|
+
output_tokens: response.usage?.output_tokens ?? null,
|
|
10
|
+
cache_creation_input_tokens: response.usage?.cache_creation_input_tokens ?? null,
|
|
11
|
+
cache_read_input_tokens: response.usage?.cache_read_input_tokens ?? null,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Streaming: Anthropic splits usage across two events.
|
|
16
|
+
// message_start → usage.input_tokens (prompt cost)
|
|
17
|
+
// message_delta → usage.output_tokens (completion cost, cumulative final value)
|
|
18
|
+
// message_stop → no usage data
|
|
19
|
+
// onChunk returns true only when usage state changes, so _wrapStream fires
|
|
20
|
+
// stream_pending exactly twice: once after message_start, once after message_delta.
|
|
21
|
+
function createStreamAccumulator() {
|
|
22
|
+
let input_tokens = null;
|
|
23
|
+
let output_tokens = null;
|
|
24
|
+
let cache_creation_input_tokens = null;
|
|
25
|
+
let cache_read_input_tokens = null;
|
|
26
|
+
let model = null;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
onChunk(chunk) {
|
|
30
|
+
if (chunk.type === 'message_start') {
|
|
31
|
+
const usage = chunk.message?.usage;
|
|
32
|
+
model = chunk.message?.model ?? null;
|
|
33
|
+
input_tokens = usage?.input_tokens ?? null;
|
|
34
|
+
cache_creation_input_tokens = usage?.cache_creation_input_tokens ?? null;
|
|
35
|
+
cache_read_input_tokens = usage?.cache_read_input_tokens ?? null;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (chunk.type === 'message_delta') {
|
|
39
|
+
output_tokens = chunk.usage?.output_tokens ?? null;
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
},
|
|
44
|
+
finalize() {
|
|
45
|
+
return {
|
|
46
|
+
model,
|
|
47
|
+
input_tokens,
|
|
48
|
+
output_tokens,
|
|
49
|
+
cache_creation_input_tokens,
|
|
50
|
+
cache_read_input_tokens,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Ready-to-use method config for client.messages.create.
|
|
57
|
+
const messagesCreate = {
|
|
58
|
+
call: 'messages.create',
|
|
59
|
+
middleware: middleware,
|
|
60
|
+
streamMiddleware: createStreamAccumulator,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
module.exports = { messagesCreate, middleware, createStreamAccumulator };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('assert/strict');
|
|
5
|
+
|
|
6
|
+
// Reset module cache between tests so _config doesn't leak
|
|
7
|
+
function freshSDK() {
|
|
8
|
+
delete require.cache[require.resolve('../src/instrument')];
|
|
9
|
+
delete require.cache[require.resolve('../src/index')];
|
|
10
|
+
process.env.WEFLAYR_INTAKE_URL = 'http://localhost:19999'; // nothing listening — fire-and-forget is fine
|
|
11
|
+
process.env.WEFLAYR_CLIENT_ID = 'test-client-id';
|
|
12
|
+
process.env.WEFLAYR_CLIENT_SECRET = 'test-secret';
|
|
13
|
+
return require('../src/index');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Minimal fake client: one async method at a nested path
|
|
17
|
+
function fakeClient() {
|
|
18
|
+
return {
|
|
19
|
+
chat: {
|
|
20
|
+
completions: {
|
|
21
|
+
create: async (args) => ({ id: 'res-1', model: args.model, choices: [] }),
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Missing options don't crash
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
test('missing ignore_fields does not crash', async () => {
|
|
32
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
33
|
+
|
|
34
|
+
weflayr_setup({
|
|
35
|
+
event_mode: 'default',
|
|
36
|
+
content_policy: 'ignore',
|
|
37
|
+
// ignore_fields intentionally omitted
|
|
38
|
+
methods: [{ call: 'chat.completions.create' }],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const client = weflayr_instrument(fakeClient());
|
|
42
|
+
const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
|
|
43
|
+
|
|
44
|
+
assert.equal(result.id, 'res-1');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('missing allow_fields does not crash', async () => {
|
|
48
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
49
|
+
|
|
50
|
+
weflayr_setup({
|
|
51
|
+
event_mode: 'default',
|
|
52
|
+
content_policy: 'allow',
|
|
53
|
+
// allow_fields intentionally omitted
|
|
54
|
+
methods: [{ call: 'chat.completions.create' }],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const client = weflayr_instrument(fakeClient());
|
|
58
|
+
const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
|
|
59
|
+
|
|
60
|
+
assert.equal(result.id, 'res-1');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('missing methods does not crash on object instrumentation', async () => {
|
|
64
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
65
|
+
|
|
66
|
+
weflayr_setup({
|
|
67
|
+
event_mode: 'default',
|
|
68
|
+
content_policy: 'ignore',
|
|
69
|
+
// methods intentionally omitted
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const client = weflayr_instrument(fakeClient());
|
|
73
|
+
const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
|
|
74
|
+
|
|
75
|
+
assert.equal(result.id, 'res-1');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('missing methods does not crash on function instrumentation', async () => {
|
|
79
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
80
|
+
|
|
81
|
+
weflayr_setup({
|
|
82
|
+
event_mode: 'default',
|
|
83
|
+
content_policy: 'ignore',
|
|
84
|
+
// methods intentionally omitted
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
async function myFn(x) { return x * 2; }
|
|
88
|
+
const instrumented = weflayr_instrument(myFn);
|
|
89
|
+
|
|
90
|
+
assert.equal(await instrumented(5), 10);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// enabled: false — target passes through unchanged, calls still work
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
test('enabled:false returns the original object untouched', () => {
|
|
98
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
99
|
+
|
|
100
|
+
weflayr_setup({ methods: [{ call: 'chat.completions.create' }] }, { enabled: false });
|
|
101
|
+
|
|
102
|
+
const client = fakeClient();
|
|
103
|
+
const result = weflayr_instrument(client);
|
|
104
|
+
|
|
105
|
+
assert.strictEqual(result, client);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('enabled:false returns the original function untouched', () => {
|
|
109
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
110
|
+
|
|
111
|
+
async function myFn(x) { return x * 2; }
|
|
112
|
+
weflayr_setup({ methods: [{ call: 'myFn' }] }, { enabled: false });
|
|
113
|
+
|
|
114
|
+
assert.strictEqual(weflayr_instrument(myFn), myFn);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('enabled:false — original function still executes correctly', async () => {
|
|
118
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
119
|
+
|
|
120
|
+
weflayr_setup({ methods: [{ call: 'chat.completions.create' }] }, { enabled: false });
|
|
121
|
+
|
|
122
|
+
const client = weflayr_instrument(fakeClient());
|
|
123
|
+
const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
|
|
124
|
+
|
|
125
|
+
assert.equal(result.id, 'res-1');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Invalid content_policy falls back to allow — function still works correctly
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
test('invalid content_policy does not crash', async () => {
|
|
133
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
134
|
+
|
|
135
|
+
weflayr_setup({
|
|
136
|
+
event_mode: 'default',
|
|
137
|
+
content_policy: 'not_a_real_policy',
|
|
138
|
+
allow_fields: [],
|
|
139
|
+
methods: [{ call: 'chat.completions.create' }],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const client = weflayr_instrument(fakeClient());
|
|
143
|
+
const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
|
|
144
|
+
|
|
145
|
+
assert.equal(result.id, 'res-1');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('invalid content_policy does not apply ignore_fields', async () => {
|
|
149
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
150
|
+
|
|
151
|
+
let capturedArgs;
|
|
152
|
+
const client = {
|
|
153
|
+
chat: {
|
|
154
|
+
completions: {
|
|
155
|
+
create: async (args) => { capturedArgs = args; return { id: 'res-1' }; },
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
weflayr_setup({
|
|
161
|
+
event_mode: 'default',
|
|
162
|
+
content_policy: 'not_a_real_policy',
|
|
163
|
+
ignore_fields: ['messages'],
|
|
164
|
+
allow_fields: [],
|
|
165
|
+
methods: [{ call: 'chat.completions.create' }],
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const instrumented = weflayr_instrument(client);
|
|
169
|
+
await instrumented.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'hello' }] });
|
|
170
|
+
|
|
171
|
+
// ignore_fields must NOT have been applied — the real call receives the original args
|
|
172
|
+
assert.ok(Array.isArray(capturedArgs.messages));
|
|
173
|
+
assert.equal(capturedArgs.messages[0].content, 'hello');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('undefined content_policy falls back to allow (pass-through)', async () => {
|
|
177
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
178
|
+
|
|
179
|
+
weflayr_setup({
|
|
180
|
+
event_mode: 'default',
|
|
181
|
+
// content_policy intentionally omitted
|
|
182
|
+
ignore_fields: ['messages'],
|
|
183
|
+
allow_fields: [],
|
|
184
|
+
methods: [{ call: 'chat.completions.create' }],
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const client = weflayr_instrument(fakeClient());
|
|
188
|
+
const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
|
|
189
|
+
|
|
190
|
+
assert.equal(result.id, 'res-1');
|
|
191
|
+
});
|