weflayr 0.3.1 → 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Weflayr Node.js SDK
2
2
 
3
- Observability for Node.js - instrument any LLM client or function with one line.
3
+ Observability for Node.js instrument any LLM client or function with one line.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,218 +8,6 @@ Observability for Node.js - instrument any LLM client or function with one line.
8
8
  npm install weflayr
9
9
  ```
10
10
 
11
- ## Quick start
11
+ ## Documentation
12
12
 
13
- ```js
14
- const { weflayr_setup, weflayr_instrument } = require('weflayr');
15
-
16
- weflayr_setup({
17
- intake_url: 'https://api.weflayr.com',
18
- client_id: 'your-client-id-uuid',
19
- client_secret: 'your-client-secret',
20
- methods: [
21
- { call: 'chat.completions.create' },
22
- ],
23
- });
24
-
25
- const OpenAI = require('openai');
26
- const client = weflayr_instrument(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }));
27
-
28
- const response = await client.chat.completions.create({
29
- model: 'gpt-4o-mini',
30
- messages: [{ role: 'user', content: 'Hello' }],
31
- __weflayr_tags: { feature: 'chat', customer_id: '42' },
32
- });
33
- ```
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
-
137
- ## Instrumenting a plain function
138
-
139
- ```js
140
- async function fetchModels() {
141
- const res = await fetch('https://api.example.com/models');
142
- return res.json();
143
- }
144
-
145
- weflayr_setup({
146
- // ...
147
- methods: [
148
- {
149
- call: 'fetchModels',
150
- middleware: (_args, response) => ({ count: response?.data?.length ?? 0 }),
151
- },
152
- ],
153
- });
154
-
155
- const instrumented = weflayr_instrument(fetchModels);
156
- const models = await instrumented({ __weflayr_tags: { app: 'my-app' } });
157
- ```
158
-
159
- ---
160
-
161
- ## Disabling instrumentation
162
-
163
- ```js
164
- weflayr_setup({
165
- // ...
166
- enabled: false,
167
- });
168
- ```
169
-
170
- `weflayr_instrument` returns the original object untouched when `enabled` is `false`.
171
-
172
- ---
173
-
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 }));
190
- ```
191
-
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
- ```
13
+ Full documentation, examples, and configuration reference: **[docs.weflayr.com](https://docs.weflayr.com)**
package/index.d.ts CHANGED
@@ -36,13 +36,19 @@ export interface MethodConfig {
36
36
  call: string;
37
37
  middleware?: MiddlewareFn;
38
38
  streamMiddleware?: StreamMiddlewareFn;
39
+ /**
40
+ * Property name on the response object that holds the async iterable stream.
41
+ * Use when the SDK returns `{ [streamPath]: AsyncIterable }` instead of a direct AsyncIterable.
42
+ * e.g. `'stream'` for AWS Bedrock's `ConverseStreamCommand`.
43
+ */
44
+ streamPath?: string;
39
45
  }
40
46
 
41
47
  /** Full configuration object passed to `weflayr_setup`. */
42
48
  export interface WeflayrSettings {
43
49
  /** Base URL of the Weflayr intake API (e.g. `'https://api.weflayr.com'`). */
44
50
  intake_url: string;
45
- /** UUID identifying the Flare credential pair. */
51
+ /** UUID identifying the flayr credential pair. */
46
52
  client_id: string;
47
53
  /** Bearer token used when calling the intake API. */
48
54
  client_secret: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weflayr",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Weflayr Node.js SDK — instrument any LLM client via JS Proxy",
5
5
  "main": "src/index.js",
6
6
  "types": "index.d.ts",
package/src/instrument.js CHANGED
@@ -87,6 +87,12 @@ function _wrapFn(fn, methodName, methodConfig) {
87
87
  return _wrapStream(result, methodName, tags, requestArgs, settings, startTime, methodConfig, eventId);
88
88
  }
89
89
 
90
+ if (methodConfig.streamPath && result && _isAsyncIterable(result[methodConfig.streamPath])) {
91
+ _sendEvent('stream_start', { event_id: eventId, method: methodName, tags });
92
+ const wrappedStream = _wrapStream(result[methodConfig.streamPath], methodName, tags, requestArgs, settings, startTime, methodConfig, eventId);
93
+ return { ...result, [methodConfig.streamPath]: wrappedStream };
94
+ }
95
+
90
96
  const middlewareData = methodConfig.middleware
91
97
  ? methodConfig.middleware(requestArgs, result) || {}
92
98
  : {};
@@ -166,14 +172,23 @@ async function* _wrapStream(stream, methodName, tags, requestArgs, settings, sta
166
172
 
167
173
  function _extractTags(args) {
168
174
  let tags = {};
169
- const cleanArgs = args.map(arg => {
170
- if (arg && typeof arg === 'object' && !Array.isArray(arg) && '__weflayr_tags' in arg) {
171
- const { __weflayr_tags, ...rest } = arg;
172
- tags = __weflayr_tags || {};
173
- return rest;
175
+
176
+ function strip(obj) {
177
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return;
178
+ if ('__weflayr_tags' in obj) {
179
+ tags = obj.__weflayr_tags || {};
180
+ delete obj.__weflayr_tags;
174
181
  }
182
+ for (const key of Object.keys(obj)) {
183
+ strip(obj[key]);
184
+ }
185
+ }
186
+
187
+ const cleanArgs = args.map(arg => {
188
+ if (arg && typeof arg === 'object' && !Array.isArray(arg)) strip(arg);
175
189
  return arg;
176
190
  });
191
+
177
192
  return { tags, cleanArgs };
178
193
  }
179
194
 
@@ -0,0 +1,6 @@
1
+ import { MethodConfig, StreamAccumulator } from '../../../index';
2
+
3
+ export function createStreamAccumulator(): StreamAccumulator;
4
+
5
+ /** Ready-to-use method config for `client.send(new ConverseStreamCommand(...))`. */
6
+ export const converseStream: MethodConfig;
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ function createStreamAccumulator() {
4
+ let input_tokens = null;
5
+ let output_tokens = null;
6
+
7
+ return {
8
+ onChunk(chunk) {
9
+ if (chunk.metadata?.usage) {
10
+ input_tokens = chunk.metadata.usage.inputTokens ?? null;
11
+ output_tokens = chunk.metadata.usage.outputTokens ?? null;
12
+ return true;
13
+ }
14
+ return false;
15
+ },
16
+ finalize() {
17
+ return { input_tokens, output_tokens };
18
+ },
19
+ };
20
+ }
21
+
22
+ const converseStream = {
23
+ call: 'send',
24
+ streamPath: 'stream',
25
+ streamMiddleware: createStreamAccumulator,
26
+ };
27
+
28
+ module.exports = { converseStream, createStreamAccumulator };