weflayr 0.2.0 → 0.3.1
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 +171 -38
- package/index.d.ts +97 -0
- package/package.json +4 -5
- package/src/index.js +21 -15
- package/src/instrument.js +97 -97
- package/src/providers/anthropic-ai-sdk.js +63 -0
- package/tests/index.test.js +188 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Weflayr Node.js SDK
|
|
2
2
|
|
|
3
|
-
Observability for Node.js - instrument any client or
|
|
3
|
+
Observability for Node.js - instrument any LLM client or function with one line.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,33 +8,23 @@ Observability for Node.js - instrument any client or method with one line.
|
|
|
8
8
|
npm install weflayr
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
```bash
|
|
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
|
|
11
|
+
## Quick start
|
|
20
12
|
|
|
21
13
|
```js
|
|
22
14
|
const { weflayr_setup, weflayr_instrument } = require('weflayr');
|
|
23
15
|
|
|
24
16
|
weflayr_setup({
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
allow_fields: [],
|
|
17
|
+
intake_url: 'https://api.weflayr.com',
|
|
18
|
+
client_id: 'your-client-id-uuid',
|
|
19
|
+
client_secret: 'your-client-secret',
|
|
29
20
|
methods: [
|
|
30
|
-
{ call: 'chat.completions.create' }
|
|
21
|
+
{ call: 'chat.completions.create' },
|
|
31
22
|
],
|
|
32
|
-
}
|
|
23
|
+
});
|
|
33
24
|
|
|
34
25
|
const OpenAI = require('openai');
|
|
35
26
|
const client = weflayr_instrument(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }));
|
|
36
27
|
|
|
37
|
-
// Tags are stripped from args before the API call and attached to the event
|
|
38
28
|
const response = await client.chat.completions.create({
|
|
39
29
|
model: 'gpt-4o-mini',
|
|
40
30
|
messages: [{ role: 'user', content: 'Hello' }],
|
|
@@ -42,6 +32,108 @@ const response = await client.chat.completions.create({
|
|
|
42
32
|
});
|
|
43
33
|
```
|
|
44
34
|
|
|
35
|
+
Credentials are typically read from environment variables via `dotenv`:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
weflayr_setup({
|
|
39
|
+
intake_url: process.env.WEFLAYR_INTAKE_URL,
|
|
40
|
+
client_id: process.env.WEFLAYR_CLIENT_ID,
|
|
41
|
+
client_secret: process.env.WEFLAYR_CLIENT_SECRET,
|
|
42
|
+
methods: [...],
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# .env
|
|
48
|
+
WEFLAYR_INTAKE_URL=https://api.weflayr.com
|
|
49
|
+
WEFLAYR_CLIENT_ID=your-client-id-uuid
|
|
50
|
+
WEFLAYR_CLIENT_SECRET=your-client-secret
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Settings reference
|
|
56
|
+
|
|
57
|
+
| Field | Type | Required | Description |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| `intake_url` | `string` | ✓ | Base URL of the Weflayr intake API |
|
|
60
|
+
| `client_id` | `string` | ✓ | UUID identifying your Flare credential pair |
|
|
61
|
+
| `client_secret` | `string` | ✓ | Bearer token used to authenticate events |
|
|
62
|
+
| `event_mode` | `'default'` \| `'light'` | | `light` skips `before` events. Default: `'default'` |
|
|
63
|
+
| `enabled` | `boolean` | | Set to `false` to disable instrumentation entirely. Default: `true` |
|
|
64
|
+
| `ignore_fields` | `function` | | Middleware to strip sensitive fields from event payloads. Mutually exclusive with `allow_fields`. |
|
|
65
|
+
| `allow_fields` | `function` | | Middleware to keep only approved fields in event payloads. Mutually exclusive with `ignore_fields`. |
|
|
66
|
+
| `methods` | `MethodConfig[]` | | Methods to instrument on the proxied object |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Filtering event payloads
|
|
71
|
+
|
|
72
|
+
`ignore_fields` and `allow_fields` are **middleware functions**, not field lists. Each receives a deep clone of the event payload (request args or response body) and returns the filtered version. The original args forwarded to the real provider call are never affected.
|
|
73
|
+
|
|
74
|
+
Only one may be set. Setting both logs a warning and blocks all events.
|
|
75
|
+
|
|
76
|
+
### ignore_fields strip specific fields
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
weflayr_setup({
|
|
80
|
+
// ...
|
|
81
|
+
ignore_fields: (data) => {
|
|
82
|
+
(data.messages ?? []).forEach(m => delete m.content);
|
|
83
|
+
(data.choices ?? []).forEach(c => { if (c.message) delete c.message.content; });
|
|
84
|
+
return data;
|
|
85
|
+
},
|
|
86
|
+
methods: [{ call: 'chat.completions.create' }],
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### allow_fields keep only specific fields
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
weflayr_setup({
|
|
94
|
+
// ...
|
|
95
|
+
allow_fields: (data) => ({
|
|
96
|
+
model: data.model,
|
|
97
|
+
usage: data.usage,
|
|
98
|
+
}),
|
|
99
|
+
methods: [{ call: 'chat.completions.create' }],
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Metadata tags
|
|
106
|
+
|
|
107
|
+
Pass `__weflayr_tags` on any instrumented call to attach arbitrary key-value metadata to the event. Tags are stripped before the real provider call is made.
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
await client.chat.completions.create({
|
|
111
|
+
model: 'gpt-4o-mini',
|
|
112
|
+
messages: [...],
|
|
113
|
+
__weflayr_tags: { feature: 'summarise', customer_id: '42' },
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Middleware
|
|
120
|
+
|
|
121
|
+
Middleware extracts structured fields that are merged into the event payload. `response` is `null` for `before` events.
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
methods: [
|
|
125
|
+
{
|
|
126
|
+
call: 'audio.speech.create',
|
|
127
|
+
middleware: (args, response) => ({
|
|
128
|
+
char_count: args?.input?.length ?? 0,
|
|
129
|
+
result_count: response?.data?.length ?? 0,
|
|
130
|
+
}),
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
45
137
|
## Instrumenting a plain function
|
|
46
138
|
|
|
47
139
|
```js
|
|
@@ -51,42 +143,83 @@ async function fetchModels() {
|
|
|
51
143
|
}
|
|
52
144
|
|
|
53
145
|
weflayr_setup({
|
|
54
|
-
|
|
55
|
-
content_policy: 'ignore',
|
|
56
|
-
ignore_fields: ['data'],
|
|
146
|
+
// ...
|
|
57
147
|
methods: [
|
|
58
148
|
{
|
|
59
149
|
call: 'fetchModels',
|
|
60
150
|
middleware: (_args, response) => ({ count: response?.data?.length ?? 0 }),
|
|
61
151
|
},
|
|
62
152
|
],
|
|
63
|
-
}
|
|
153
|
+
});
|
|
64
154
|
|
|
65
155
|
const instrumented = weflayr_instrument(fetchModels);
|
|
66
156
|
const models = await instrumented({ __weflayr_tags: { app: 'my-app' } });
|
|
67
157
|
```
|
|
68
158
|
|
|
69
|
-
|
|
159
|
+
---
|
|
70
160
|
|
|
71
|
-
|
|
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 |
|
|
161
|
+
## Disabling instrumentation
|
|
78
162
|
|
|
79
|
-
|
|
163
|
+
```js
|
|
164
|
+
weflayr_setup({
|
|
165
|
+
// ...
|
|
166
|
+
enabled: false,
|
|
167
|
+
});
|
|
168
|
+
```
|
|
80
169
|
|
|
81
|
-
|
|
170
|
+
`weflayr_instrument` returns the original object untouched when `enabled` is `false`.
|
|
82
171
|
|
|
83
|
-
|
|
172
|
+
---
|
|
84
173
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
174
|
+
## TypeScript
|
|
175
|
+
|
|
176
|
+
The SDK ships with full type declarations. No additional `@types` package is needed.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { weflayr_setup, weflayr_instrument, flayred } from 'weflayr';
|
|
180
|
+
import OpenAI from 'openai';
|
|
181
|
+
|
|
182
|
+
weflayr_setup({
|
|
183
|
+
intake_url: process.env.WEFLAYR_INTAKE_URL!,
|
|
184
|
+
client_id: process.env.WEFLAYR_CLIENT_ID!,
|
|
185
|
+
client_secret: process.env.WEFLAYR_CLIENT_SECRET!,
|
|
186
|
+
methods: [{ call: 'chat.completions.create' }],
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const client = weflayr_instrument(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }));
|
|
90
190
|
```
|
|
91
191
|
|
|
92
|
-
|
|
192
|
+
### Adding tags in TypeScript
|
|
193
|
+
|
|
194
|
+
Because `__weflayr_tags` is not part of the provider SDK's types, use the `flayred` helper to attach tags without casting:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const response = await client.chat.completions.create(flayred({
|
|
198
|
+
model: 'gpt-4o-mini',
|
|
199
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
200
|
+
__weflayr_tags: { feature: 'chat', customer_id: '42' },
|
|
201
|
+
}));
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`flayred<T>(options: T & { __weflayr_tags? }): T` TypeScript sees the return type as `T`, so it is fully compatible with the provider SDK's expected parameter type.
|
|
205
|
+
|
|
206
|
+
### Typing middleware
|
|
207
|
+
|
|
208
|
+
Cast inside the middleware body the SDK is provider-agnostic so it cannot infer the concrete response type:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import OpenAI from 'openai';
|
|
212
|
+
|
|
213
|
+
methods: [
|
|
214
|
+
{
|
|
215
|
+
call: 'chat.completions.create',
|
|
216
|
+
middleware: (_args, response) => {
|
|
217
|
+
const r = response as OpenAI.ChatCompletion | null;
|
|
218
|
+
return {
|
|
219
|
+
prompt_tokens: r?.usage?.prompt_tokens ?? 0,
|
|
220
|
+
completion_tokens: r?.usage?.completion_tokens ?? 0,
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
```
|
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 Flare 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,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weflayr",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
-
"test": "node --test tests/"
|
|
8
|
+
"test": "node --test tests/index.test.js"
|
|
8
9
|
},
|
|
9
10
|
"keywords": ["llm", "observability", "weflayr"],
|
|
10
11
|
"author": "Weflayr <contact@weflayr.com>",
|
|
@@ -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('
|
|
4
|
-
|
|
5
|
-
const { configure, weflayr_instrument } = require('./instrument');
|
|
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 };
|
|
35
|
+
module.exports = { weflayr_setup, weflayr_instrument, send_event, flayred };
|
package/src/instrument.js
CHANGED
|
@@ -7,14 +7,19 @@ 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) {
|
|
14
19
|
if (!_config) return target;
|
|
15
20
|
|
|
16
21
|
if (typeof target === 'function') {
|
|
17
|
-
const methodConfig = _config.settings.methods.find(m => m.call === target.name);
|
|
22
|
+
const methodConfig = (_config.settings.methods || []).find(m => m.call === target.name);
|
|
18
23
|
if (!methodConfig) return target;
|
|
19
24
|
return _wrapFn(target, target.name, methodConfig);
|
|
20
25
|
}
|
|
@@ -35,7 +40,7 @@ function _makeProxy(target, pathPrefix) {
|
|
|
35
40
|
const fullPath = pathPrefix ? `${pathPrefix}.${prop}` : prop;
|
|
36
41
|
|
|
37
42
|
if (typeof value === 'function') {
|
|
38
|
-
const methodConfig = _config.settings.methods.find(m => m.call === fullPath);
|
|
43
|
+
const methodConfig = (_config.settings.methods || []).find(m => m.call === fullPath);
|
|
39
44
|
if (methodConfig) {
|
|
40
45
|
return _wrapFn(value.bind(obj), fullPath, methodConfig);
|
|
41
46
|
}
|
|
@@ -57,16 +62,19 @@ function _wrapFn(fn, methodName, methodConfig) {
|
|
|
57
62
|
const startTime = Date.now();
|
|
58
63
|
const { settings } = _config;
|
|
59
64
|
const eventId = randomUUID();
|
|
65
|
+
// For multi-arg methods (e.g. convert(voiceId, request)), use the last arg as the
|
|
66
|
+
// request object so middleware and content policy see the options, not the id.
|
|
67
|
+
const requestArgs = cleanArgs[cleanArgs.length - 1];
|
|
60
68
|
|
|
61
69
|
if (settings.event_mode !== 'light') {
|
|
62
70
|
const beforeMiddlewareData = methodConfig.middleware
|
|
63
|
-
? methodConfig.middleware(
|
|
71
|
+
? methodConfig.middleware(requestArgs, null) || {}
|
|
64
72
|
: {};
|
|
65
73
|
_sendEvent('before', {
|
|
66
74
|
event_id: eventId,
|
|
67
75
|
method: methodName,
|
|
68
76
|
tags,
|
|
69
|
-
args: _applyContentPolicy(
|
|
77
|
+
args: _applyContentPolicy(requestArgs, settings),
|
|
70
78
|
...beforeMiddlewareData,
|
|
71
79
|
});
|
|
72
80
|
}
|
|
@@ -76,18 +84,18 @@ function _wrapFn(fn, methodName, methodConfig) {
|
|
|
76
84
|
|
|
77
85
|
if (_isAsyncIterable(result)) {
|
|
78
86
|
_sendEvent('stream_start', { event_id: eventId, method: methodName, tags });
|
|
79
|
-
return _wrapStream(result, methodName, tags,
|
|
87
|
+
return _wrapStream(result, methodName, tags, requestArgs, settings, startTime, methodConfig, eventId);
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
const middlewareData = methodConfig.middleware
|
|
83
|
-
? methodConfig.middleware(
|
|
91
|
+
? methodConfig.middleware(requestArgs, result) || {}
|
|
84
92
|
: {};
|
|
85
93
|
|
|
86
94
|
_sendEvent('after', {
|
|
87
95
|
event_id: eventId,
|
|
88
96
|
method: methodName,
|
|
89
97
|
tags,
|
|
90
|
-
args: _applyContentPolicy(
|
|
98
|
+
args: _applyContentPolicy(requestArgs, settings),
|
|
91
99
|
response: _applyContentPolicy(_toPlain(result), settings),
|
|
92
100
|
elapsed_ms: Date.now() - startTime,
|
|
93
101
|
...middlewareData,
|
|
@@ -95,13 +103,13 @@ function _wrapFn(fn, methodName, methodConfig) {
|
|
|
95
103
|
|
|
96
104
|
return result;
|
|
97
105
|
} catch (err) {
|
|
98
|
-
_sendEvent('after', {
|
|
106
|
+
await _sendEvent('after', {
|
|
99
107
|
event_id: eventId,
|
|
100
108
|
method: methodName,
|
|
101
109
|
tags,
|
|
102
|
-
args: _applyContentPolicy(
|
|
103
|
-
error: err.message,
|
|
104
|
-
status_code: err.status,
|
|
110
|
+
args: _applyContentPolicy(requestArgs, settings),
|
|
111
|
+
error: err.message ?? 'unknown',
|
|
112
|
+
status_code: err.status ?? err.statusCode ?? 'unknown',
|
|
105
113
|
elapsed_ms: Date.now() - startTime,
|
|
106
114
|
});
|
|
107
115
|
throw err;
|
|
@@ -111,16 +119,27 @@ function _wrapFn(fn, methodName, methodConfig) {
|
|
|
111
119
|
|
|
112
120
|
async function* _wrapStream(stream, methodName, tags, requestArgs, settings, startTime, methodConfig, eventId) {
|
|
113
121
|
let lastChunk = null;
|
|
122
|
+
const accumulator = methodConfig.streamMiddleware ? methodConfig.streamMiddleware() : null;
|
|
114
123
|
|
|
115
124
|
try {
|
|
116
125
|
for await (const chunk of stream) {
|
|
117
126
|
lastChunk = chunk;
|
|
127
|
+
if (accumulator && accumulator.onChunk(chunk)) {
|
|
128
|
+
_sendEvent('stream_pending', {
|
|
129
|
+
event_id: eventId,
|
|
130
|
+
method: methodName,
|
|
131
|
+
tags,
|
|
132
|
+
elapsed_ms: Date.now() - startTime,
|
|
133
|
+
...accumulator.finalize(),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
118
136
|
yield chunk;
|
|
119
137
|
}
|
|
120
138
|
|
|
121
|
-
const middlewareData =
|
|
122
|
-
?
|
|
123
|
-
: {}
|
|
139
|
+
const middlewareData = accumulator
|
|
140
|
+
? accumulator.finalize()
|
|
141
|
+
: (methodConfig.middleware && lastChunk ? methodConfig.middleware(requestArgs, lastChunk) || {}
|
|
142
|
+
: {});
|
|
124
143
|
|
|
125
144
|
_sendEvent('after', {
|
|
126
145
|
event_id: eventId,
|
|
@@ -132,12 +151,13 @@ async function* _wrapStream(stream, methodName, tags, requestArgs, settings, sta
|
|
|
132
151
|
...middlewareData,
|
|
133
152
|
});
|
|
134
153
|
} catch (err) {
|
|
135
|
-
_sendEvent('after', {
|
|
154
|
+
await _sendEvent('after', {
|
|
136
155
|
event_id: eventId,
|
|
137
156
|
method: methodName,
|
|
138
157
|
tags,
|
|
139
158
|
args: _applyContentPolicy(requestArgs, settings),
|
|
140
|
-
error: err.message,
|
|
159
|
+
error: err.message ?? 'unknown',
|
|
160
|
+
status_code: err.status ?? err.statusCode ?? 'unknown',
|
|
141
161
|
elapsed_ms: Date.now() - startTime,
|
|
142
162
|
});
|
|
143
163
|
throw err;
|
|
@@ -163,86 +183,54 @@ function _isAsyncIterable(value) {
|
|
|
163
183
|
|
|
164
184
|
function _toPlain(value) {
|
|
165
185
|
if (!value) return value;
|
|
186
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array || value instanceof ArrayBuffer) return null;
|
|
166
187
|
if (typeof value.toJSON === 'function') return value.toJSON();
|
|
167
188
|
return value;
|
|
168
189
|
}
|
|
169
190
|
|
|
170
191
|
function _applyContentPolicy(data, settings) {
|
|
171
192
|
if (!data) return data;
|
|
172
|
-
|
|
193
|
+
const fn = settings.ignore_fields || settings.allow_fields;
|
|
194
|
+
if (!fn) return data;
|
|
173
195
|
let clone;
|
|
174
196
|
try {
|
|
175
197
|
clone = JSON.parse(JSON.stringify(data));
|
|
176
198
|
} catch {
|
|
177
199
|
return data;
|
|
178
200
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
for (const field of (settings.ignore_fields || [])) {
|
|
185
|
-
_deleteField(clone, field);
|
|
201
|
+
try {
|
|
202
|
+
return fn(clone) ?? clone;
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.warn('[weflayr] content policy function error:', err.message);
|
|
205
|
+
return clone;
|
|
186
206
|
}
|
|
187
|
-
|
|
188
|
-
return clone;
|
|
189
207
|
}
|
|
190
208
|
|
|
191
|
-
|
|
192
|
-
if (!obj || typeof obj !== 'object' || allowFields.length === 0) return obj;
|
|
209
|
+
const _debug = process.env.WEFLAYR_DEBUG === 'true';
|
|
193
210
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const [, arrayKey, rest] = arrayMatch;
|
|
199
|
-
if (Array.isArray(obj[arrayKey])) {
|
|
200
|
-
result[arrayKey] = obj[arrayKey].map(item => _pickFields(item, [rest]));
|
|
201
|
-
}
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const dotIdx = field.indexOf('.');
|
|
206
|
-
if (dotIdx === -1) {
|
|
207
|
-
if (field in obj) result[field] = obj[field];
|
|
208
|
-
} else {
|
|
209
|
-
const key = field.slice(0, dotIdx);
|
|
210
|
-
const rest = field.slice(dotIdx + 1);
|
|
211
|
-
if (key in obj) result[key] = _pickFields(obj[key], [rest]);
|
|
212
|
-
}
|
|
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;
|
|
213
215
|
}
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function _deleteField(obj, path) {
|
|
218
|
-
if (!obj || typeof obj !== 'object') return;
|
|
216
|
+
const { intakeUrl, clientId, clientSecret } = _config;
|
|
217
|
+
const endpoint = `${intakeUrl.replace(/\/$/, '')}/${clientId}/`;
|
|
219
218
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
219
|
+
let body;
|
|
220
|
+
try {
|
|
221
|
+
body = JSON.stringify({
|
|
222
|
+
event_type: eventType,
|
|
223
|
+
timestamp: new Date().toISOString(),
|
|
224
|
+
...data,
|
|
225
|
+
});
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.warn(`[weflayr] could not serialize event "${eventType}":`, err.message);
|
|
226
228
|
return;
|
|
227
229
|
}
|
|
228
230
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
delete obj[path];
|
|
232
|
-
} else {
|
|
233
|
-
_deleteField(obj[path.slice(0, dotIdx)], path.slice(dotIdx + 1));
|
|
231
|
+
if (_debug) {
|
|
232
|
+
console.debug(`[weflayr] → ${eventType}`, JSON.parse(body));
|
|
234
233
|
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function _sendEvent(eventType, data) {
|
|
238
|
-
const { intakeUrl, clientId, clientSecret } = _config;
|
|
239
|
-
const endpoint = `${intakeUrl.replace(/\/$/, '')}/${clientId}/`;
|
|
240
|
-
|
|
241
|
-
const body = JSON.stringify({
|
|
242
|
-
event_type: eventType,
|
|
243
|
-
timestamp: new Date().toISOString(),
|
|
244
|
-
...data,
|
|
245
|
-
});
|
|
246
234
|
|
|
247
235
|
let parsed;
|
|
248
236
|
try {
|
|
@@ -252,30 +240,42 @@ function _sendEvent(eventType, data) {
|
|
|
252
240
|
}
|
|
253
241
|
|
|
254
242
|
const lib = parsed.protocol === 'https:' ? https : http;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
res
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
243
|
+
return new Promise((resolve) => {
|
|
244
|
+
const req = lib.request({
|
|
245
|
+
hostname: parsed.hostname,
|
|
246
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
247
|
+
path: parsed.pathname,
|
|
248
|
+
method: 'POST',
|
|
249
|
+
headers: {
|
|
250
|
+
'Content-Type': 'application/json',
|
|
251
|
+
'Content-Length': Buffer.byteLength(body),
|
|
252
|
+
'Authorization': `Bearer ${clientSecret}`,
|
|
253
|
+
},
|
|
254
|
+
}, res => {
|
|
255
|
+
res.resume();
|
|
256
|
+
if (_debug) {
|
|
257
|
+
console.debug(`[weflayr] ← ${eventType} status ${res.statusCode}`);
|
|
258
|
+
}
|
|
259
|
+
if (res.statusCode >= 400) {
|
|
260
|
+
console.warn(`[weflayr] intake returned ${res.statusCode} for event "${eventType}"`);
|
|
261
|
+
}
|
|
262
|
+
resolve();
|
|
263
|
+
});
|
|
271
264
|
|
|
272
|
-
|
|
273
|
-
|
|
265
|
+
req.on('error', (err) => {
|
|
266
|
+
console.warn(`[weflayr] send error for event "${eventType}":`, err.message);
|
|
267
|
+
resolve();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
req.write(body);
|
|
271
|
+
req.end();
|
|
274
272
|
});
|
|
273
|
+
}
|
|
275
274
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
275
|
+
// Send an arbitrary event to the intake. Requires configure() to have been called first.
|
|
276
|
+
function send_event(eventType, data) {
|
|
277
|
+
if (!_config) return;
|
|
278
|
+
return _sendEvent(eventType, data);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
module.exports = { configure, weflayr_instrument };
|
|
281
|
+
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,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('assert/strict');
|
|
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
|
+
|
|
12
|
+
// Reset module cache between tests so _config doesn't leak
|
|
13
|
+
function freshSDK() {
|
|
14
|
+
delete require.cache[require.resolve('../src/instrument')];
|
|
15
|
+
delete require.cache[require.resolve('../src/index')];
|
|
16
|
+
return require('../src/index');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Minimal fake client: one async method at a nested path
|
|
20
|
+
function fakeClient() {
|
|
21
|
+
return {
|
|
22
|
+
chat: {
|
|
23
|
+
completions: {
|
|
24
|
+
create: async (args) => ({ id: 'res-1', model: args.model, choices: [] }),
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Content policy — function-based middleware
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
test('ignore_fields function filters event data', async () => {
|
|
35
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
36
|
+
|
|
37
|
+
weflayr_setup({
|
|
38
|
+
...TEST_CREDS,
|
|
39
|
+
event_mode: 'default',
|
|
40
|
+
ignore_fields: (data) => { delete data.messages; return data; },
|
|
41
|
+
methods: [{ call: 'chat.completions.create' }],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const client = weflayr_instrument(fakeClient());
|
|
45
|
+
const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
|
|
46
|
+
|
|
47
|
+
assert.equal(result.id, 'res-1');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('allow_fields function filters event data', async () => {
|
|
51
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
52
|
+
|
|
53
|
+
weflayr_setup({
|
|
54
|
+
...TEST_CREDS,
|
|
55
|
+
event_mode: 'default',
|
|
56
|
+
allow_fields: (data) => ({ model: data.model }),
|
|
57
|
+
methods: [{ call: 'chat.completions.create' }],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const client = weflayr_instrument(fakeClient());
|
|
61
|
+
const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
|
|
62
|
+
|
|
63
|
+
assert.equal(result.id, 'res-1');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('both ignore_fields and allow_fields set — no events sent, function still works', async () => {
|
|
67
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
68
|
+
|
|
69
|
+
weflayr_setup({
|
|
70
|
+
...TEST_CREDS,
|
|
71
|
+
event_mode: 'default',
|
|
72
|
+
ignore_fields: (data) => data,
|
|
73
|
+
allow_fields: (data) => data,
|
|
74
|
+
methods: [{ call: 'chat.completions.create' }],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const client = weflayr_instrument(fakeClient());
|
|
78
|
+
const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
|
|
79
|
+
|
|
80
|
+
assert.equal(result.id, 'res-1');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('no content policy — pass-through, function still works', async () => {
|
|
84
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
85
|
+
|
|
86
|
+
weflayr_setup({
|
|
87
|
+
...TEST_CREDS,
|
|
88
|
+
event_mode: 'default',
|
|
89
|
+
methods: [{ call: 'chat.completions.create' }],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const client = weflayr_instrument(fakeClient());
|
|
93
|
+
const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
|
|
94
|
+
|
|
95
|
+
assert.equal(result.id, 'res-1');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('ignore_fields does not mutate actual call args', async () => {
|
|
99
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
100
|
+
|
|
101
|
+
let capturedArgs;
|
|
102
|
+
const client = {
|
|
103
|
+
chat: {
|
|
104
|
+
completions: {
|
|
105
|
+
create: async (args) => { capturedArgs = args; return { id: 'res-1' }; },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
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
|
+
});
|
|
116
|
+
|
|
117
|
+
const instrumented = weflayr_instrument(client);
|
|
118
|
+
await instrumented.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'hello' }] });
|
|
119
|
+
|
|
120
|
+
assert.ok(Array.isArray(capturedArgs.messages));
|
|
121
|
+
assert.equal(capturedArgs.messages[0].content, 'hello');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('missing methods does not crash on object instrumentation', async () => {
|
|
125
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
126
|
+
|
|
127
|
+
weflayr_setup({
|
|
128
|
+
...TEST_CREDS,
|
|
129
|
+
event_mode: 'default',
|
|
130
|
+
// methods intentionally omitted
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const client = weflayr_instrument(fakeClient());
|
|
134
|
+
const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
|
|
135
|
+
|
|
136
|
+
assert.equal(result.id, 'res-1');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('missing methods does not crash on function instrumentation', async () => {
|
|
140
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
141
|
+
|
|
142
|
+
weflayr_setup({
|
|
143
|
+
...TEST_CREDS,
|
|
144
|
+
event_mode: 'default',
|
|
145
|
+
// methods intentionally omitted
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
async function myFn(x) { return x * 2; }
|
|
149
|
+
const instrumented = weflayr_instrument(myFn);
|
|
150
|
+
|
|
151
|
+
assert.equal(await instrumented(5), 10);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// enabled: false — target passes through unchanged, calls still work
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
test('enabled:false returns the original object untouched', () => {
|
|
159
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
160
|
+
|
|
161
|
+
weflayr_setup({ methods: [{ call: 'chat.completions.create' }] , enabled: false });
|
|
162
|
+
|
|
163
|
+
const client = fakeClient();
|
|
164
|
+
const result = weflayr_instrument(client);
|
|
165
|
+
|
|
166
|
+
assert.strictEqual(result, client);
|
|
167
|
+
});
|
|
168
|
+
|
|
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);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('enabled:false — original function still executes correctly', async () => {
|
|
179
|
+
const { weflayr_setup, weflayr_instrument } = freshSDK();
|
|
180
|
+
|
|
181
|
+
weflayr_setup({ methods: [{ call: 'chat.completions.create' }] , enabled: false });
|
|
182
|
+
|
|
183
|
+
const client = weflayr_instrument(fakeClient());
|
|
184
|
+
const result = await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [] });
|
|
185
|
+
|
|
186
|
+
assert.equal(result.id, 'res-1');
|
|
187
|
+
});
|
|
188
|
+
|