weflayr 0.14.4 → 0.15.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/index.d.ts CHANGED
@@ -99,6 +99,24 @@ export function send_event(
99
99
  data: Record<string, unknown>,
100
100
  ): Promise<void> | undefined;
101
101
 
102
+ /** Options for {@link weflayr_vercel_ai_middleware}. */
103
+ export interface WeflayrVercelAiMiddlewareOptions {
104
+ /** Static tags attached to every event emitted by this middleware instance. */
105
+ tags?: Tags;
106
+ }
107
+
108
+ /**
109
+ * Returns a Vercel AI SDK `LanguageModelV1Middleware` that emits Weflayr events.
110
+ * Requires `weflayr_setup` to have been called first.
111
+ *
112
+ * @example
113
+ * const model = wrapLanguageModel({
114
+ * model: openai('gpt-4o-mini'),
115
+ * middleware: weflayr_vercel_ai_middleware({ tags: { feature: 'chat' } }),
116
+ * });
117
+ */
118
+ export function weflayr_vercel_ai_middleware(options?: WeflayrVercelAiMiddlewareOptions): object;
119
+
102
120
  /**
103
121
  * Attaches weflayr metadata tags to any SDK call options object.
104
122
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weflayr",
3
- "version": "0.14.4",
3
+ "version": "0.15.1",
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/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { configure, weflayr_instrument, weflayr_propagate, send_event } = require('./instrument');
4
+ const { weflayr_vercel_ai_middleware } = require('./vercel-ai-middleware');
4
5
 
5
6
  /**
6
7
  *
@@ -32,4 +33,4 @@ function flayred(options) {
32
33
  return options;
33
34
  }
34
35
 
35
- module.exports = { weflayr_setup, weflayr_instrument, weflayr_propagate, send_event, flayred };
36
+ module.exports = { weflayr_setup, weflayr_instrument, weflayr_propagate, send_event, flayred, weflayr_vercel_ai_middleware };
package/src/instrument.js CHANGED
@@ -315,4 +315,10 @@ function send_event(eventType, data) {
315
315
  return _sendEvent(eventType, data);
316
316
  }
317
317
 
318
- module.exports = { configure, weflayr_instrument, weflayr_propagate, send_event };
318
+ // Returns merged default_tags + propagated async-local tags. Used by the Vercel AI middleware.
319
+ function get_context_tags() {
320
+ if (!_config) return {};
321
+ return { ...(_config.settings.default_tags || {}), ..._asyncStore.getStore() };
322
+ }
323
+
324
+ module.exports = { configure, weflayr_instrument, weflayr_propagate, send_event, get_context_tags };
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ const { randomUUID } = require('crypto');
4
+ const { send_event, get_context_tags } = require('./instrument');
5
+
6
+ /**
7
+ * Returns a Vercel AI SDK LanguageModelV1Middleware that emits Weflayr events.
8
+ * Requires weflayr_setup() to have been called first.
9
+ *
10
+ * @param {{ tags?: Record<string, string | number | boolean> }} [options]
11
+ * @returns {object} Vercel AI SDK middleware
12
+ */
13
+ function weflayr_vercel_ai_middleware(options) {
14
+ const callTags = options?.tags ?? {};
15
+ const modelId = options?.model ?? 'unknown';
16
+
17
+ return {
18
+ wrapGenerate: async ({ doGenerate }) => {
19
+ const eventId = randomUUID();
20
+ const t0 = Date.now();
21
+ const tags = { ...get_context_tags(), ...callTags };
22
+
23
+ send_event('before', {
24
+ event_id: eventId,
25
+ method: 'vercel.generate',
26
+ tags,
27
+ args: { model: modelId },
28
+ });
29
+
30
+ try {
31
+ const result = await doGenerate();
32
+ send_event('after', {
33
+ event_id: eventId,
34
+ method: 'vercel.generate',
35
+ tags,
36
+ args: { model: modelId },
37
+ response: { usage: result.usage, finish_reason: result.finishReason },
38
+ elapsed_ms: Date.now() - t0,
39
+ });
40
+ return result;
41
+ } catch (err) {
42
+ send_event('after', {
43
+ event_id: eventId,
44
+ method: 'vercel.generate',
45
+ tags,
46
+ args: { model: modelId },
47
+ error: err.message ?? 'unknown',
48
+ elapsed_ms: Date.now() - t0,
49
+ });
50
+ throw err;
51
+ }
52
+ },
53
+
54
+ wrapStream: async ({ doStream }) => {
55
+ const eventId = randomUUID();
56
+ const t0 = Date.now();
57
+ const tags = { ...get_context_tags(), ...callTags };
58
+
59
+ send_event('before', {
60
+ event_id: eventId,
61
+ method: 'vercel.stream',
62
+ tags,
63
+ args: { model: modelId },
64
+ });
65
+
66
+ try {
67
+ const { stream, ...rest } = await doStream();
68
+
69
+ send_event('stream_start', {
70
+ event_id: eventId,
71
+ method: 'vercel.stream',
72
+ tags,
73
+ });
74
+
75
+ let finishChunk = null;
76
+ let errorChunk = null;
77
+ const patched = new TransformStream({
78
+ transform(chunk, controller) {
79
+ if (chunk.type === 'finish') finishChunk = chunk;
80
+ if (chunk.type === 'error') errorChunk = chunk;
81
+ controller.enqueue(chunk);
82
+ },
83
+ flush() {
84
+ if (errorChunk) {
85
+ send_event('after', {
86
+ event_id: eventId,
87
+ method: 'vercel.stream',
88
+ tags,
89
+ args: { model: modelId },
90
+ error: String(errorChunk.error),
91
+ elapsed_ms: Date.now() - t0,
92
+ });
93
+ } else {
94
+ send_event('after', {
95
+ event_id: eventId,
96
+ method: 'vercel.stream',
97
+ tags,
98
+ args: { model: modelId },
99
+ response: finishChunk
100
+ ? { usage: finishChunk.usage, finish_reason: finishChunk.finishReason }
101
+ : null,
102
+ elapsed_ms: Date.now() - t0,
103
+ });
104
+ }
105
+ },
106
+ });
107
+
108
+ return { stream: stream.pipeThrough(patched), ...rest };
109
+ } catch (err) {
110
+ send_event('after', {
111
+ event_id: eventId,
112
+ method: 'vercel.stream',
113
+ tags,
114
+ args: { model: modelId },
115
+ error: err.message ?? 'unknown',
116
+ elapsed_ms: Date.now() - t0,
117
+ });
118
+ throw err;
119
+ }
120
+ },
121
+ };
122
+ }
123
+
124
+ module.exports = { weflayr_vercel_ai_middleware };
@@ -62,6 +62,72 @@
62
62
  if (request === '@ai-sdk/openai') {
63
63
  return { openai: (modelId) => ({ _provider: 'openai', modelId }) };
64
64
  }
