@agentuity/runtime 1.0.24 → 1.0.26

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.
Files changed (40) hide show
  1. package/dist/dev-patches/aisdk.d.ts +17 -0
  2. package/dist/dev-patches/aisdk.d.ts.map +1 -0
  3. package/dist/dev-patches/aisdk.js +154 -0
  4. package/dist/dev-patches/aisdk.js.map +1 -0
  5. package/dist/dev-patches/gateway.d.ts +16 -0
  6. package/dist/dev-patches/gateway.d.ts.map +1 -0
  7. package/dist/dev-patches/gateway.js +55 -0
  8. package/dist/dev-patches/gateway.js.map +1 -0
  9. package/dist/dev-patches/index.d.ts +21 -0
  10. package/dist/dev-patches/index.d.ts.map +1 -0
  11. package/dist/dev-patches/index.js +33 -0
  12. package/dist/dev-patches/index.js.map +1 -0
  13. package/dist/dev-patches/otel-llm.d.ts +12 -0
  14. package/dist/dev-patches/otel-llm.d.ts.map +1 -0
  15. package/dist/dev-patches/otel-llm.js +345 -0
  16. package/dist/dev-patches/otel-llm.js.map +1 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/services/local/_db.d.ts.map +1 -1
  22. package/dist/services/local/_db.js +78 -1
  23. package/dist/services/local/_db.js.map +1 -1
  24. package/dist/services/local/email.d.ts +4 -1
  25. package/dist/services/local/email.d.ts.map +1 -1
  26. package/dist/services/local/email.js +9 -0
  27. package/dist/services/local/email.js.map +1 -1
  28. package/dist/services/local/task.d.ts +26 -1
  29. package/dist/services/local/task.d.ts.map +1 -1
  30. package/dist/services/local/task.js +304 -4
  31. package/dist/services/local/task.js.map +1 -1
  32. package/package.json +7 -7
  33. package/src/dev-patches/aisdk.ts +172 -0
  34. package/src/dev-patches/gateway.ts +70 -0
  35. package/src/dev-patches/index.ts +37 -0
  36. package/src/dev-patches/otel-llm.ts +408 -0
  37. package/src/index.ts +3 -0
  38. package/src/services/local/_db.ts +98 -5
  39. package/src/services/local/email.ts +14 -4
  40. package/src/services/local/task.ts +448 -4
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Runtime AI SDK patches for dev mode.
3
+ *
4
+ * Replaces the build-time patches from cli/src/cmd/build/patch/aisdk.ts.
5
+ * Monkey-patches Vercel AI SDK functions to:
6
+ * 1. Enable experimental telemetry on all AI function calls
7
+ * 2. Route AI SDK provider factory functions through the AI Gateway
8
+ */
9
+
10
+ /* eslint-disable @typescript-eslint/no-explicit-any */
11
+
12
+ function warnMissingKey(envKey: string): void {
13
+ const isDev =
14
+ process.env.AGENTUITY_ENVIRONMENT === 'development' || process.env.NODE_ENV !== 'production';
15
+ if (isDev) {
16
+ console.error('[ERROR] No credentials found for this AI provider. To fix this, either:');
17
+ console.error(
18
+ ' 1. Login to Agentuity Cloud (agentuity auth login) to use the AI Gateway (recommended)'
19
+ );
20
+ console.error(` 2. Set ${envKey} in your .env file to use the provider directly`);
21
+ } else {
22
+ console.error(`[ERROR] The environment variable ${envKey} is required. Either:`);
23
+ console.error(
24
+ ' 1. Use Agentuity Cloud AI Gateway by ensuring AGENTUITY_SDK_KEY is configured'
25
+ );
26
+ console.error(` 2. Set ${envKey} using "agentuity env set ${envKey}" and redeploy`);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Wrap an AI SDK function to inject experimental_telemetry: { isEnabled: true }.
32
+ */
33
+ function wrapWithTelemetry(originalFn: (...args: any[]) => any): (...args: any[]) => any {
34
+ return function (this: any, ...args: any[]) {
35
+ const opts = { ...(args[0] ?? {}) };
36
+ opts.experimental_telemetry = { isEnabled: true };
37
+ args[0] = opts;
38
+ return originalFn.apply(this, args);
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Wrap a provider factory function (e.g., createOpenAI) to inject gateway config.
44
+ */
45
+ function wrapProviderFactory(
46
+ originalFn: (...args: any[]) => any,
47
+ envKey: string,
48
+ provider: string
49
+ ): (...args: any[]) => any {
50
+ return function (this: any, ...args: any[]) {
51
+ const currentKey = process.env[envKey];
52
+ const sdkKey = process.env.AGENTUITY_SDK_KEY;
53
+
54
+ // If user provided their own key (and it's not the SDK key), leave it alone
55
+ if (currentKey && currentKey !== sdkKey) {
56
+ if (!sdkKey) {
57
+ console.log(
58
+ `User provided ${provider} api key set. Use the Agentuity AI Gateway more features.`
59
+ );
60
+ }
61
+ return originalFn.apply(this, args);
62
+ }
63
+
64
+ // Route through gateway
65
+ const transportUrl = process.env.AGENTUITY_TRANSPORT_URL;
66
+ if (transportUrl && sdkKey) {
67
+ const opts = { ...(args[0] ?? {}) };
68
+ if (!opts.baseURL) {
69
+ opts.apiKey = sdkKey;
70
+ opts.baseURL = `${transportUrl}/gateway/${provider}`;
71
+ args[0] = opts;
72
+ }
73
+ } else if (!currentKey) {
74
+ warnMissingKey(envKey);
75
+ }
76
+
77
+ return originalFn.apply(this, args);
78
+ };
79
+ }
80
+
81
+ /** AI SDK core functions to wrap with telemetry injection */
82
+ const AI_CORE_FUNCTIONS = [
83
+ 'generateText',
84
+ 'streamText',
85
+ 'generateObject',
86
+ 'streamObject',
87
+ 'embed',
88
+ 'embedMany',
89
+ ] as const;
90
+
91
+ /** AI SDK provider packages and their factory functions */
92
+ const AI_PROVIDER_CONFIGS = [
93
+ {
94
+ module: '@ai-sdk/openai',
95
+ factory: 'createOpenAI',
96
+ envKey: 'OPENAI_API_KEY',
97
+ provider: 'openai',
98
+ },
99
+ {
100
+ module: '@ai-sdk/anthropic',
101
+ factory: 'createAnthropic',
102
+ envKey: 'ANTHROPIC_API_KEY',
103
+ provider: 'anthropic',
104
+ },
105
+ {
106
+ module: '@ai-sdk/cohere',
107
+ factory: 'createCohere',
108
+ envKey: 'COHERE_API_KEY',
109
+ provider: 'cohere',
110
+ },
111
+ {
112
+ module: '@ai-sdk/deepseek',
113
+ factory: 'createDeepSeek',
114
+ envKey: 'DEEPSEEK_API_KEY',
115
+ provider: 'deepseek',
116
+ },
117
+ {
118
+ module: '@ai-sdk/google',
119
+ factory: 'createGoogleGenerativeAI',
120
+ envKey: 'GOOGLE_GENERATIVE_AI_API_KEY',
121
+ provider: 'google-ai-studio',
122
+ },
123
+ { module: '@ai-sdk/xai', factory: 'createXai', envKey: 'XAI_API_KEY', provider: 'grok' },
124
+ { module: '@ai-sdk/groq', factory: 'createGroq', envKey: 'GROQ_API_KEY', provider: 'groq' },
125
+ {
126
+ module: '@ai-sdk/mistral',
127
+ factory: 'createMistral',
128
+ envKey: 'MISTRAL_API_KEY',
129
+ provider: 'mistral',
130
+ },
131
+ {
132
+ module: '@ai-sdk/perplexity',
133
+ factory: 'createPerplexity',
134
+ envKey: 'PERPLEXITY_API_KEY',
135
+ provider: 'perplexity-ai',
136
+ },
137
+ ] as const;
138
+
139
+ /**
140
+ * Patch AI SDK core functions (generateText, streamText, etc.) with telemetry injection.
141
+ */
142
+ export async function applyAISDKCorePatches(): Promise<void> {
143
+ try {
144
+ const aiModule = await import('ai');
145
+
146
+ for (const fnName of AI_CORE_FUNCTIONS) {
147
+ const original = (aiModule as any)[fnName];
148
+ if (typeof original === 'function') {
149
+ (aiModule as any)[fnName] = wrapWithTelemetry(original);
150
+ }
151
+ }
152
+ } catch {
153
+ // 'ai' package not installed — skip
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Patch AI SDK provider factory functions to route through the AI Gateway.
159
+ */
160
+ export async function applyAISDKProviderPatches(): Promise<void> {
161
+ for (const config of AI_PROVIDER_CONFIGS) {
162
+ try {
163
+ const mod = await import(config.module);
164
+ const original = mod[config.factory];
165
+ if (typeof original === 'function') {
166
+ mod[config.factory] = wrapProviderFactory(original, config.envKey, config.provider);
167
+ }
168
+ } catch {
169
+ // Provider package not installed — skip
170
+ }
171
+ }
172
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Runtime LLM Gateway patches for dev mode.
3
+ *
4
+ * Replaces the build-time patches from cli/src/cmd/build/patch/llm.ts.
5
+ * Sets environment variables to route LLM SDK calls through the Agentuity AI Gateway
6
+ * when the user hasn't provided their own API keys.
7
+ */
8
+
9
+ interface GatewayConfig {
10
+ apiKeyEnv: string;
11
+ baseUrlEnv: string;
12
+ provider: string;
13
+ }
14
+
15
+ const GATEWAY_CONFIGS: GatewayConfig[] = [
16
+ { apiKeyEnv: 'ANTHROPIC_API_KEY', baseUrlEnv: 'ANTHROPIC_BASE_URL', provider: 'anthropic' },
17
+ { apiKeyEnv: 'GROQ_API_KEY', baseUrlEnv: 'GROQ_BASE_URL', provider: 'groq' },
18
+ { apiKeyEnv: 'OPENAI_API_KEY', baseUrlEnv: 'OPENAI_BASE_URL', provider: 'openai' },
19
+ ];
20
+
21
+ function warnMissingKey(envKey: string): void {
22
+ const isDev =
23
+ process.env.AGENTUITY_ENVIRONMENT === 'development' || process.env.NODE_ENV !== 'production';
24
+ if (isDev) {
25
+ console.error('[ERROR] No credentials found for this AI provider. To fix this, either:');
26
+ console.error(
27
+ ' 1. Login to Agentuity Cloud (agentuity auth login) to use the AI Gateway (recommended)'
28
+ );
29
+ console.error(` 2. Set ${envKey} in your .env file to use the provider directly`);
30
+ } else {
31
+ console.error(`[ERROR] The environment variable ${envKey} is required. Either:`);
32
+ console.error(
33
+ ' 1. Use Agentuity Cloud AI Gateway by ensuring AGENTUITY_SDK_KEY is configured'
34
+ );
35
+ console.error(` 2. Set ${envKey} using "agentuity env set ${envKey}" and redeploy`);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Set environment variables to route LLM calls through the AI Gateway.
41
+ *
42
+ * For each provider, if the user hasn't set their own API key (or it equals
43
+ * the SDK key), we redirect to the gateway. This matches the behavior of
44
+ * the build-time patches in patch/llm.ts.
45
+ */
46
+ export function applyGatewayPatches(): void {
47
+ const sdkKey = process.env.AGENTUITY_SDK_KEY;
48
+ const gatewayUrl =
49
+ process.env.AGENTUITY_AIGATEWAY_URL ||
50
+ process.env.AGENTUITY_TRANSPORT_URL ||
51
+ (sdkKey ? 'https://agentuity.ai' : '');
52
+
53
+ for (const config of GATEWAY_CONFIGS) {
54
+ const currentKey = process.env[config.apiKeyEnv];
55
+
56
+ // If the user provided their own key (and it's not the SDK key), leave it alone
57
+ if (currentKey && currentKey !== sdkKey) {
58
+ continue;
59
+ }
60
+
61
+ // Route through gateway if we have both URL and SDK key
62
+ if (gatewayUrl && sdkKey) {
63
+ process.env[config.apiKeyEnv] = sdkKey;
64
+ process.env[config.baseUrlEnv] = `${gatewayUrl}/gateway/${config.provider}`;
65
+ console.debug(`Enabled Agentuity AI Gateway for ${config.provider}`);
66
+ } else if (!currentKey) {
67
+ warnMissingKey(config.apiKeyEnv);
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Runtime dev patches — replaces build-time Bun.build patches for dev mode.
3
+ *
4
+ * When --experimental-no-bundle is used, the generated entry file (src/generated/app.ts)
5
+ * is run directly by Bun without bundling. These runtime patches apply the same
6
+ * monkey-patches that would normally be injected by the Bun.build plugin during bundling.
7
+ *
8
+ * Three categories of patches:
9
+ * 1. Gateway: Set env vars to route LLM calls through Agentuity AI Gateway
10
+ * 2. AI SDK: Wrap Vercel AI SDK functions with telemetry + gateway config
11
+ * 3. OTel LLM: Wrap LLM SDK .create() methods with OpenTelemetry spans
12
+ */
13
+
14
+ import { applyGatewayPatches } from './gateway';
15
+ import { applyAISDKCorePatches, applyAISDKProviderPatches } from './aisdk';
16
+ import { applyOtelLLMPatches } from './otel-llm';
17
+
18
+ /**
19
+ * Apply all runtime dev patches.
20
+ *
21
+ * Must be called:
22
+ * - AFTER bootstrapRuntimeEnv() (so env vars like AGENTUITY_SDK_KEY are loaded)
23
+ * - BEFORE any user code imports LLM SDKs
24
+ */
25
+ export async function applyDevPatches(): Promise<void> {
26
+ // 1. Set gateway env vars first (other patches may read them)
27
+ applyGatewayPatches();
28
+
29
+ // 2. Patch AI SDK core functions (telemetry injection)
30
+ await applyAISDKCorePatches();
31
+
32
+ // 3. Patch AI SDK provider factories (gateway routing)
33
+ await applyAISDKProviderPatches();
34
+
35
+ // 4. Patch LLM SDK prototypes with OTel spans
36
+ await applyOtelLLMPatches();
37
+ }
@@ -0,0 +1,408 @@
1
+ /**
2
+ * Runtime OpenTelemetry LLM instrumentation patches for dev mode.
3
+ *
4
+ * Replaces the build-time patches from cli/src/cmd/build/patch/otel-llm.ts.
5
+ * Wraps LLM SDK methods (OpenAI, Anthropic, Groq) with OTel spans to capture
6
+ * model, tokens, latency, and streaming support.
7
+ */
8
+
9
+ /* eslint-disable @typescript-eslint/no-explicit-any */
10
+
11
+ import { SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
12
+ import type { Span } from '@opentelemetry/api';
13
+
14
+ const ATTR_GEN_AI_SYSTEM = 'gen_ai.system';
15
+ const ATTR_GEN_AI_REQUEST_MODEL = 'gen_ai.request.model';
16
+ const ATTR_GEN_AI_REQUEST_MAX_TOKENS = 'gen_ai.request.max_tokens';
17
+ const ATTR_GEN_AI_REQUEST_TEMPERATURE = 'gen_ai.request.temperature';
18
+ const ATTR_GEN_AI_REQUEST_TOP_P = 'gen_ai.request.top_p';
19
+ const ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY = 'gen_ai.request.frequency_penalty';
20
+ const ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY = 'gen_ai.request.presence_penalty';
21
+ const ATTR_GEN_AI_RESPONSE_MODEL = 'gen_ai.response.model';
22
+ const ATTR_GEN_AI_RESPONSE_ID = 'gen_ai.response.id';
23
+ const ATTR_GEN_AI_RESPONSE_FINISH_REASONS = 'gen_ai.response.finish_reasons';
24
+ const ATTR_GEN_AI_USAGE_INPUT_TOKENS = 'gen_ai.usage.input_tokens';
25
+ const ATTR_GEN_AI_USAGE_OUTPUT_TOKENS = 'gen_ai.usage.output_tokens';
26
+ const ATTR_GEN_AI_OPERATION_NAME = 'gen_ai.operation.name';
27
+ const ATTR_GEN_AI_REQUEST_MESSAGES = 'gen_ai.request.messages';
28
+ const ATTR_GEN_AI_RESPONSE_TEXT = 'gen_ai.response.text';
29
+
30
+ const otelTracer = trace.getTracer('@agentuity/otel-llm', '1.0.0');
31
+
32
+ interface OtelPatchConfig {
33
+ provider: string;
34
+ inputTokensField: string;
35
+ outputTokensField: string;
36
+ responseIdField: string;
37
+ finishReasonExtractor: (response: any) => string | undefined;
38
+ responseContentExtractor: (response: any) => string | undefined;
39
+ requestMessagesField: string;
40
+ streamDeltaContentExtractor: (chunk: any) => string | undefined;
41
+ streamFinishReasonExtractor: (chunk: any) => string | undefined;
42
+ streamUsageExtractor: (chunk: any) => any;
43
+ }
44
+
45
+ function wrapAsyncIterator(
46
+ iterator: AsyncIterator<any>,
47
+ span: Span,
48
+ config: OtelPatchConfig
49
+ ): AsyncIterableIterator<any> {
50
+ const contentChunks: string[] = [];
51
+ let finishReason: string | null = null;
52
+ let usage: any = null;
53
+ let model: string | null = null;
54
+ let responseId: string | null = null;
55
+
56
+ return {
57
+ [Symbol.asyncIterator]() {
58
+ return this;
59
+ },
60
+ async next() {
61
+ try {
62
+ const result = await iterator.next();
63
+ if (result.done) {
64
+ // Stream complete — finalize span
65
+ if (contentChunks.length > 0) {
66
+ span.setAttribute(ATTR_GEN_AI_RESPONSE_TEXT, contentChunks.join(''));
67
+ }
68
+ if (finishReason) {
69
+ span.setAttribute(
70
+ ATTR_GEN_AI_RESPONSE_FINISH_REASONS,
71
+ JSON.stringify([finishReason])
72
+ );
73
+ }
74
+ if (model) {
75
+ span.setAttribute(ATTR_GEN_AI_RESPONSE_MODEL, model);
76
+ }
77
+ if (responseId) {
78
+ span.setAttribute(ATTR_GEN_AI_RESPONSE_ID, responseId);
79
+ }
80
+ if (usage) {
81
+ if (usage[config.inputTokensField] !== undefined) {
82
+ span.setAttribute(
83
+ ATTR_GEN_AI_USAGE_INPUT_TOKENS,
84
+ usage[config.inputTokensField]
85
+ );
86
+ }
87
+ if (usage[config.outputTokensField] !== undefined) {
88
+ span.setAttribute(
89
+ ATTR_GEN_AI_USAGE_OUTPUT_TOKENS,
90
+ usage[config.outputTokensField]
91
+ );
92
+ }
93
+ }
94
+ span.setStatus({ code: SpanStatusCode.OK });
95
+ span.end();
96
+ return result;
97
+ }
98
+
99
+ const chunk = result.value;
100
+
101
+ if (chunk.model && !model) model = chunk.model;
102
+ if (chunk.id && !responseId) responseId = chunk.id;
103
+
104
+ const deltaContent = config.streamDeltaContentExtractor(chunk);
105
+ if (deltaContent) contentChunks.push(deltaContent);
106
+
107
+ const chunkFinishReason = config.streamFinishReasonExtractor(chunk);
108
+ if (chunkFinishReason) finishReason = chunkFinishReason;
109
+
110
+ const chunkUsage = config.streamUsageExtractor(chunk);
111
+ if (chunkUsage) usage = chunkUsage;
112
+
113
+ return result;
114
+ } catch (error: any) {
115
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error?.message });
116
+ span.recordException(error);
117
+ span.end();
118
+ throw error;
119
+ }
120
+ },
121
+ async return(value?: any) {
122
+ span.setStatus({ code: SpanStatusCode.OK });
123
+ span.end();
124
+ if (iterator.return) {
125
+ return iterator.return(value);
126
+ }
127
+ return { done: true as const, value };
128
+ },
129
+ async throw(error?: any) {
130
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error?.message });
131
+ span.recordException(error);
132
+ span.end();
133
+ if (iterator.throw) {
134
+ return iterator.throw(error);
135
+ }
136
+ throw error;
137
+ },
138
+ };
139
+ }
140
+
141
+ function wrapStream(stream: any, span: Span, config: OtelPatchConfig): any {
142
+ const originalIterator = stream[Symbol.asyncIterator]();
143
+ const wrappedIterator = wrapAsyncIterator(originalIterator, span, config);
144
+
145
+ return new Proxy(stream, {
146
+ get(target, prop) {
147
+ if (prop === Symbol.asyncIterator) {
148
+ return () => wrappedIterator;
149
+ }
150
+ const value = target[prop];
151
+ if (typeof value === 'function') {
152
+ return value.bind(target);
153
+ }
154
+ return value;
155
+ },
156
+ });
157
+ }
158
+
159
+ function createOtelWrapper(
160
+ originalCreate: (...args: any[]) => any,
161
+ config: OtelPatchConfig
162
+ ): (body: any, options?: any) => any {
163
+ return function _agentuityOtelCreate(this: any, body: any, options?: any) {
164
+ const attributes: Record<string, any> = {
165
+ [ATTR_GEN_AI_SYSTEM]: config.provider,
166
+ [ATTR_GEN_AI_OPERATION_NAME]: 'chat',
167
+ };
168
+
169
+ if (body.model) attributes[ATTR_GEN_AI_REQUEST_MODEL] = body.model;
170
+ if (body.max_tokens) attributes[ATTR_GEN_AI_REQUEST_MAX_TOKENS] = body.max_tokens;
171
+ if (body.temperature !== undefined)
172
+ attributes[ATTR_GEN_AI_REQUEST_TEMPERATURE] = body.temperature;
173
+ if (body.top_p !== undefined) attributes[ATTR_GEN_AI_REQUEST_TOP_P] = body.top_p;
174
+ if (body.frequency_penalty !== undefined)
175
+ attributes[ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY] = body.frequency_penalty;
176
+ if (body.presence_penalty !== undefined)
177
+ attributes[ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY] = body.presence_penalty;
178
+
179
+ // Capture request messages
180
+ const messages = body[config.requestMessagesField];
181
+ if (messages && Array.isArray(messages)) {
182
+ try {
183
+ attributes[ATTR_GEN_AI_REQUEST_MESSAGES] = JSON.stringify(messages);
184
+ } catch {
185
+ // Ignore serialization errors
186
+ }
187
+ }
188
+
189
+ const spanName = body.model ? `chat ${body.model}` : 'chat';
190
+
191
+ return otelTracer.startActiveSpan(
192
+ spanName,
193
+ { attributes, kind: SpanKind.CLIENT },
194
+ (span: Span) => {
195
+ let result: any;
196
+ try {
197
+ result = originalCreate.call(this, body, options);
198
+ } catch (error: any) {
199
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error?.message });
200
+ span.recordException(error);
201
+ span.end();
202
+ throw error;
203
+ }
204
+
205
+ // Handle streaming responses
206
+ if (body.stream) {
207
+ if (result && typeof result.then === 'function') {
208
+ return result
209
+ .then((stream: any) => {
210
+ try {
211
+ return wrapStream(stream, span, config);
212
+ } catch (error: any) {
213
+ span.setStatus({
214
+ code: SpanStatusCode.ERROR,
215
+ message: error?.message,
216
+ });
217
+ span.recordException(error);
218
+ span.end();
219
+ throw error;
220
+ }
221
+ })
222
+ .catch((error: any) => {
223
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error?.message });
224
+ span.recordException(error);
225
+ span.end();
226
+ throw error;
227
+ });
228
+ }
229
+ try {
230
+ return wrapStream(result, span, config);
231
+ } catch (error: any) {
232
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error?.message });
233
+ span.recordException(error);
234
+ span.end();
235
+ throw error;
236
+ }
237
+ }
238
+
239
+ // Handle non-streaming responses
240
+ if (result && typeof result.then === 'function') {
241
+ return result
242
+ .then((response: any) => {
243
+ if (response) {
244
+ if (response.model) {
245
+ span.setAttribute(ATTR_GEN_AI_RESPONSE_MODEL, response.model);
246
+ }
247
+ if (response[config.responseIdField]) {
248
+ span.setAttribute(
249
+ ATTR_GEN_AI_RESPONSE_ID,
250
+ response[config.responseIdField]
251
+ );
252
+ }
253
+ if (response.usage) {
254
+ if (response.usage[config.inputTokensField] !== undefined) {
255
+ span.setAttribute(
256
+ ATTR_GEN_AI_USAGE_INPUT_TOKENS,
257
+ response.usage[config.inputTokensField]
258
+ );
259
+ }
260
+ if (response.usage[config.outputTokensField] !== undefined) {
261
+ span.setAttribute(
262
+ ATTR_GEN_AI_USAGE_OUTPUT_TOKENS,
263
+ response.usage[config.outputTokensField]
264
+ );
265
+ }
266
+ }
267
+ const finishReason = config.finishReasonExtractor(response);
268
+ if (finishReason) {
269
+ span.setAttribute(
270
+ ATTR_GEN_AI_RESPONSE_FINISH_REASONS,
271
+ JSON.stringify([finishReason])
272
+ );
273
+ }
274
+ const responseContent = config.responseContentExtractor(response);
275
+ if (responseContent) {
276
+ span.setAttribute(ATTR_GEN_AI_RESPONSE_TEXT, responseContent);
277
+ }
278
+ }
279
+ span.setStatus({ code: SpanStatusCode.OK });
280
+ span.end();
281
+ return response;
282
+ })
283
+ .catch((error: any) => {
284
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error?.message });
285
+ span.recordException(error);
286
+ span.end();
287
+ throw error;
288
+ });
289
+ }
290
+
291
+ span.end();
292
+ return result;
293
+ }
294
+ );
295
+ };
296
+ }
297
+
298
+ /** Provider-specific configurations matching the build-time patches */
299
+ const OTEL_CONFIGS: Array<{
300
+ module: string;
301
+ className: string;
302
+ config: OtelPatchConfig;
303
+ }> = [
304
+ {
305
+ module: 'openai',
306
+ // In the openai SDK, the Completions class is in resources/chat/completions
307
+ // We need to import the main module and access the prototype
308
+ className: 'Completions',
309
+ config: {
310
+ provider: 'openai',
311
+ inputTokensField: 'prompt_tokens',
312
+ outputTokensField: 'completion_tokens',
313
+ responseIdField: 'id',
314
+ finishReasonExtractor: (r) => r?.choices?.[0]?.finish_reason,
315
+ responseContentExtractor: (r) => r?.choices?.[0]?.message?.content,
316
+ requestMessagesField: 'messages',
317
+ streamDeltaContentExtractor: (c) => c?.choices?.[0]?.delta?.content,
318
+ streamFinishReasonExtractor: (c) => c?.choices?.[0]?.finish_reason,
319
+ streamUsageExtractor: (c) => c?.usage,
320
+ },
321
+ },
322
+ {
323
+ module: '@anthropic-ai/sdk',
324
+ className: 'Messages',
325
+ config: {
326
+ provider: 'anthropic',
327
+ inputTokensField: 'input_tokens',
328
+ outputTokensField: 'output_tokens',
329
+ responseIdField: 'id',
330
+ finishReasonExtractor: (r) => r?.stop_reason,
331
+ responseContentExtractor: (r) => r?.content?.[0]?.text,
332
+ requestMessagesField: 'messages',
333
+ streamDeltaContentExtractor: (c) => c?.delta?.text,
334
+ streamFinishReasonExtractor: (c) => c?.delta?.stop_reason,
335
+ streamUsageExtractor: (c) => c?.usage,
336
+ },
337
+ },
338
+ {
339
+ module: 'groq-sdk',
340
+ className: 'Completions',
341
+ config: {
342
+ provider: 'groq',
343
+ inputTokensField: 'prompt_tokens',
344
+ outputTokensField: 'completion_tokens',
345
+ responseIdField: 'id',
346
+ finishReasonExtractor: (r) => r?.choices?.[0]?.finish_reason,
347
+ responseContentExtractor: (r) => r?.choices?.[0]?.message?.content,
348
+ requestMessagesField: 'messages',
349
+ streamDeltaContentExtractor: (c) => c?.choices?.[0]?.delta?.content,
350
+ streamFinishReasonExtractor: (c) => c?.choices?.[0]?.finish_reason,
351
+ streamUsageExtractor: (c) => c?.x_groq?.usage,
352
+ },
353
+ },
354
+ ];
355
+
356
+ /**
357
+ * Find a class prototype by name within a module's exports (recursive search).
358
+ * The SDKs nest classes in sub-objects (e.g., openai.Chat.Completions).
359
+ */
360
+ function findPrototype(obj: any, className: string, depth = 0): any | null {
361
+ if (depth > 5 || !obj || typeof obj !== 'object') return null;
362
+
363
+ // Check if this object itself is the class we're looking for
364
+ if (typeof obj === 'function' && obj.name === className && obj.prototype?.create) {
365
+ return obj.prototype;
366
+ }
367
+
368
+ // Search properties
369
+ for (const key of Object.keys(obj)) {
370
+ try {
371
+ const val = obj[key];
372
+ if (typeof val === 'function' && val.name === className && val.prototype?.create) {
373
+ return val.prototype;
374
+ }
375
+ if (typeof val === 'object' && val !== null) {
376
+ const found = findPrototype(val, className, depth + 1);
377
+ if (found) return found;
378
+ }
379
+ } catch {
380
+ // Skip inaccessible properties
381
+ }
382
+ }
383
+ return null;
384
+ }
385
+
386
+ /**
387
+ * Apply OTel instrumentation patches to LLM SDK prototype methods.
388
+ */
389
+ export async function applyOtelLLMPatches(): Promise<void> {
390
+ for (const { module: moduleName, className, config } of OTEL_CONFIGS) {
391
+ try {
392
+ const mod = await import(moduleName);
393
+ const proto = findPrototype(mod, className);
394
+
395
+ if (!proto || typeof proto.create !== 'function') {
396
+ console.debug(
397
+ `[Agentuity OTel] Skipping patch: ${className}.prototype.create not found in ${moduleName}`
398
+ );
399
+ continue;
400
+ }
401
+
402
+ const originalCreate = proto.create;
403
+ proto.create = createOtelWrapper(originalCreate, config);
404
+ } catch {
405
+ // Module not installed — skip
406
+ }
407
+ }
408
+ }
package/src/index.ts CHANGED
@@ -263,3 +263,6 @@ export { bootstrapRuntimeEnv, type RuntimeBootstrapOptions, mimeTypes } from '@a
263
263
 
264
264
  // bun-s3-patch.ts exports
265
265
  export { patchBunS3ForStorageDev, isAgentuityStorageEndpoint } from './bun-s3-patch';
266
+
267
+ // dev-patches exports (runtime monkey-patches for --experimental-no-bundle dev mode)
268
+ export { applyDevPatches } from './dev-patches';