weflayr 0.3.0 → 0.3.2
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 +3 -82
- package/index.d.ts +97 -0
- package/package.json +3 -4
- package/src/index.js +20 -14
- package/src/instrument.js +16 -55
- package/tests/index.test.js +75 -78
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Weflayr Node.js SDK
|
|
2
2
|
|
|
3
|
-
Observability for Node.js
|
|
3
|
+
Observability for Node.js — instrument any LLM client or function with one line.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,85 +8,6 @@ Observability for Node.js - instrument any client or method with one line.
|
|
|
8
8
|
npm install weflayr
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Documentation
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
WEFLAYR_INTAKE_URL=https://api.weflayr.com
|
|
15
|
-
WEFLAYR_CLIENT_ID=your-client-id-uuid
|
|
16
|
-
WEFLAYR_CLIENT_SECRET=your-client-secret
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Usage
|
|
20
|
-
|
|
21
|
-
```js
|
|
22
|
-
const { weflayr_setup, weflayr_instrument } = require('weflayr');
|
|
23
|
-
|
|
24
|
-
weflayr_setup({
|
|
25
|
-
event_mode: 'default', // 'default' | 'light' (light skips before events)
|
|
26
|
-
content_policy: 'ignore', // 'ignore' removes ignore_fields | 'allow' keeps only allow_fields
|
|
27
|
-
ignore_fields: ['messages[].content', 'choices[].message.content'],
|
|
28
|
-
allow_fields: [],
|
|
29
|
-
methods: [
|
|
30
|
-
{ call: 'chat.completions.create' }
|
|
31
|
-
],
|
|
32
|
-
}, { enabled: true });
|
|
33
|
-
|
|
34
|
-
const OpenAI = require('openai');
|
|
35
|
-
const client = weflayr_instrument(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }));
|
|
36
|
-
|
|
37
|
-
// Tags are stripped from args before the API call and attached to the event
|
|
38
|
-
const response = await client.chat.completions.create({
|
|
39
|
-
model: 'gpt-4o-mini',
|
|
40
|
-
messages: [{ role: 'user', content: 'Hello' }],
|
|
41
|
-
__weflayr_tags: { feature: 'chat', customer_id: '42' },
|
|
42
|
-
});
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## Instrumenting a plain function
|
|
46
|
-
|
|
47
|
-
```js
|
|
48
|
-
async function fetchModels() {
|
|
49
|
-
const res = await fetch('https://api.example.com/models');
|
|
50
|
-
return res.json();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
weflayr_setup({
|
|
54
|
-
event_mode: 'default',
|
|
55
|
-
content_policy: 'ignore',
|
|
56
|
-
ignore_fields: ['data'],
|
|
57
|
-
methods: [
|
|
58
|
-
{
|
|
59
|
-
call: 'fetchModels',
|
|
60
|
-
middleware: (_args, response) => ({ count: response?.data?.length ?? 0 }),
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
}, { enabled: true });
|
|
64
|
-
|
|
65
|
-
const instrumented = weflayr_instrument(fetchModels);
|
|
66
|
-
const models = await instrumented({ __weflayr_tags: { app: 'my-app' } });
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Settings reference
|
|
70
|
-
|
|
71
|
-
| Field | Type | Description |
|
|
72
|
-
|---|---|---|
|
|
73
|
-
| `event_mode` | `'default'` \| `'light'` | `light` skips before events |
|
|
74
|
-
| `content_policy` | `'ignore'` \| `'allow'` | Which filtering mode to apply |
|
|
75
|
-
| `ignore_fields` | `string[]` | Fields removed from payloads (used when `content_policy: 'ignore'`) |
|
|
76
|
-
| `allow_fields` | `string[]` | Fields kept in payloads; everything else removed (used when `content_policy: 'allow'`) |
|
|
77
|
-
| `methods` | `{ call, middleware? }[]` | Whitelisted method paths to instrument |
|
|
78
|
-
|
|
79
|
-
Field paths support dot notation (`foo.bar`), and array traversal (`messages[].content`).
|
|
80
|
-
|
|
81
|
-
## Middleware
|
|
82
|
-
|
|
83
|
-
Middleware runs on both **before** and **after** events. `response` is `null` for before events.
|
|
84
|
-
|
|
85
|
-
```js
|
|
86
|
-
middleware: (args, response) => ({
|
|
87
|
-
char_count: args?.input?.length ?? 0,
|
|
88
|
-
result_count: response?.data?.length ?? 0,
|
|
89
|
-
})
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
The returned object is merged into the event payload.
|
|
13
|
+
Full documentation, examples, and configuration reference: **[docs.weflayr.com](https://docs.weflayr.com)**
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Arbitrary key-value metadata attached to an LLM call. */
|
|
2
|
+
export type Tags = Record<string, string | number | boolean>;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Receives a deep clone of the request args or response, mutates or replaces
|
|
6
|
+
* fields, and returns the filtered object sent to the intake API.
|
|
7
|
+
*/
|
|
8
|
+
export type ContentPolicyFn = (data: Record<string, unknown>) => Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extracts extra structured fields merged into the intake event.
|
|
12
|
+
* Called with `(args, null)` for the `before` event and `(args, result)` for `after`.
|
|
13
|
+
*/
|
|
14
|
+
export type MiddlewareFn = (
|
|
15
|
+
args: Record<string, unknown>,
|
|
16
|
+
response: Record<string, unknown> | null,
|
|
17
|
+
) => Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
/** Per-chunk accumulator returned by a `streamMiddleware` factory. */
|
|
20
|
+
export interface StreamAccumulator {
|
|
21
|
+
/** Return `true` to emit a `stream_pending` event immediately. */
|
|
22
|
+
onChunk: (chunk: unknown) => boolean;
|
|
23
|
+
/** Called once the stream ends; returns extra fields for the `after` event. */
|
|
24
|
+
finalize: () => Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Factory called once per stream invocation. */
|
|
28
|
+
export type StreamMiddlewareFn = () => StreamAccumulator;
|
|
29
|
+
|
|
30
|
+
/** Instrumentation configuration for a single method path. */
|
|
31
|
+
export interface MethodConfig {
|
|
32
|
+
/**
|
|
33
|
+
* Dot-separated path to the method on the instrumented object
|
|
34
|
+
* (e.g. `'chat.completions.create'`), or the function name for bare functions.
|
|
35
|
+
*/
|
|
36
|
+
call: string;
|
|
37
|
+
middleware?: MiddlewareFn;
|
|
38
|
+
streamMiddleware?: StreamMiddlewareFn;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Full configuration object passed to `weflayr_setup`. */
|
|
42
|
+
export interface WeflayrSettings {
|
|
43
|
+
/** Base URL of the Weflayr intake API (e.g. `'https://api.weflayr.com'`). */
|
|
44
|
+
intake_url: string;
|
|
45
|
+
/** UUID identifying the flayr credential pair. */
|
|
46
|
+
client_id: string;
|
|
47
|
+
/** Bearer token used when calling the intake API. */
|
|
48
|
+
client_secret: string;
|
|
49
|
+
/** `'default'` emits before + after events; `'light'` emits only after. */
|
|
50
|
+
event_mode?: 'default' | 'light';
|
|
51
|
+
/** Set to `false` to disable instrumentation entirely. Defaults to `true`. */
|
|
52
|
+
enabled?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Middleware to strip sensitive fields from event payloads before sending to the intake API.
|
|
55
|
+
* Mutually exclusive with `allow_fields` — setting both blocks all events.
|
|
56
|
+
*/
|
|
57
|
+
ignore_fields?: ContentPolicyFn;
|
|
58
|
+
/**
|
|
59
|
+
* Middleware to keep only approved fields in event payloads before sending to the intake API.
|
|
60
|
+
* Mutually exclusive with `ignore_fields` — setting both blocks all events.
|
|
61
|
+
*/
|
|
62
|
+
allow_fields?: ContentPolicyFn;
|
|
63
|
+
/** Methods to instrument on the proxied object. */
|
|
64
|
+
methods?: MethodConfig[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Configures weflayr with your credentials and instrumentation settings. */
|
|
68
|
+
export function weflayr_setup(settings: WeflayrSettings): void;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Wraps an LLM client object or async function so matching method calls emit
|
|
72
|
+
* events to the intake API. Returns the original value unchanged when weflayr
|
|
73
|
+
* is disabled or the target has no matching method config.
|
|
74
|
+
*/
|
|
75
|
+
export function weflayr_instrument<T>(target: T): T;
|
|
76
|
+
|
|
77
|
+
/** Sends a custom event to the intake API from user code. No-ops if `weflayr_setup` hasn't been called. */
|
|
78
|
+
export function send_event(
|
|
79
|
+
eventType: string,
|
|
80
|
+
data: Record<string, unknown>,
|
|
81
|
+
): Promise<void> | undefined;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Attaches weflayr metadata tags to any SDK call options object.
|
|
85
|
+
*
|
|
86
|
+
* `__weflayr_tags` is stripped by the instrumented proxy before the real provider call
|
|
87
|
+
* is made, so it never reaches the underlying SDK. At runtime this is the identity
|
|
88
|
+
* function — the generic signature is its only purpose.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* await openai.chat.completions.create(flayred({
|
|
92
|
+
* model: 'gpt-4o-mini',
|
|
93
|
+
* messages: [...],
|
|
94
|
+
* __weflayr_tags: { feature: 'haiku', customer_id: '42' },
|
|
95
|
+
* }));
|
|
96
|
+
*/
|
|
97
|
+
export function flayred<T>(options: T & { __weflayr_tags?: Record<string, string | number | boolean> }): T;
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weflayr",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Weflayr Node.js SDK — instrument any LLM client via JS Proxy",
|
|
5
5
|
"main": "src/index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
6
7
|
"scripts": {
|
|
7
8
|
"test": "node --test tests/index.test.js"
|
|
8
9
|
},
|
|
@@ -12,7 +13,5 @@
|
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": ">=18.0.0"
|
|
14
15
|
},
|
|
15
|
-
"dependencies": {
|
|
16
|
-
"dotenv": "^16.0.0"
|
|
17
|
-
}
|
|
16
|
+
"dependencies": {}
|
|
18
17
|
}
|
package/src/index.js
CHANGED
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
require('dotenv').config();
|
|
4
|
-
|
|
5
3
|
const { configure, weflayr_instrument, send_event } = require('./instrument');
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
|
-
*
|
|
6
|
+
*
|
|
9
7
|
* @param {*} settings: settings for weflayr
|
|
10
|
-
* @
|
|
11
|
-
* @returns
|
|
8
|
+
* @returns
|
|
12
9
|
*/
|
|
13
|
-
function weflayr_setup(settings
|
|
14
|
-
if (
|
|
10
|
+
function weflayr_setup(settings) {
|
|
11
|
+
if (settings.enabled === false) return;
|
|
15
12
|
|
|
16
|
-
const
|
|
17
|
-
const clientId = process.env.WEFLAYR_CLIENT_ID;
|
|
18
|
-
const clientSecret = process.env.WEFLAYR_CLIENT_SECRET;
|
|
13
|
+
const { intake_url, client_id, client_secret } = settings;
|
|
19
14
|
|
|
20
|
-
if (!
|
|
15
|
+
if (!intake_url || !client_id || !client_secret) {
|
|
21
16
|
throw new Error(
|
|
22
|
-
'Weflayr:
|
|
17
|
+
'Weflayr: intake_url, client_id and client_secret must be set in settings'
|
|
23
18
|
);
|
|
24
19
|
}
|
|
25
20
|
|
|
26
|
-
configure(
|
|
21
|
+
configure(intake_url, client_id, client_secret, settings);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Attaches weflayr metadata tags to any SDK call options object.
|
|
26
|
+
*
|
|
27
|
+
* @template T
|
|
28
|
+
* @param {T & { __weflayr_tags?: Record<string, string | number | boolean> }} options
|
|
29
|
+
* @returns {T}
|
|
30
|
+
*/
|
|
31
|
+
function flayred(options) {
|
|
32
|
+
return options;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
module.exports = { weflayr_setup, weflayr_instrument, send_event };
|
|
35
|
+
module.exports = { weflayr_setup, weflayr_instrument, send_event, flayred };
|
package/src/instrument.js
CHANGED
|
@@ -7,7 +7,12 @@ const { randomUUID } = require('crypto');
|
|
|
7
7
|
let _config = null;
|
|
8
8
|
|
|
9
9
|
function configure(intakeUrl, clientId, clientSecret, settings) {
|
|
10
|
-
|
|
10
|
+
if (settings.ignore_fields && settings.allow_fields) {
|
|
11
|
+
console.warn('[weflayr] both ignore_fields and allow_fields are set - no events will be sent to the intake API');
|
|
12
|
+
_config = { intakeUrl, clientId, clientSecret, settings, _blocked: true };
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
_config = { intakeUrl, clientId, clientSecret, settings, _blocked: false };
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
function weflayr_instrument(target) {
|
|
@@ -185,73 +190,29 @@ function _toPlain(value) {
|
|
|
185
190
|
|
|
186
191
|
function _applyContentPolicy(data, settings) {
|
|
187
192
|
if (!data) return data;
|
|
188
|
-
|
|
193
|
+
const fn = settings.ignore_fields || settings.allow_fields;
|
|
194
|
+
if (!fn) return data;
|
|
189
195
|
let clone;
|
|
190
196
|
try {
|
|
191
197
|
clone = JSON.parse(JSON.stringify(data));
|
|
192
198
|
} catch {
|
|
193
199
|
return data;
|
|
194
200
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
201
|
+
try {
|
|
202
|
+
return fn(clone) ?? clone;
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.warn('[weflayr] content policy function error:', err.message);
|
|
200
205
|
return clone;
|
|
201
206
|
}
|
|
202
|
-
|
|
203
|
-
return _pickFields(clone, settings.allow_fields || []);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function _pickFields(obj, allowFields) {
|
|
207
|
-
if (!obj || typeof obj !== 'object' || allowFields.length === 0) return obj;
|
|
208
|
-
|
|
209
|
-
const result = {};
|
|
210
|
-
for (const field of allowFields) {
|
|
211
|
-
const arrayMatch = field.match(/^(\w+)\[\]\.(.+)$/);
|
|
212
|
-
if (arrayMatch) {
|
|
213
|
-
const [, arrayKey, rest] = arrayMatch;
|
|
214
|
-
if (Array.isArray(obj[arrayKey])) {
|
|
215
|
-
result[arrayKey] = obj[arrayKey].map(item => _pickFields(item, [rest]));
|
|
216
|
-
}
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const dotIdx = field.indexOf('.');
|
|
221
|
-
if (dotIdx === -1) {
|
|
222
|
-
if (field in obj) result[field] = obj[field];
|
|
223
|
-
} else {
|
|
224
|
-
const key = field.slice(0, dotIdx);
|
|
225
|
-
const rest = field.slice(dotIdx + 1);
|
|
226
|
-
if (key in obj) result[key] = _pickFields(obj[key], [rest]);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return result;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function _deleteField(obj, path) {
|
|
233
|
-
if (!obj || typeof obj !== 'object') return;
|
|
234
|
-
|
|
235
|
-
const arrayMatch = path.match(/^(\w+)\[\]\.(.+)$/);
|
|
236
|
-
if (arrayMatch) {
|
|
237
|
-
const [, arrayKey, rest] = arrayMatch;
|
|
238
|
-
if (Array.isArray(obj[arrayKey])) {
|
|
239
|
-
obj[arrayKey].forEach(item => _deleteField(item, rest));
|
|
240
|
-
}
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const dotIdx = path.indexOf('.');
|
|
245
|
-
if (dotIdx === -1) {
|
|
246
|
-
delete obj[path];
|
|
247
|
-
} else {
|
|
248
|
-
_deleteField(obj[path.slice(0, dotIdx)], path.slice(dotIdx + 1));
|
|
249
|
-
}
|
|
250
207
|
}
|
|
251
208
|
|
|
252
209
|
const _debug = process.env.WEFLAYR_DEBUG === 'true';
|
|
253
210
|
|
|
254
211
|
function _sendEvent(eventType, data) {
|
|
212
|
+
if (_config._blocked) {
|
|
213
|
+
console.warn(`[weflayr] is blocked and can't send event, ensure your configuration is valid`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
255
216
|
const { intakeUrl, clientId, clientSecret } = _config;
|
|
256
217
|
const endpoint = `${intakeUrl.replace(/\/$/, '')}/${clientId}/`;
|
|
257
218
|
|
package/tests/index.test.js
CHANGED
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
const { test } = require('node:test');
|
|
4
4
|
const assert = require('assert/strict');
|
|
5
5
|
|
|
6
|
+
const TEST_CREDS = {
|
|
7
|
+
intake_url: 'http://localhost:19999', // nothing listening — fire-and-forget is fine
|
|
8
|
+
client_id: 'test-client-id',
|
|
9
|
+
client_secret: 'test-secret',
|
|
10
|
+
};
|
|
11
|
+
|
|
6
12
|
// Reset module cache between tests so _config doesn't leak
|
|
7
13
|
function freshSDK() {
|
|
8
14
|
delete require.cache[require.resolve('../src/instrument')];
|
|
9
15
|
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
16
|
return require('../src/index');
|
|
14
17
|
}
|
|
15
18
|
|
|
@@ -25,16 +28,16 @@ function fakeClient() {
|
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
// ---------------------------------------------------------------------------
|
|
28
|
-
//
|
|
31
|
+
// Content policy — function-based middleware
|
|
29
32
|
// ---------------------------------------------------------------------------
|
|
30
33
|
|
|
31
|
-
test('
|
|
34
|
+
test('ignore_fields function filters event data', async () => {
|
|
32
35
|
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
33
36
|
|
|
34
37
|
weflayr_setup({
|
|
38
|
+
...TEST_CREDS,
|
|
35
39
|
event_mode: 'default',
|
|
36
|
-
|
|
37
|
-
// ignore_fields intentionally omitted
|
|
40
|
+
ignore_fields: (data) => { delete data.messages; return data; },
|
|
38
41
|
methods: [{ call: 'chat.completions.create' }],
|
|
39
42
|
});
|
|
40
43
|
|
|
@@ -44,13 +47,13 @@ test('missing ignore_fields does not crash', async () => {
|
|
|
44
47
|
assert.equal(result.id, 'res-1');
|
|
45
48
|
});
|
|
46
49
|
|
|
47
|
-
test('
|
|
50
|
+
test('allow_fields function filters event data', async () => {
|
|
48
51
|
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
49
52
|
|
|
50
53
|
weflayr_setup({
|
|
54
|
+
...TEST_CREDS,
|
|
51
55
|
event_mode: 'default',
|
|
52
|
-
|
|
53
|
-
// allow_fields intentionally omitted
|
|
56
|
+
allow_fields: (data) => ({ model: data.model }),
|
|
54
57
|
methods: [{ call: 'chat.completions.create' }],
|
|
55
58
|
});
|
|
56
59
|
|
|
@@ -60,13 +63,15 @@ test('missing allow_fields does not crash', async () => {
|
|
|
60
63
|
assert.equal(result.id, 'res-1');
|
|
61
64
|
});
|
|
62
65
|
|
|
63
|
-
test('
|
|
66
|
+
test('both ignore_fields and allow_fields set — no events sent, function still works', async () => {
|
|
64
67
|
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
65
68
|
|
|
66
69
|
weflayr_setup({
|
|
70
|
+
...TEST_CREDS,
|
|
67
71
|
event_mode: 'default',
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
ignore_fields: (data) => data,
|
|
73
|
+
allow_fields: (data) => data,
|
|
74
|
+
methods: [{ call: 'chat.completions.create' }],
|
|
70
75
|
});
|
|
71
76
|
|
|
72
77
|
const client = weflayr_instrument(fakeClient());
|
|
@@ -75,49 +80,55 @@ test('missing methods does not crash on object instrumentation', async () => {
|
|
|
75
80
|
assert.equal(result.id, 'res-1');
|
|
76
81
|
});
|
|
77
82
|
|
|
78
|
-
test('
|
|
83
|
+
test('no content policy — pass-through, function still works', async () => {
|
|
79
84
|
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
80
85
|
|
|
81
86
|
weflayr_setup({
|
|
87
|
+
...TEST_CREDS,
|
|
82
88
|
event_mode: 'default',
|
|
83
|
-
|
|
84
|
-
// methods intentionally omitted
|
|
89
|
+
methods: [{ call: 'chat.completions.create' }],
|
|
85
90
|
});
|
|
86
91
|
|
|
87
|
-
|
|
88
|
-
const
|
|
92
|
+
const client = weflayr_instrument(fakeClient());
|
|
93
|
+
const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
|
|
89
94
|
|
|
90
|
-
assert.equal(
|
|
95
|
+
assert.equal(result.id, 'res-1');
|
|
91
96
|
});
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
// enabled: false — target passes through unchanged, calls still work
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
test('enabled:false returns the original object untouched', () => {
|
|
98
|
+
test('ignore_fields does not mutate actual call args', async () => {
|
|
98
99
|
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
101
|
+
let capturedArgs;
|
|
102
|
+
const client = {
|
|
103
|
+
chat: {
|
|
104
|
+
completions: {
|
|
105
|
+
create: async (args) => { capturedArgs = args; return { id: 'res-1' }; },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
107
109
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
weflayr_setup({
|
|
111
|
+
...TEST_CREDS,
|
|
112
|
+
event_mode: 'default',
|
|
113
|
+
ignore_fields: (data) => { delete data.messages; return data; },
|
|
114
|
+
methods: [{ call: 'chat.completions.create' }],
|
|
115
|
+
});
|
|
110
116
|
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
const instrumented = weflayr_instrument(client);
|
|
118
|
+
await instrumented.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'hello' }] });
|
|
113
119
|
|
|
114
|
-
assert.
|
|
120
|
+
assert.ok(Array.isArray(capturedArgs.messages));
|
|
121
|
+
assert.equal(capturedArgs.messages[0].content, 'hello');
|
|
115
122
|
});
|
|
116
123
|
|
|
117
|
-
test('
|
|
124
|
+
test('missing methods does not crash on object instrumentation', async () => {
|
|
118
125
|
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
119
126
|
|
|
120
|
-
weflayr_setup({
|
|
127
|
+
weflayr_setup({
|
|
128
|
+
...TEST_CREDS,
|
|
129
|
+
event_mode: 'default',
|
|
130
|
+
// methods intentionally omitted
|
|
131
|
+
});
|
|
121
132
|
|
|
122
133
|
const client = weflayr_instrument(fakeClient());
|
|
123
134
|
const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
|
|
@@ -125,67 +136,53 @@ test('enabled:false — original function still executes correctly', async () =>
|
|
|
125
136
|
assert.equal(result.id, 'res-1');
|
|
126
137
|
});
|
|
127
138
|
|
|
128
|
-
|
|
129
|
-
// Invalid content_policy falls back to allow — function still works correctly
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
|
|
132
|
-
test('invalid content_policy does not crash', async () => {
|
|
139
|
+
test('missing methods does not crash on function instrumentation', async () => {
|
|
133
140
|
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
134
141
|
|
|
135
142
|
weflayr_setup({
|
|
143
|
+
...TEST_CREDS,
|
|
136
144
|
event_mode: 'default',
|
|
137
|
-
|
|
138
|
-
allow_fields: [],
|
|
139
|
-
methods: [{ call: 'chat.completions.create' }],
|
|
145
|
+
// methods intentionally omitted
|
|
140
146
|
});
|
|
141
147
|
|
|
142
|
-
|
|
143
|
-
const
|
|
148
|
+
async function myFn(x) { return x * 2; }
|
|
149
|
+
const instrumented = weflayr_instrument(myFn);
|
|
144
150
|
|
|
145
|
-
assert.equal(
|
|
151
|
+
assert.equal(await instrumented(5), 10);
|
|
146
152
|
});
|
|
147
153
|
|
|
148
|
-
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// enabled: false — target passes through unchanged, calls still work
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
test('enabled:false returns the original object untouched', () => {
|
|
149
159
|
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
150
160
|
|
|
151
|
-
|
|
152
|
-
const client = {
|
|
153
|
-
chat: {
|
|
154
|
-
completions: {
|
|
155
|
-
create: async (args) => { capturedArgs = args; return { id: 'res-1' }; },
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
};
|
|
161
|
+
weflayr_setup({ methods: [{ call: 'chat.completions.create' }] , enabled: false });
|
|
159
162
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
content_policy: 'not_a_real_policy',
|
|
163
|
-
ignore_fields: ['messages'],
|
|
164
|
-
allow_fields: [],
|
|
165
|
-
methods: [{ call: 'chat.completions.create' }],
|
|
166
|
-
});
|
|
163
|
+
const client = fakeClient();
|
|
164
|
+
const result = weflayr_instrument(client);
|
|
167
165
|
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
assert.strictEqual(result, client);
|
|
167
|
+
});
|
|
170
168
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
169
|
+
test('enabled:false returns the original function untouched', () => {
|
|
170
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
171
|
+
|
|
172
|
+
async function myFn(x) { return x * 2; }
|
|
173
|
+
weflayr_setup({ methods: [{ call: 'myFn' }] , enabled: false });
|
|
174
|
+
|
|
175
|
+
assert.strictEqual(weflayr_instrument(myFn), myFn);
|
|
174
176
|
});
|
|
175
177
|
|
|
176
|
-
test('
|
|
178
|
+
test('enabled:false — original function still executes correctly', async () => {
|
|
177
179
|
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
178
180
|
|
|
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
|
-
});
|
|
181
|
+
weflayr_setup({ methods: [{ call: 'chat.completions.create' }] , enabled: false });
|
|
186
182
|
|
|
187
183
|
const client = weflayr_instrument(fakeClient());
|
|
188
184
|
const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
|
|
189
185
|
|
|
190
186
|
assert.equal(result.id, 'res-1');
|
|
191
187
|
});
|
|
188
|
+
|