65
+ if (request === '@ai-sdk/openai-compatible') {
66
+ function createOpenAICompatible({ name }) {
67
+ return function(modelId) {
68
+ return {
69
+ specificationVersion: 'v1',
70
+ provider: name,
71
+ modelId,
72
+ defaultObjectGenerationMode: undefined,
73
+ doGenerate: async () => ({
74
+ text: 'Hello!',
75
+ usage: { promptTokens: 10, completionTokens: 5 },
76
+ finishReason: 'stop',
77
+ rawCall: { rawPrompt: [], rawSettings: {} },
78
+ rawResponse: { headers: {} },
79
+ warnings: [],
80
+ }),
81
+ doStream: async () => ({
82
+ stream: new ReadableStream({
83
+ start(ctrl) {
84
+ ctrl.enqueue({ type: 'text-delta', textDelta: 'Hello!' });
85
+ ctrl.enqueue({ type: 'finish', finishReason: 'stop', usage: { promptTokens: 10, completionTokens: 5 } });
86
+ ctrl.close();
87
+ },
88
+ }),
89
+ rawCall: { rawPrompt: [], rawSettings: {} },
90
+ rawResponse: { headers: {} },
91
+ warnings: [],
92
+ }),
93
+ };
94
+ };
95
+ }
96
+ return { createOpenAICompatible };
97
+ }
98
+ if (request === 'ai') {
99
+ function wrapLanguageModel({ model, middleware }) {
100
+ return {
101
+ modelId: model.modelId,
102
+ doGenerate: async (params) =>
103
+ middleware.wrapGenerate({ doGenerate: () => model.doGenerate(params), params, model }),
104
+ doStream: async (params) =>
105
+ middleware.wrapStream({ doStream: () => model.doStream(params), params, model }),
106
+ };
107
+ }
108
+ async function generateText({ model, prompt }) {
109
+ const params = { prompt: [{ role: 'user', content: [{ type: 'text', text: prompt }] }] };
110
+ return model.doGenerate(params);
111
+ }
112
+ function streamText({ model, prompt }) {
113
+ const params = { prompt: [{ role: 'user', content: [{ type: 'text', text: prompt }] }] };
114
+ const streamPromise = model.doStream(params);
115
+ return {
116
+ get textStream() {
117
+ return (async function* () {
118
+ const { stream } = await streamPromise;
119
+ const reader = stream.getReader();
120
+ while (true) {
121
+ const { done, value } = await reader.read();
122
+ if (done) break;
123
+ if (value && value.type === 'text-delta') yield value.textDelta;
124
+ }
125
+ })();
126
+ },
127
+ };
128
+ }
129
+ return { wrapLanguageModel, generateText, streamText };
130
+ }
65
131
  if (request === 'weflayr') {
66
132
  return _origLoad.call(this, path.resolve(__dirname, '../src/index.js'), parent, isMain);
67
133
  }
