moda-ai 0.1.5 → 0.1.8
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 +105 -3
- package/dist/index.cjs +395 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +343 -2
- package/dist/index.mjs +393 -21
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,9 +100,9 @@ await withContext('conv_123', 'user_456', async () => {
|
|
|
100
100
|
});
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
## Automatic Fallback
|
|
103
|
+
## Automatic Fallback (Simple Chatbots Only)
|
|
104
104
|
|
|
105
|
-
If you don't set a conversation ID, the SDK automatically computes one
|
|
105
|
+
If you don't set a conversation ID, the SDK automatically computes one by hashing the first user message and system prompt. **This only works for simple chatbots where you pass the full message history with each API call:**
|
|
106
106
|
|
|
107
107
|
```typescript
|
|
108
108
|
// Turn 1
|
|
@@ -114,7 +114,44 @@ messages.push({ role: 'assistant', content: r1.choices[0].message.content });
|
|
|
114
114
|
messages.push({ role: 'user', content: 'How do I read a file?' });
|
|
115
115
|
const r2 = await openai.chat.completions.create({ model: 'gpt-4', messages });
|
|
116
116
|
|
|
117
|
-
// Both turns have the SAME conversation_id
|
|
117
|
+
// Both turns have the SAME conversation_id because "Hi, help with TypeScript"
|
|
118
|
+
// is still the first user message in both calls
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Why This Works
|
|
122
|
+
|
|
123
|
+
LLM APIs are stateless. Each API call must include the full conversation history. The SDK extracts the first user message from the `messages` array and hashes it to create a stable conversation ID across turns.
|
|
124
|
+
|
|
125
|
+
### When Automatic Detection Does NOT Work
|
|
126
|
+
|
|
127
|
+
**Agent frameworks (LangChain, Claude Agent SDK, CrewAI, AutoGPT, etc.) do NOT pass full message history.** Each agent iteration typically passes only:
|
|
128
|
+
- System prompt (with context baked in)
|
|
129
|
+
- Tool results from the previous step
|
|
130
|
+
- A continuation prompt
|
|
131
|
+
|
|
132
|
+
This means each iteration has a **different** first user message, resulting in **different** conversation IDs:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// Agent iteration 1
|
|
136
|
+
messages = [{ role: 'user', content: 'What are my top clusters?' }] // conv_abc123
|
|
137
|
+
|
|
138
|
+
// Agent iteration 2 (tool result)
|
|
139
|
+
messages = [{ role: 'user', content: 'Tool returned: ...' }] // conv_xyz789 - DIFFERENT!
|
|
140
|
+
|
|
141
|
+
// Agent iteration 3
|
|
142
|
+
messages = [{ role: 'user', content: 'Based on the data...' }] // conv_def456 - DIFFERENT!
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**For agent-based applications, you MUST use explicit conversation IDs:**
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// Wrap your entire agent execution
|
|
149
|
+
Moda.conversationId = 'agent_session_' + sessionId;
|
|
150
|
+
|
|
151
|
+
const agent = new LangChainAgent();
|
|
152
|
+
await agent.run('What are my top clusters?'); // All internal LLM calls share same ID
|
|
153
|
+
|
|
154
|
+
Moda.conversationId = null;
|
|
118
155
|
```
|
|
119
156
|
|
|
120
157
|
## Anthropic Support
|
|
@@ -153,6 +190,71 @@ for await (const chunk of stream) {
|
|
|
153
190
|
// Streaming responses are automatically tracked
|
|
154
191
|
```
|
|
155
192
|
|
|
193
|
+
## Using with Sentry (or other OpenTelemetry SDKs)
|
|
194
|
+
|
|
195
|
+
The Moda SDK automatically detects and coexists with other OpenTelemetry-based SDKs like Sentry. When an existing TracerProvider is detected, Moda adds its SpanProcessor to the existing provider instead of creating a new one.
|
|
196
|
+
|
|
197
|
+
### Sentry v8+ Integration
|
|
198
|
+
|
|
199
|
+
Sentry v8+ uses OpenTelemetry internally for tracing. Initialize Sentry first, then Moda:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import * as Sentry from '@sentry/node';
|
|
203
|
+
import { Moda } from 'moda-ai';
|
|
204
|
+
import OpenAI from 'openai';
|
|
205
|
+
|
|
206
|
+
// 1. Initialize Sentry FIRST (sets up OpenTelemetry TracerProvider)
|
|
207
|
+
Sentry.init({
|
|
208
|
+
dsn: 'https://xxx@xxx.ingest.sentry.io/xxx',
|
|
209
|
+
tracesSampleRate: 1.0,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// 2. Initialize Moda SECOND (detects Sentry's provider automatically)
|
|
213
|
+
await Moda.init('moda_your_api_key', {
|
|
214
|
+
debug: true, // Shows: "[Moda] Detected existing TracerProvider, adding Moda SpanProcessor to it"
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// 3. Use OpenAI normally - spans go to BOTH Sentry and Moda
|
|
218
|
+
const openai = new OpenAI();
|
|
219
|
+
const response = await openai.chat.completions.create({
|
|
220
|
+
model: 'gpt-4o-mini',
|
|
221
|
+
messages: [{ role: 'user', content: 'Hello!' }],
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// 4. Cleanup - Moda shutdown preserves Sentry
|
|
225
|
+
await Moda.flush();
|
|
226
|
+
await Moda.shutdown(); // Only shuts down Moda's processor, Sentry continues working
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### How It Works
|
|
230
|
+
|
|
231
|
+
When Moda detects an existing TracerProvider (e.g., from Sentry):
|
|
232
|
+
- Moda adds its SpanProcessor to the existing provider
|
|
233
|
+
- Both SDKs receive the same spans with identical trace IDs
|
|
234
|
+
- `Moda.shutdown()` only removes Moda's processor, preserving the other SDK
|
|
235
|
+
- You can re-initialize Moda after shutdown
|
|
236
|
+
|
|
237
|
+
### Expected Behavior
|
|
238
|
+
|
|
239
|
+
With `debug: true`, you should see:
|
|
240
|
+
```
|
|
241
|
+
[Moda] Detected existing TracerProvider, adding Moda SpanProcessor to it
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
You should NOT see:
|
|
245
|
+
```
|
|
246
|
+
Error: Attempted duplicate registration of tracer provider
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Supported SDKs
|
|
250
|
+
|
|
251
|
+
This coexistence works with any SDK that uses OpenTelemetry's TracerProvider:
|
|
252
|
+
- Sentry v8+
|
|
253
|
+
- Datadog APM
|
|
254
|
+
- New Relic
|
|
255
|
+
- Honeycomb
|
|
256
|
+
- Custom OpenTelemetry setups
|
|
257
|
+
|
|
156
258
|
## Configuration Options
|
|
157
259
|
|
|
158
260
|
```typescript
|
package/dist/index.cjs
CHANGED
|
@@ -1040,6 +1040,9 @@ async function registerInstrumentations() {
|
|
|
1040
1040
|
|
|
1041
1041
|
let provider = null;
|
|
1042
1042
|
let exporter = null;
|
|
1043
|
+
let modaProcessor = null;
|
|
1044
|
+
// Track if we're using an external provider (e.g., Sentry)
|
|
1045
|
+
let usingExternalProvider = false;
|
|
1043
1046
|
/**
|
|
1044
1047
|
* Check if the SDK is initialized
|
|
1045
1048
|
*/
|
|
@@ -1093,12 +1096,6 @@ async function init(apiKey, options = {}) {
|
|
|
1093
1096
|
if (mergedOptions.debug) {
|
|
1094
1097
|
api.diag.setLogger(new api.DiagConsoleLogger(), api.DiagLogLevel.DEBUG);
|
|
1095
1098
|
}
|
|
1096
|
-
// Create resource with service info
|
|
1097
|
-
const resource = new resources.Resource({
|
|
1098
|
-
[semconv__namespace.ATTR_SERVICE_NAME]: 'moda-sdk',
|
|
1099
|
-
[semconv__namespace.ATTR_SERVICE_VERSION]: '0.1.0',
|
|
1100
|
-
'moda.environment': mergedOptions.environment,
|
|
1101
|
-
});
|
|
1102
1099
|
// Create OTLP exporter with Moda API key in headers
|
|
1103
1100
|
exporter = new exporterTraceOtlpProto.OTLPTraceExporter({
|
|
1104
1101
|
url: mergedOptions.baseUrl,
|
|
@@ -1107,20 +1104,54 @@ async function init(apiKey, options = {}) {
|
|
|
1107
1104
|
'Content-Type': 'application/x-protobuf',
|
|
1108
1105
|
},
|
|
1109
1106
|
});
|
|
1110
|
-
// Create
|
|
1111
|
-
|
|
1112
|
-
resource,
|
|
1113
|
-
});
|
|
1114
|
-
// Use BatchSpanProcessor for production, SimpleSpanProcessor for debug
|
|
1115
|
-
const processor = mergedOptions.debug
|
|
1107
|
+
// Create Moda's span processor
|
|
1108
|
+
modaProcessor = mergedOptions.debug
|
|
1116
1109
|
? new sdkTraceBase.SimpleSpanProcessor(exporter)
|
|
1117
1110
|
: new sdkTraceBase.BatchSpanProcessor(exporter, {
|
|
1118
1111
|
maxQueueSize: mergedOptions.batchSize * 2,
|
|
1119
1112
|
maxExportBatchSize: mergedOptions.batchSize,
|
|
1120
1113
|
scheduledDelayMillis: mergedOptions.flushInterval,
|
|
1121
1114
|
});
|
|
1122
|
-
|
|
1123
|
-
|
|
1115
|
+
// Check if there's already a registered TracerProvider (e.g., from Sentry)
|
|
1116
|
+
// Sentry (and other SDKs) may wrap their provider in a ProxyTracerProvider,
|
|
1117
|
+
// so we need to check both the proxy and its delegate
|
|
1118
|
+
const existingProvider = api.trace.getTracerProvider();
|
|
1119
|
+
const isProxyProvider = existingProvider?.constructor?.name === 'ProxyTracerProvider';
|
|
1120
|
+
// Try to get the underlying provider if it's a proxy
|
|
1121
|
+
let targetProvider = existingProvider;
|
|
1122
|
+
if (isProxyProvider) {
|
|
1123
|
+
// ProxyTracerProvider wraps the real provider - get the delegate
|
|
1124
|
+
const delegate = existingProvider.getDelegate?.() || existingProvider._delegate;
|
|
1125
|
+
if (delegate && delegate.constructor?.name !== 'ProxyTracerProvider') {
|
|
1126
|
+
targetProvider = delegate;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
// Check if the target provider has addSpanProcessor (indicates a real SDK-managed provider)
|
|
1130
|
+
const hasAddSpanProcessor = targetProvider &&
|
|
1131
|
+
typeof targetProvider.addSpanProcessor === 'function';
|
|
1132
|
+
// Detect if this is a real external provider (not just the default noop proxy)
|
|
1133
|
+
const isExternalProvider = hasAddSpanProcessor &&
|
|
1134
|
+
targetProvider?.constructor?.name !== 'ProxyTracerProvider';
|
|
1135
|
+
if (isExternalProvider && targetProvider) {
|
|
1136
|
+
// Another SDK (like Sentry) already set up OTEL - add our processor to their provider
|
|
1137
|
+
targetProvider.addSpanProcessor(modaProcessor);
|
|
1138
|
+
usingExternalProvider = true;
|
|
1139
|
+
if (mergedOptions.debug) {
|
|
1140
|
+
console.log('[Moda] Detected existing TracerProvider, adding Moda SpanProcessor to it');
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
else {
|
|
1144
|
+
// No existing provider - create and register our own
|
|
1145
|
+
const resource = new resources.Resource({
|
|
1146
|
+
[semconv__namespace.ATTR_SERVICE_NAME]: 'moda-sdk',
|
|
1147
|
+
[semconv__namespace.ATTR_SERVICE_VERSION]: '0.1.0',
|
|
1148
|
+
'moda.environment': mergedOptions.environment,
|
|
1149
|
+
});
|
|
1150
|
+
provider = new sdkTraceNode.NodeTracerProvider({ resource });
|
|
1151
|
+
provider.addSpanProcessor(modaProcessor);
|
|
1152
|
+
provider.register();
|
|
1153
|
+
usingExternalProvider = false;
|
|
1154
|
+
}
|
|
1124
1155
|
// Register LLM instrumentations (async - uses dynamic imports for ESM compatibility)
|
|
1125
1156
|
await registerInstrumentations();
|
|
1126
1157
|
state.initialized = true;
|
|
@@ -1142,11 +1173,20 @@ async function init(apiKey, options = {}) {
|
|
|
1142
1173
|
* ```
|
|
1143
1174
|
*/
|
|
1144
1175
|
async function flush() {
|
|
1145
|
-
if (!state.initialized
|
|
1176
|
+
if (!state.initialized) {
|
|
1146
1177
|
return;
|
|
1147
1178
|
}
|
|
1148
1179
|
try {
|
|
1149
|
-
|
|
1180
|
+
if (usingExternalProvider) {
|
|
1181
|
+
// External provider: flush our processor directly
|
|
1182
|
+
if (modaProcessor) {
|
|
1183
|
+
await modaProcessor.forceFlush();
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
else if (provider) {
|
|
1187
|
+
// Our own provider: flush the whole provider
|
|
1188
|
+
await provider.forceFlush();
|
|
1189
|
+
}
|
|
1150
1190
|
if (state.options.debug) {
|
|
1151
1191
|
console.log('[Moda] Flushed all pending spans');
|
|
1152
1192
|
}
|
|
@@ -1175,11 +1215,21 @@ async function shutdown() {
|
|
|
1175
1215
|
return;
|
|
1176
1216
|
}
|
|
1177
1217
|
try {
|
|
1178
|
-
if (
|
|
1179
|
-
|
|
1218
|
+
if (usingExternalProvider) {
|
|
1219
|
+
// External provider: shutdown our processor only, preserve their provider
|
|
1220
|
+
if (modaProcessor) {
|
|
1221
|
+
await modaProcessor.shutdown();
|
|
1222
|
+
}
|
|
1223
|
+
if (state.options.debug) {
|
|
1224
|
+
console.log('[Moda] Moda processor shutdown complete (external provider preserved)');
|
|
1225
|
+
}
|
|
1180
1226
|
}
|
|
1181
|
-
if (
|
|
1182
|
-
|
|
1227
|
+
else if (provider) {
|
|
1228
|
+
// Our own provider: shutdown everything
|
|
1229
|
+
await provider.shutdown();
|
|
1230
|
+
if (state.options.debug) {
|
|
1231
|
+
console.log('[Moda] SDK shutdown complete');
|
|
1232
|
+
}
|
|
1183
1233
|
}
|
|
1184
1234
|
}
|
|
1185
1235
|
catch (error) {
|
|
@@ -1192,6 +1242,8 @@ async function shutdown() {
|
|
|
1192
1242
|
resetState();
|
|
1193
1243
|
provider = null;
|
|
1194
1244
|
exporter = null;
|
|
1245
|
+
modaProcessor = null;
|
|
1246
|
+
usingExternalProvider = false;
|
|
1195
1247
|
}
|
|
1196
1248
|
}
|
|
1197
1249
|
/**
|
|
@@ -1201,6 +1253,289 @@ async function shutdown() {
|
|
|
1201
1253
|
function getTracer() {
|
|
1202
1254
|
return api.trace.getTracer('moda-sdk', '0.1.0');
|
|
1203
1255
|
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Create a standalone Moda SpanProcessor for advanced OTEL setups.
|
|
1258
|
+
* Use when you need full control over your OpenTelemetry configuration.
|
|
1259
|
+
*
|
|
1260
|
+
* @example
|
|
1261
|
+
* ```typescript
|
|
1262
|
+
* import { createModaSpanProcessor } from 'moda-ai';
|
|
1263
|
+
* import { trace } from '@opentelemetry/api';
|
|
1264
|
+
*
|
|
1265
|
+
* const processor = createModaSpanProcessor({ apiKey: 'moda_xxx' });
|
|
1266
|
+
* (trace.getTracerProvider() as any).addSpanProcessor(processor);
|
|
1267
|
+
* ```
|
|
1268
|
+
*/
|
|
1269
|
+
function createModaSpanProcessor(options) {
|
|
1270
|
+
const { apiKey, baseUrl = DEFAULT_OPTIONS.baseUrl, debug = false, batchSize = DEFAULT_OPTIONS.batchSize, flushInterval = DEFAULT_OPTIONS.flushInterval, } = options;
|
|
1271
|
+
if (!apiKey || typeof apiKey !== 'string') {
|
|
1272
|
+
throw new Error('[Moda] API key is required');
|
|
1273
|
+
}
|
|
1274
|
+
const processorExporter = new exporterTraceOtlpProto.OTLPTraceExporter({
|
|
1275
|
+
url: baseUrl,
|
|
1276
|
+
headers: {
|
|
1277
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
1278
|
+
'Content-Type': 'application/x-protobuf',
|
|
1279
|
+
},
|
|
1280
|
+
});
|
|
1281
|
+
return debug
|
|
1282
|
+
? new sdkTraceBase.SimpleSpanProcessor(processorExporter)
|
|
1283
|
+
: new sdkTraceBase.BatchSpanProcessor(processorExporter, {
|
|
1284
|
+
maxQueueSize: batchSize * 2,
|
|
1285
|
+
maxExportBatchSize: batchSize,
|
|
1286
|
+
scheduledDelayMillis: flushInterval,
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* Manual LLM tracing API for instrumenting arbitrary LLM calls.
|
|
1292
|
+
* Use this when you can't use auto-instrumented SDKs (OpenAI/Anthropic).
|
|
1293
|
+
*
|
|
1294
|
+
* @example
|
|
1295
|
+
* ```typescript
|
|
1296
|
+
* const result = await withLLMCall(
|
|
1297
|
+
* { vendor: 'openrouter', type: 'chat' },
|
|
1298
|
+
* async ({ span }) => {
|
|
1299
|
+
* span.reportRequest({ model: 'gpt-4', messages });
|
|
1300
|
+
* const response = await fetch('https://api.example.com/chat', {...});
|
|
1301
|
+
* const data = await response.json();
|
|
1302
|
+
* span.reportResponse({ model: data.model, usage: data.usage, completions: data.choices });
|
|
1303
|
+
* return data;
|
|
1304
|
+
* }
|
|
1305
|
+
* );
|
|
1306
|
+
* ```
|
|
1307
|
+
*/
|
|
1308
|
+
/**
|
|
1309
|
+
* Create an LLMSpanHelper that wraps an OpenTelemetry span
|
|
1310
|
+
*/
|
|
1311
|
+
function createSpanHelper(span) {
|
|
1312
|
+
return {
|
|
1313
|
+
reportRequest(options) {
|
|
1314
|
+
const { model, messages, conversationId, userId } = options;
|
|
1315
|
+
// Set model
|
|
1316
|
+
span.setAttribute('llm.request.model', model);
|
|
1317
|
+
// Get effective context (global + local overrides)
|
|
1318
|
+
const globalContext = getEffectiveContext();
|
|
1319
|
+
// Determine conversation ID: local override > global > computed
|
|
1320
|
+
let effectiveConversationId = conversationId;
|
|
1321
|
+
if (!effectiveConversationId && globalContext.conversationId) {
|
|
1322
|
+
effectiveConversationId = globalContext.conversationId;
|
|
1323
|
+
}
|
|
1324
|
+
if (!effectiveConversationId) {
|
|
1325
|
+
effectiveConversationId = computeConversationId(messages);
|
|
1326
|
+
}
|
|
1327
|
+
span.setAttribute('moda.conversation_id', effectiveConversationId);
|
|
1328
|
+
// Set user ID if provided or from global context
|
|
1329
|
+
const effectiveUserId = userId ?? globalContext.userId;
|
|
1330
|
+
if (effectiveUserId) {
|
|
1331
|
+
span.setAttribute('moda.user_id', effectiveUserId);
|
|
1332
|
+
}
|
|
1333
|
+
// Format and set message attributes
|
|
1334
|
+
const messageAttrs = formatMessagesForSpan(messages);
|
|
1335
|
+
for (const [key, value] of Object.entries(messageAttrs)) {
|
|
1336
|
+
span.setAttribute(key, value);
|
|
1337
|
+
}
|
|
1338
|
+
},
|
|
1339
|
+
reportResponse(options) {
|
|
1340
|
+
const { model, usage, completions } = options;
|
|
1341
|
+
// Set response model if provided
|
|
1342
|
+
if (model) {
|
|
1343
|
+
span.setAttribute('llm.response.model', model);
|
|
1344
|
+
}
|
|
1345
|
+
// Set usage metrics
|
|
1346
|
+
if (usage) {
|
|
1347
|
+
const promptTokens = usage.prompt_tokens ?? usage.input_tokens;
|
|
1348
|
+
const completionTokens = usage.completion_tokens ?? usage.output_tokens;
|
|
1349
|
+
const totalTokens = usage.total_tokens ??
|
|
1350
|
+
(promptTokens !== undefined && completionTokens !== undefined
|
|
1351
|
+
? promptTokens + completionTokens
|
|
1352
|
+
: undefined);
|
|
1353
|
+
if (promptTokens !== undefined) {
|
|
1354
|
+
span.setAttribute('llm.usage.prompt_tokens', promptTokens);
|
|
1355
|
+
}
|
|
1356
|
+
if (completionTokens !== undefined) {
|
|
1357
|
+
span.setAttribute('llm.usage.completion_tokens', completionTokens);
|
|
1358
|
+
}
|
|
1359
|
+
if (totalTokens !== undefined) {
|
|
1360
|
+
span.setAttribute('llm.usage.total_tokens', totalTokens);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
// Set completion attributes
|
|
1364
|
+
if (completions && completions.length > 0) {
|
|
1365
|
+
completions.forEach((completion, index) => {
|
|
1366
|
+
// Handle OpenAI-style nested message or direct properties
|
|
1367
|
+
const role = completion.role ?? completion.message?.role ?? 'assistant';
|
|
1368
|
+
const content = completion.content ?? completion.message?.content ?? '';
|
|
1369
|
+
const attrs = formatCompletionForSpan(role, content, index);
|
|
1370
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
1371
|
+
span.setAttribute(key, value);
|
|
1372
|
+
}
|
|
1373
|
+
// Set finish reason from first completion
|
|
1374
|
+
if (index === 0 && completion.finish_reason) {
|
|
1375
|
+
span.setAttribute('llm.response.finish_reason', completion.finish_reason);
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
},
|
|
1380
|
+
get rawSpan() {
|
|
1381
|
+
return span;
|
|
1382
|
+
},
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Wrap an arbitrary LLM call with OpenTelemetry tracing.
|
|
1387
|
+
*
|
|
1388
|
+
* Use this when you can't use auto-instrumented SDKs (OpenAI/Anthropic)
|
|
1389
|
+
* and need to manually instrument LLM calls (e.g., direct fetch to OpenRouter,
|
|
1390
|
+
* custom LLM providers, proxied requests).
|
|
1391
|
+
*
|
|
1392
|
+
* @param options - Vendor and request type configuration
|
|
1393
|
+
* @param callback - Async function that makes the LLM call
|
|
1394
|
+
* @returns The return value of the callback
|
|
1395
|
+
*
|
|
1396
|
+
* @example
|
|
1397
|
+
* ```typescript
|
|
1398
|
+
* const result = await withLLMCall(
|
|
1399
|
+
* { vendor: 'openrouter', type: 'chat' },
|
|
1400
|
+
* async ({ span }) => {
|
|
1401
|
+
* span.reportRequest({ model: 'anthropic/claude-3-sonnet', messages });
|
|
1402
|
+
*
|
|
1403
|
+
* const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
1404
|
+
* method: 'POST',
|
|
1405
|
+
* headers: { Authorization: `Bearer ${apiKey}` },
|
|
1406
|
+
* body: JSON.stringify({ model, messages }),
|
|
1407
|
+
* });
|
|
1408
|
+
* const data = await response.json();
|
|
1409
|
+
*
|
|
1410
|
+
* span.reportResponse({
|
|
1411
|
+
* model: data.model,
|
|
1412
|
+
* usage: data.usage,
|
|
1413
|
+
* completions: data.choices,
|
|
1414
|
+
* });
|
|
1415
|
+
*
|
|
1416
|
+
* return data;
|
|
1417
|
+
* }
|
|
1418
|
+
* );
|
|
1419
|
+
* ```
|
|
1420
|
+
*/
|
|
1421
|
+
async function withLLMCall(options, callback) {
|
|
1422
|
+
const { vendor, type } = options;
|
|
1423
|
+
const tracer = api.trace.getTracer('moda-sdk', '0.1.0');
|
|
1424
|
+
const span = tracer.startSpan(`${vendor}.${type}`, {
|
|
1425
|
+
attributes: {
|
|
1426
|
+
'llm.vendor': vendor,
|
|
1427
|
+
'llm.request.type': type,
|
|
1428
|
+
},
|
|
1429
|
+
});
|
|
1430
|
+
const spanHelper = createSpanHelper(span);
|
|
1431
|
+
try {
|
|
1432
|
+
const result = await callback({ span: spanHelper });
|
|
1433
|
+
span.setStatus({ code: api.SpanStatusCode.OK });
|
|
1434
|
+
return result;
|
|
1435
|
+
}
|
|
1436
|
+
catch (error) {
|
|
1437
|
+
span.setStatus({
|
|
1438
|
+
code: api.SpanStatusCode.ERROR,
|
|
1439
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1440
|
+
});
|
|
1441
|
+
if (error instanceof Error) {
|
|
1442
|
+
span.recordException(error);
|
|
1443
|
+
}
|
|
1444
|
+
throw error;
|
|
1445
|
+
}
|
|
1446
|
+
finally {
|
|
1447
|
+
span.end();
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
/**
|
|
1452
|
+
* Vercel AI SDK integration for Moda observability.
|
|
1453
|
+
*
|
|
1454
|
+
* The Vercel AI SDK has built-in OpenTelemetry support via `experimental_telemetry`.
|
|
1455
|
+
* This module provides helper functions to integrate Moda with the AI SDK.
|
|
1456
|
+
*
|
|
1457
|
+
* @example
|
|
1458
|
+
* ```typescript
|
|
1459
|
+
* import { Moda } from 'moda-ai';
|
|
1460
|
+
* import { generateText } from 'ai';
|
|
1461
|
+
*
|
|
1462
|
+
* Moda.init('your-api-key');
|
|
1463
|
+
*
|
|
1464
|
+
* const result = await generateText({
|
|
1465
|
+
* model: openai('gpt-4o'),
|
|
1466
|
+
* prompt: 'Hello',
|
|
1467
|
+
* experimental_telemetry: Moda.getVercelAITelemetry(),
|
|
1468
|
+
* });
|
|
1469
|
+
* ```
|
|
1470
|
+
*/
|
|
1471
|
+
/**
|
|
1472
|
+
* Get a telemetry configuration object for the Vercel AI SDK.
|
|
1473
|
+
*
|
|
1474
|
+
* This returns a configuration that can be passed directly to the
|
|
1475
|
+
* `experimental_telemetry` option of AI SDK functions like `generateText`,
|
|
1476
|
+
* `streamText`, `generateObject`, etc.
|
|
1477
|
+
*
|
|
1478
|
+
* The configuration includes:
|
|
1479
|
+
* - Moda's OpenTelemetry tracer for span collection
|
|
1480
|
+
* - Automatic inclusion of conversation_id and user_id in metadata
|
|
1481
|
+
* - Configurable input/output recording for privacy control
|
|
1482
|
+
*
|
|
1483
|
+
* @param options - Optional configuration overrides
|
|
1484
|
+
* @returns Telemetry configuration for Vercel AI SDK
|
|
1485
|
+
*
|
|
1486
|
+
* @example
|
|
1487
|
+
* ```typescript
|
|
1488
|
+
* import { Moda } from 'moda-ai';
|
|
1489
|
+
* import { generateText } from 'ai';
|
|
1490
|
+
* import { openai } from '@ai-sdk/openai';
|
|
1491
|
+
*
|
|
1492
|
+
* Moda.init('your-api-key');
|
|
1493
|
+
* Moda.conversationId = 'conv_123';
|
|
1494
|
+
*
|
|
1495
|
+
* const result = await generateText({
|
|
1496
|
+
* model: openai('gpt-4o'),
|
|
1497
|
+
* prompt: 'Write a haiku about coding',
|
|
1498
|
+
* experimental_telemetry: Moda.getVercelAITelemetry(),
|
|
1499
|
+
* });
|
|
1500
|
+
* ```
|
|
1501
|
+
*
|
|
1502
|
+
* @example
|
|
1503
|
+
* ```typescript
|
|
1504
|
+
* // With custom options
|
|
1505
|
+
* const result = await generateText({
|
|
1506
|
+
* model: openai('gpt-4o'),
|
|
1507
|
+
* prompt: 'Process this sensitive data',
|
|
1508
|
+
* experimental_telemetry: Moda.getVercelAITelemetry({
|
|
1509
|
+
* recordInputs: false, // Don't record sensitive prompts
|
|
1510
|
+
* recordOutputs: false, // Don't record sensitive outputs
|
|
1511
|
+
* functionId: 'sensitive-processor',
|
|
1512
|
+
* metadata: { operation: 'pii-processing' },
|
|
1513
|
+
* }),
|
|
1514
|
+
* });
|
|
1515
|
+
* ```
|
|
1516
|
+
*/
|
|
1517
|
+
function getVercelAITelemetry(options = {}) {
|
|
1518
|
+
const context = getEffectiveContext();
|
|
1519
|
+
// Build metadata with Moda context
|
|
1520
|
+
const metadata = {
|
|
1521
|
+
...options.metadata,
|
|
1522
|
+
};
|
|
1523
|
+
// Add Moda context to metadata
|
|
1524
|
+
if (context.conversationId) {
|
|
1525
|
+
metadata['moda.conversation_id'] = context.conversationId;
|
|
1526
|
+
}
|
|
1527
|
+
if (context.userId) {
|
|
1528
|
+
metadata['moda.user_id'] = context.userId;
|
|
1529
|
+
}
|
|
1530
|
+
return {
|
|
1531
|
+
isEnabled: true,
|
|
1532
|
+
recordInputs: options.recordInputs,
|
|
1533
|
+
recordOutputs: options.recordOutputs,
|
|
1534
|
+
functionId: options.functionId,
|
|
1535
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
1536
|
+
tracer: getTracer(),
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1204
1539
|
|
|
1205
1540
|
/**
|
|
1206
1541
|
* @moda/sdk - Official TypeScript/Node.js SDK for Moda LLM observability
|
|
@@ -1270,6 +1605,43 @@ const Moda = {
|
|
|
1270
1605
|
* @see {@link getTracer}
|
|
1271
1606
|
*/
|
|
1272
1607
|
getTracer,
|
|
1608
|
+
/**
|
|
1609
|
+
* Manually trace an LLM call when using non-instrumented providers
|
|
1610
|
+
* @see {@link withLLMCall}
|
|
1611
|
+
*/
|
|
1612
|
+
withLLMCall,
|
|
1613
|
+
/**
|
|
1614
|
+
* Get telemetry configuration for Vercel AI SDK integration.
|
|
1615
|
+
* Returns a config object for the `experimental_telemetry` option.
|
|
1616
|
+
* @see {@link getVercelAITelemetry}
|
|
1617
|
+
*
|
|
1618
|
+
* @example
|
|
1619
|
+
* ```typescript
|
|
1620
|
+
* import { generateText } from 'ai';
|
|
1621
|
+
*
|
|
1622
|
+
* const result = await generateText({
|
|
1623
|
+
* model: openai('gpt-4o'),
|
|
1624
|
+
* prompt: 'Hello',
|
|
1625
|
+
* experimental_telemetry: Moda.getVercelAITelemetry(),
|
|
1626
|
+
* });
|
|
1627
|
+
* ```
|
|
1628
|
+
*/
|
|
1629
|
+
getVercelAITelemetry,
|
|
1630
|
+
/**
|
|
1631
|
+
* Create a standalone Moda SpanProcessor for advanced OTEL setups.
|
|
1632
|
+
* Use when you need full control over your OpenTelemetry configuration.
|
|
1633
|
+
* @see {@link createModaSpanProcessor}
|
|
1634
|
+
*
|
|
1635
|
+
* @example
|
|
1636
|
+
* ```typescript
|
|
1637
|
+
* import { Moda } from 'moda-ai';
|
|
1638
|
+
* import { trace } from '@opentelemetry/api';
|
|
1639
|
+
*
|
|
1640
|
+
* const processor = Moda.createModaSpanProcessor({ apiKey: 'moda_xxx' });
|
|
1641
|
+
* (trace.getTracerProvider() as any).addSpanProcessor(processor);
|
|
1642
|
+
* ```
|
|
1643
|
+
*/
|
|
1644
|
+
createModaSpanProcessor,
|
|
1273
1645
|
/**
|
|
1274
1646
|
* Get or set the global conversation ID.
|
|
1275
1647
|
* Setting to null clears the conversation ID.
|
|
@@ -1321,6 +1693,7 @@ exports.Moda = Moda;
|
|
|
1321
1693
|
exports.clearConversationId = clearConversationId;
|
|
1322
1694
|
exports.clearUserId = clearUserId;
|
|
1323
1695
|
exports.computeConversationId = computeConversationId;
|
|
1696
|
+
exports.createModaSpanProcessor = createModaSpanProcessor;
|
|
1324
1697
|
exports.default = Moda;
|
|
1325
1698
|
exports.flush = flush;
|
|
1326
1699
|
exports.generateRandomConversationId = generateRandomConversationId;
|
|
@@ -1328,6 +1701,7 @@ exports.getContext = getContext;
|
|
|
1328
1701
|
exports.getEffectiveContext = getEffectiveContext;
|
|
1329
1702
|
exports.getGlobalContext = getGlobalContext;
|
|
1330
1703
|
exports.getTracer = getTracer;
|
|
1704
|
+
exports.getVercelAITelemetry = getVercelAITelemetry;
|
|
1331
1705
|
exports.init = init;
|
|
1332
1706
|
exports.isInitialized = isInitialized;
|
|
1333
1707
|
exports.isValidConversationId = isValidConversationId;
|
|
@@ -1336,5 +1710,6 @@ exports.setUserId = setUserId;
|
|
|
1336
1710
|
exports.shutdown = shutdown;
|
|
1337
1711
|
exports.withContext = withContext;
|
|
1338
1712
|
exports.withConversationId = withConversationId;
|
|
1713
|
+
exports.withLLMCall = withLLMCall;
|
|
1339
1714
|
exports.withUserId = withUserId;
|
|
1340
1715
|
//# sourceMappingURL=index.cjs.map
|