@@ -81,6 +147,7 @@ const TEST_CREDS = {
81
147
  // Reset module cache between tests so _config doesn't leak
82
148
  function freshSDK() {
83
149
  delete require.cache[require.resolve('../src/instrument')];
150
+ delete require.cache[require.resolve('../src/vercel-ai-middleware')];
84
151
  delete require.cache[require.resolve('../src/index')];
85
152
  return require('../src/index');
86
153
  }
@@ -804,3 +871,232 @@ test('snippet mastra_stream: stream_start + after emitted, model/usage in after
804
871
  assert.equal(after.tags.customer_id, '42');
805
872
  });
806
873
 
874
+ // ---------------------------------------------------------------------------
875
+ // Vercel AI middleware — behavioural
876
+ // ---------------------------------------------------------------------------
877
+
878
+ test('weflayr_vercel_ai_middleware: wrapGenerate sends before + after with model and tags', async () => {
879
+ const { weflayr_setup, weflayr_vercel_ai_middleware } = freshSDK();
880
+
881
+ weflayr_setup({ ...TEST_CREDS });
882
+
883
+ const middleware = weflayr_vercel_ai_middleware({
884
+ model: 'openai/gpt-4o-mini',
885
+ tags: { feature: 'chat' },
886
+ });
887
+
888
+ const sent = await captureHttp(async () => {
889
+ await middleware.wrapGenerate({
890
+ doGenerate: async () => ({
891
+ text: 'Hello!',
892
+ usage: { promptTokens: 10, completionTokens: 5 },
893
+ finishReason: 'stop',
894
+ rawCall: {}, rawResponse: {}, warnings: [],
895
+ }),
896
+ params: {},
897
+ model: {},
898
+ });
899
+ });
900
+
901
+ assert.ok(sent.some(e => e.event_type === 'before'), 'before event sent');
902
+ assert.ok(sent.some(e => e.event_type === 'after'), 'after event sent');
903
+ const before = sent.find(e => e.event_type === 'before');
904
+ assert.equal(before.method, 'vercel.generate');
905
+ assert.equal(before.args.model, 'openai/gpt-4o-mini');
906
+ assert.equal(before.tags.feature, 'chat');
907
+ const after = sent.find(e => e.event_type === 'after');
908
+ assert.deepEqual(after.response.usage, { promptTokens: 10, completionTokens: 5 });
909
+ assert.equal(after.response.finish_reason, 'stop');
910
+ assert.ok(after.elapsed_ms >= 0);
911
+ });
912
+
913
+ test('weflayr_vercel_ai_middleware: wrapStream sends before + stream_start + after with usage', async () => {
914
+ const { weflayr_setup, weflayr_vercel_ai_middleware } = freshSDK();
915
+
916
+ weflayr_setup({ ...TEST_CREDS });
917
+
918
+ const middleware = weflayr_vercel_ai_middleware({
919
+ model: 'openai/gpt-4o-mini',
920
+ tags: { feature: 'chat' },
921
+ });
922
+
923
+ const sent = await captureHttp(async () => {
924
+ const { stream } = await middleware.wrapStream({
925
+ doStream: async () => ({
926
+ stream: new ReadableStream({
927
+ start(ctrl) {
928
+ ctrl.enqueue({ type: 'text-delta', textDelta: 'Hello!' });
929
+ ctrl.enqueue({ type: 'finish', finishReason: 'stop', usage: { promptTokens: 10, completionTokens: 5 } });
930
+ ctrl.close();
931
+ },
932
+ }),
933
+ rawCall: {}, rawResponse: {}, warnings: [],
934
+ }),
935
+ params: {},
936
+ model: {},
937
+ });
938
+
939
+ const reader = stream.getReader();
940
+ while (true) {
941
+ const { done } = await reader.read();
942
+ if (done) break;
943
+ }
944
+ });
945
+
946
+ const types = sent.map(e => e.event_type);
947
+ assert.ok(types.includes('before'), 'before event sent');
948
+ assert.ok(types.includes('stream_start'), 'stream_start event sent');
949
+ assert.ok(types.includes('after'), 'after event sent');
950
+ const after = sent.find(e => e.event_type === 'after');
951
+ assert.equal(after.method, 'vercel.stream');
952
+ assert.deepEqual(after.response.usage, { promptTokens: 10, completionTokens: 5 });
953
+ assert.equal(after.response.finish_reason, 'stop');
954
+ });
955
+
956
+ test('weflayr_vercel_ai_middleware: weflayr_propagate tags appear in events', async () => {
957
+ const { weflayr_setup, weflayr_vercel_ai_middleware, weflayr_propagate } = freshSDK();
958
+
959
+ weflayr_setup({ ...TEST_CREDS });
960
+ weflayr_propagate('customer_id', 'acme-corp');
961
+
962
+ const middleware = weflayr_vercel_ai_middleware({ model: 'openai/gpt-4o-mini' });
963
+
964
+ const sent = await captureHttp(async () => {
965
+ await middleware.wrapGenerate({
966
+ doGenerate: async () => ({ text: 'ok', usage: { promptTokens: 1, completionTokens: 1 }, finishReason: 'stop', rawCall: {}, rawResponse: {}, warnings: [] }),
967
+ params: {},
968
+ model: {},
969
+ });
970
+ });
971
+
972
+ const before = sent.find(e => e.event_type === 'before');
973
+ assert.equal(before.tags.customer_id, 'acme-corp');
974
+ const after = sent.find(e => e.event_type === 'after');
975
+ assert.equal(after.tags.customer_id, 'acme-corp');
976
+ });
977
+
978
+ // ---------------------------------------------------------------------------
979
+ // Vercel AI middleware — documentation snippets
980
+ // @ai-sdk/openai-compatible and ai are faked by Module._load at top of file.
981
+ // ---------------------------------------------------------------------------
982
+
983
+ test('snippet vercel_ai_setup: before+after via generateText, method and args correct', async () => {
984
+ freshSDK();
985
+ setSnippetEnv();
986
+
987
+ const sent = await captureHttp(async () => {
988
+ // SNIPPET_START: vercel_ai_setup
989
+ const { weflayr_setup, weflayr_vercel_ai_middleware } = require('weflayr');
990
+ const { wrapLanguageModel, generateText } = require('ai');
991
+ const { createOpenAICompatible } = require('@ai-sdk/openai-compatible');
992
+
993
+ weflayr_setup({
994
+ intake_url: process.env.WEFLAYR_INTAKE_URL,
995
+ client_id: process.env.WEFLAYR_CLIENT_ID,
996
+ client_secret: process.env.WEFLAYR_CLIENT_SECRET,
997
+ });
998
+
999
+ const provider = createOpenAICompatible({
1000
+ name: 'openai',
1001
+ baseURL: 'https://api.openai.com/v1',
1002
+ headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
1003
+ });
1004
+
1005
+ const model = wrapLanguageModel({
1006
+ model: provider('gpt-4o-mini'),
1007
+ middleware: weflayr_vercel_ai_middleware({
1008
+ model: 'openai/gpt-4o-mini',
1009
+ tags: { env: 'production' },
1010
+ }),
1011
+ });
1012
+
1013
+ await generateText({ model, prompt: 'Hello!' });
1014
+ // SNIPPET_END: vercel_ai_setup
1015
+ });
1016
+
1017
+ assert.ok(sent.some(e => e.event_type === 'before'), 'before event sent');
1018
+ assert.ok(sent.some(e => e.event_type === 'after'), 'after event sent');
1019
+ const before = sent.find(e => e.event_type === 'before');
1020
+ assert.equal(before.method, 'vercel.generate');
1021
+ assert.equal(before.args.model, 'openai/gpt-4o-mini');
1022
+ assert.equal(before.tags.env, 'production');
1023
+ const after = sent.find(e => e.event_type === 'after');
1024
+ assert.ok(after.response.usage, 'usage in after event');
1025
+ assert.ok(after.elapsed_ms >= 0);
1026
+ });
1027
+
1028
+ test('snippet vercel_ai_propagate: propagated customer_id in before+after events', async () => {
1029
+ const { weflayr_setup, weflayr_vercel_ai_middleware } = freshSDK();
1030
+ setSnippetEnv();
1031
+
1032
+ const { wrapLanguageModel, generateText } = require('ai');
1033
+ const { createOpenAICompatible } = require('@ai-sdk/openai-compatible');
1034
+
1035
+ weflayr_setup({ ...TEST_CREDS });
1036
+
1037
+ const provider = createOpenAICompatible({ name: 'openai', baseURL: 'https://api.openai.com/v1', headers: {} });
1038
+ const model = wrapLanguageModel({
1039
+ model: provider('gpt-4o-mini'),
1040
+ middleware: weflayr_vercel_ai_middleware({ model: 'openai/gpt-4o-mini' }),
1041
+ });
1042
+
1043
+ const sent = await captureHttp(async () => {
1044
+ // SNIPPET_START: vercel_ai_propagate
1045
+ const { weflayr_propagate } = require('weflayr');
1046
+
1047
+ weflayr_propagate('customer_id', 'acme-corp');
1048
+ await generateText({ model, prompt: 'Hello!' });
1049
+ // SNIPPET_END: vercel_ai_propagate
1050
+ });
1051
+
1052
+ const before = sent.find(e => e.event_type === 'before');
1053
+ assert.ok(before, 'before event sent');
1054
+ assert.equal(before.tags.customer_id, 'acme-corp');
1055
+ const after = sent.find(e => e.event_type === 'after');
1056
+ assert.equal(after.tags.customer_id, 'acme-corp');
1057
+ });
1058
+
1059
+ test('snippet vercel_ai_stream: before + stream_start + after via streamText', async () => {
1060
+ freshSDK();
1061
+ setSnippetEnv();
1062
+
1063
+ const sent = await captureHttp(async () => {
1064
+ // SNIPPET_START: vercel_ai_stream
1065
+ const { weflayr_setup, weflayr_vercel_ai_middleware } = require('weflayr');
1066
+ const { wrapLanguageModel, streamText } = require('ai');
1067
+ const { createOpenAICompatible } = require('@ai-sdk/openai-compatible');
1068
+
1069
+ weflayr_setup({
1070
+ intake_url: process.env.WEFLAYR_INTAKE_URL,
1071
+ client_id: process.env.WEFLAYR_CLIENT_ID,
1072
+ client_secret: process.env.WEFLAYR_CLIENT_SECRET,
1073
+ });
1074
+
1075
+ const provider = createOpenAICompatible({
1076
+ name: 'openai',
1077
+ baseURL: 'https://api.openai.com/v1',
1078
+ headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
1079
+ });
1080
+
1081
+ const model = wrapLanguageModel({
1082
+ model: provider('gpt-4o-mini'),
1083
+ middleware: weflayr_vercel_ai_middleware({
1084
+ model: 'openai/gpt-4o-mini',
1085
+ tags: { feature: 'chat' },
1086
+ }),
1087
+ });
1088
+
1089
+ const { textStream } = streamText({ model, prompt: 'Hello!' });
1090
+ for await (const _chunk of textStream) {}
1091
+ // SNIPPET_END: vercel_ai_stream
1092
+ });
1093
+
1094
+ const types = sent.map(e => e.event_type);
1095
+ assert.ok(types.includes('before'), 'before event sent');
1096
+ assert.ok(types.includes('stream_start'), 'stream_start event sent');
1097
+ assert.ok(types.includes('after'), 'after event sent');
1098
+ const after = sent.find(e => e.event_type === 'after');
1099
+ assert.equal(after.method, 'vercel.stream');
1100
+ assert.equal(after.tags.feature, 'chat');
1101
+ assert.ok(after.response.usage, 'usage in after event');
1102
+ });