scorecard-ai 2.5.0 → 3.0.0-beta.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/CHANGELOG.md +68 -0
- package/LICENSE +1 -1
- package/README.md +9 -0
- package/client.d.mts +1 -1
- package/client.d.mts.map +1 -1
- package/client.d.ts +1 -1
- package/client.d.ts.map +1 -1
- package/index.d.mts +1 -0
- package/index.d.mts.map +1 -1
- package/index.d.ts +1 -0
- package/index.d.ts.map +1 -1
- package/index.js +5 -1
- package/index.js.map +1 -1
- package/index.mjs +1 -0
- package/index.mjs.map +1 -1
- package/lib/wrapLLMs.d.mts +63 -0
- package/lib/wrapLLMs.d.mts.map +1 -0
- package/lib/wrapLLMs.d.ts +63 -0
- package/lib/wrapLLMs.d.ts.map +1 -0
- package/lib/wrapLLMs.js +386 -0
- package/lib/wrapLLMs.js.map +1 -0
- package/lib/wrapLLMs.mjs +382 -0
- package/lib/wrapLLMs.mjs.map +1 -0
- package/package.json +1 -1
- package/src/client.ts +1 -1
- package/src/index.ts +1 -0
- package/src/lib/wrapLLMs.ts +485 -0
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.mts.map +1 -1
- package/version.d.ts +1 -1
- package/version.d.ts.map +1 -1
- package/version.js +1 -1
- package/version.js.map +1 -1
- package/version.mjs +1 -1
- package/version.mjs.map +1 -1
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { trace, context, Span, Context } from '@opentelemetry/api';
|
|
2
|
+
import {
|
|
3
|
+
NodeTracerProvider,
|
|
4
|
+
BatchSpanProcessor,
|
|
5
|
+
ReadableSpan,
|
|
6
|
+
Span as SdkSpan,
|
|
7
|
+
SpanProcessor,
|
|
8
|
+
} from '@opentelemetry/sdk-trace-node';
|
|
9
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
10
|
+
import { resourceFromAttributes } from '@opentelemetry/resources';
|
|
11
|
+
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
|
|
12
|
+
import { readEnv } from '../internal/utils';
|
|
13
|
+
import { ScorecardError } from '../error';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configuration for wrapping LLM SDKs
|
|
17
|
+
*/
|
|
18
|
+
interface WrapConfig {
|
|
19
|
+
/**
|
|
20
|
+
* ID of the Scorecard project that traces should be associated with.
|
|
21
|
+
* Defaults to SCORECARD_PROJECT_ID environment variable.
|
|
22
|
+
*/
|
|
23
|
+
projectId?: string | undefined;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Scorecard API key for authentication.
|
|
27
|
+
* Defaults to SCORECARD_API_KEY environment variable.
|
|
28
|
+
*/
|
|
29
|
+
apiKey?: string | undefined;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Service name for telemetry.
|
|
33
|
+
* Defaults to "llm-app".
|
|
34
|
+
*/
|
|
35
|
+
serviceName?: string | undefined;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* OTLP endpoint for trace export.
|
|
39
|
+
* Defaults to "https://tracing.scorecard.io/otel/v1/traces".
|
|
40
|
+
*/
|
|
41
|
+
endpoint?: string | undefined;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Maximum batch size of spans to be exported in a single request.
|
|
45
|
+
* Lower values provide faster feedback but more network requests.
|
|
46
|
+
* Higher values are more efficient but delay span visibility.
|
|
47
|
+
* @default 1
|
|
48
|
+
*/
|
|
49
|
+
maxExportBatchSize?: number | undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type LLMProvider = 'openai' | 'anthropic';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Custom exporter that wraps OTLP exporter and injects projectId from span attributes
|
|
56
|
+
* into the resource before export. This allows per-span projectId while keeping
|
|
57
|
+
* ResourceAttributes where the backend expects them.
|
|
58
|
+
*/
|
|
59
|
+
class ScorecardExporter extends OTLPTraceExporter {
|
|
60
|
+
override export(spans: ReadableSpan[], resultCallback: (result: any) => void): void {
|
|
61
|
+
// For each span, inject all scorecard.* attributes into the resource
|
|
62
|
+
spans.forEach((span) => {
|
|
63
|
+
// Collect all scorecard.* attributes from span attributes
|
|
64
|
+
const scorecardAttrs = Object.entries(span.attributes).reduce(
|
|
65
|
+
(acc, [key, value]) => {
|
|
66
|
+
if (key.startsWith('scorecard.')) {
|
|
67
|
+
acc[key] = value;
|
|
68
|
+
}
|
|
69
|
+
return acc;
|
|
70
|
+
},
|
|
71
|
+
{} as Record<string, any>,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (Object.keys(scorecardAttrs).length > 0) {
|
|
75
|
+
// Merge all scorecard.* attributes into the resource
|
|
76
|
+
const newResource = span.resource.merge(resourceFromAttributes(scorecardAttrs));
|
|
77
|
+
|
|
78
|
+
// Directly assign the new resource (cast to any to bypass readonly)
|
|
79
|
+
(span as any).resource = newResource;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Call the parent exporter with modified spans
|
|
84
|
+
super.export(spans, resultCallback);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Composite processor that forwards span events to all registered processors.
|
|
90
|
+
* Allows dynamic addition of exporters after provider registration.
|
|
91
|
+
*/
|
|
92
|
+
class CompositeProcessor implements SpanProcessor {
|
|
93
|
+
private processors = new Map<string, BatchSpanProcessor>();
|
|
94
|
+
|
|
95
|
+
addProcessor(apiKey: string, endpoint: string, maxExportBatchSize: number): void {
|
|
96
|
+
const key = `${apiKey}:${endpoint}`;
|
|
97
|
+
if (this.processors.has(key)) return;
|
|
98
|
+
|
|
99
|
+
const exporter = new ScorecardExporter({
|
|
100
|
+
url: endpoint,
|
|
101
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const processor = new BatchSpanProcessor(exporter, {
|
|
105
|
+
maxExportBatchSize,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
this.processors.set(key, processor);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
onStart(span: SdkSpan, parentContext: Context): void {
|
|
112
|
+
for (const processor of this.processors.values()) {
|
|
113
|
+
processor.onStart(span, parentContext);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
onEnd(span: ReadableSpan): void {
|
|
118
|
+
for (const processor of this.processors.values()) {
|
|
119
|
+
processor.onEnd(span);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async forceFlush(): Promise<void> {
|
|
124
|
+
await Promise.all(Array.from(this.processors.values()).map((p) => p.forceFlush()));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async shutdown(): Promise<void> {
|
|
128
|
+
await Promise.all(Array.from(this.processors.values()).map((p) => p.shutdown()));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let globalProvider: NodeTracerProvider | null = null;
|
|
133
|
+
let globalTracer: ReturnType<typeof trace.getTracer> | null = null;
|
|
134
|
+
let compositeProcessor: CompositeProcessor | null = null;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Initialize OpenTelemetry provider for LLM SDK wrappers.
|
|
138
|
+
* Creates a single global provider for nesting support, with exporters
|
|
139
|
+
* added dynamically for each unique apiKey+endpoint combination.
|
|
140
|
+
*/
|
|
141
|
+
function initProvider(config: WrapConfig): string | undefined {
|
|
142
|
+
const apiKey = config.apiKey || readEnv('SCORECARD_API_KEY');
|
|
143
|
+
if (!apiKey) {
|
|
144
|
+
throw new ScorecardError(
|
|
145
|
+
'Scorecard API key is required. Set SCORECARD_API_KEY environment variable or pass apiKey in config.',
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const endpoint = config.endpoint || 'https://tracing.scorecard.io/otel/v1/traces';
|
|
150
|
+
const serviceName = config.serviceName || 'llm-app';
|
|
151
|
+
const projectId = config.projectId || readEnv('SCORECARD_PROJECT_ID');
|
|
152
|
+
const maxExportBatchSize = config.maxExportBatchSize ?? 1;
|
|
153
|
+
|
|
154
|
+
// Create the global provider once (enables span nesting)
|
|
155
|
+
if (!globalProvider) {
|
|
156
|
+
compositeProcessor = new CompositeProcessor();
|
|
157
|
+
|
|
158
|
+
const resource = resourceFromAttributes({
|
|
159
|
+
[ATTR_SERVICE_NAME]: serviceName,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
globalProvider = new NodeTracerProvider({
|
|
163
|
+
resource,
|
|
164
|
+
spanProcessors: [compositeProcessor as any],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
globalProvider.register();
|
|
168
|
+
globalTracer = trace.getTracer('scorecard-llm');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Add an exporter for this specific apiKey+endpoint (if not already added)
|
|
172
|
+
compositeProcessor?.addProcessor(apiKey, endpoint, maxExportBatchSize);
|
|
173
|
+
|
|
174
|
+
return projectId;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Detect which LLM provider the client is for
|
|
179
|
+
*/
|
|
180
|
+
function detectProvider(client: any): LLMProvider {
|
|
181
|
+
// Check for OpenAI SDK structure
|
|
182
|
+
if (client.chat?.completions) {
|
|
183
|
+
return 'openai';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check for Anthropic SDK structure
|
|
187
|
+
if (client.messages) {
|
|
188
|
+
return 'anthropic';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
throw new ScorecardError(
|
|
192
|
+
'Unable to detect LLM provider. Client must be an OpenAI or Anthropic SDK instance.',
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Handle OpenAI-specific response parsing
|
|
198
|
+
*/
|
|
199
|
+
function handleOpenAIResponse(span: Span, result: any, params: any) {
|
|
200
|
+
span.setAttributes({
|
|
201
|
+
'gen_ai.response.id': result.id || 'unknown',
|
|
202
|
+
'gen_ai.response.model': result.model || params.model || 'unknown',
|
|
203
|
+
'gen_ai.response.finish_reason': result.choices?.[0]?.finish_reason || 'unknown',
|
|
204
|
+
'gen_ai.usage.prompt_tokens': result.usage?.prompt_tokens || 0,
|
|
205
|
+
'gen_ai.usage.completion_tokens': result.usage?.completion_tokens || 0,
|
|
206
|
+
'gen_ai.usage.total_tokens': result.usage?.total_tokens || 0,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (result.choices?.[0]?.message) {
|
|
210
|
+
span.setAttribute('gen_ai.completion.choices', JSON.stringify([result.choices[0].message]));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Handle Anthropic-specific response parsing
|
|
216
|
+
*/
|
|
217
|
+
function handleAnthropicResponse(span: Span, result: any, params: any) {
|
|
218
|
+
span.setAttributes({
|
|
219
|
+
'gen_ai.response.id': result.id || 'unknown',
|
|
220
|
+
'gen_ai.response.model': result.model || params.model || 'unknown',
|
|
221
|
+
'gen_ai.response.finish_reason': result.stop_reason || 'unknown',
|
|
222
|
+
'gen_ai.usage.prompt_tokens': result.usage?.input_tokens || 0,
|
|
223
|
+
'gen_ai.usage.completion_tokens': result.usage?.output_tokens || 0,
|
|
224
|
+
'gen_ai.usage.total_tokens': (result.usage?.input_tokens || 0) + (result.usage?.output_tokens || 0),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Collect text from all text blocks (Anthropic can return multiple content blocks)
|
|
228
|
+
if (result.content) {
|
|
229
|
+
const completionTexts = result.content.filter((c: any) => c.text).map((c: any) => c.text);
|
|
230
|
+
if (completionTexts.length > 0) {
|
|
231
|
+
span.setAttribute(
|
|
232
|
+
'gen_ai.completion.choices',
|
|
233
|
+
JSON.stringify([{ message: { role: 'assistant', content: completionTexts.join('\n') } }]),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Wrapper for async streams that collects metadata and ends span when consumed
|
|
241
|
+
*/
|
|
242
|
+
class StreamWrapper {
|
|
243
|
+
private contentParts: string[] = [];
|
|
244
|
+
private finishReason: string | null = null;
|
|
245
|
+
private usageData: Record<string, number> = {};
|
|
246
|
+
private responseId: string | null = null;
|
|
247
|
+
private model: string | null = null;
|
|
248
|
+
|
|
249
|
+
constructor(
|
|
250
|
+
private stream: AsyncIterable<any>,
|
|
251
|
+
private span: Span,
|
|
252
|
+
private provider: LLMProvider,
|
|
253
|
+
private params: any,
|
|
254
|
+
) {}
|
|
255
|
+
|
|
256
|
+
async *[Symbol.asyncIterator](): AsyncIterator<any> {
|
|
257
|
+
try {
|
|
258
|
+
for await (const chunk of this.stream) {
|
|
259
|
+
this.processChunk(chunk);
|
|
260
|
+
yield chunk;
|
|
261
|
+
}
|
|
262
|
+
} finally {
|
|
263
|
+
this.finalizeSpan();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private processChunk(chunk: any): void {
|
|
268
|
+
// OpenAI streaming format
|
|
269
|
+
if (this.provider === 'openai') {
|
|
270
|
+
if (!this.responseId && chunk.id) {
|
|
271
|
+
this.responseId = chunk.id;
|
|
272
|
+
}
|
|
273
|
+
if (!this.model && chunk.model) {
|
|
274
|
+
this.model = chunk.model;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (chunk.choices?.[0]) {
|
|
278
|
+
const choice = chunk.choices[0];
|
|
279
|
+
if (choice.delta?.content) {
|
|
280
|
+
this.contentParts.push(choice.delta.content);
|
|
281
|
+
}
|
|
282
|
+
if (choice.finish_reason) {
|
|
283
|
+
this.finishReason = choice.finish_reason;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// OpenAI includes usage in the last chunk with stream_options
|
|
288
|
+
if (chunk.usage) {
|
|
289
|
+
this.usageData = {
|
|
290
|
+
prompt_tokens: chunk.usage.prompt_tokens || 0,
|
|
291
|
+
completion_tokens: chunk.usage.completion_tokens || 0,
|
|
292
|
+
total_tokens: chunk.usage.total_tokens || 0,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Anthropic streaming format
|
|
297
|
+
else if (this.provider === 'anthropic') {
|
|
298
|
+
if (chunk.type === 'message_start' && chunk.message) {
|
|
299
|
+
this.responseId = chunk.message.id;
|
|
300
|
+
this.model = chunk.message.model;
|
|
301
|
+
if (chunk.message.usage) {
|
|
302
|
+
this.usageData['input_tokens'] = chunk.message.usage.input_tokens || 0;
|
|
303
|
+
}
|
|
304
|
+
} else if (chunk.type === 'content_block_delta' && chunk.delta?.text) {
|
|
305
|
+
this.contentParts.push(chunk.delta.text);
|
|
306
|
+
} else if (chunk.type === 'message_delta') {
|
|
307
|
+
if (chunk.delta?.stop_reason) {
|
|
308
|
+
this.finishReason = chunk.delta.stop_reason;
|
|
309
|
+
}
|
|
310
|
+
if (chunk.usage?.output_tokens) {
|
|
311
|
+
this.usageData['output_tokens'] = chunk.usage.output_tokens;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private finalizeSpan(): void {
|
|
318
|
+
// Set response attributes
|
|
319
|
+
this.span.setAttributes({
|
|
320
|
+
'gen_ai.response.id': this.responseId || 'unknown',
|
|
321
|
+
'gen_ai.response.model': this.model || this.params.model || 'unknown',
|
|
322
|
+
'gen_ai.response.finish_reason': this.finishReason || 'unknown',
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Set usage data
|
|
326
|
+
if (Object.keys(this.usageData).length > 0) {
|
|
327
|
+
if (this.provider === 'openai') {
|
|
328
|
+
this.span.setAttributes({
|
|
329
|
+
'gen_ai.usage.prompt_tokens': this.usageData['prompt_tokens'] || 0,
|
|
330
|
+
'gen_ai.usage.completion_tokens': this.usageData['completion_tokens'] || 0,
|
|
331
|
+
'gen_ai.usage.total_tokens': this.usageData['total_tokens'] || 0,
|
|
332
|
+
});
|
|
333
|
+
} else if (this.provider === 'anthropic') {
|
|
334
|
+
const inputTokens = this.usageData['input_tokens'] || 0;
|
|
335
|
+
const outputTokens = this.usageData['output_tokens'] || 0;
|
|
336
|
+
this.span.setAttributes({
|
|
337
|
+
'gen_ai.usage.prompt_tokens': inputTokens,
|
|
338
|
+
'gen_ai.usage.completion_tokens': outputTokens,
|
|
339
|
+
'gen_ai.usage.total_tokens': inputTokens + outputTokens,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Set completion content if any was collected
|
|
345
|
+
if (this.contentParts.length > 0) {
|
|
346
|
+
const content = this.contentParts.join('');
|
|
347
|
+
this.span.setAttribute(
|
|
348
|
+
'gen_ai.completion.choices',
|
|
349
|
+
JSON.stringify([{ message: { role: 'assistant', content } }]),
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
this.span.end();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Wrap any LLM SDK (OpenAI or Anthropic) to automatically trace all API calls
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* ```typescript
|
|
362
|
+
* import { wrap } from '@scorecard/node';
|
|
363
|
+
* import OpenAI from 'openai';
|
|
364
|
+
* import Anthropic from '@anthropic-ai/sdk';
|
|
365
|
+
*
|
|
366
|
+
* // Works with OpenAI
|
|
367
|
+
* const openai = wrap(new OpenAI({ apiKey: '...' }), {
|
|
368
|
+
* apiKey: process.env.SCORECARD_API_KEY,
|
|
369
|
+
* projectId: '123'
|
|
370
|
+
* });
|
|
371
|
+
*
|
|
372
|
+
* // Works with Anthropic
|
|
373
|
+
* const claude = wrap(new Anthropic({ apiKey: '...' }), {
|
|
374
|
+
* apiKey: process.env.SCORECARD_API_KEY,
|
|
375
|
+
* projectId: '123'
|
|
376
|
+
* });
|
|
377
|
+
*
|
|
378
|
+
* // Use normally - traces are automatically sent to Scorecard
|
|
379
|
+
* const response = await openai.chat.completions.create({...});
|
|
380
|
+
* const response2 = await claude.messages.create({...});
|
|
381
|
+
* ```
|
|
382
|
+
*/
|
|
383
|
+
export function wrap<T>(client: T, config: WrapConfig = {}): T {
|
|
384
|
+
const projectId = initProvider(config);
|
|
385
|
+
|
|
386
|
+
if (!globalTracer) {
|
|
387
|
+
throw new ScorecardError('Failed to initialize tracer');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const tracer = globalTracer;
|
|
391
|
+
const provider = detectProvider(client);
|
|
392
|
+
|
|
393
|
+
// Track the path to determine if we should wrap this method
|
|
394
|
+
const createHandler = (target: any, path: string[] = []): ProxyHandler<any> => ({
|
|
395
|
+
get(target, prop: string | symbol) {
|
|
396
|
+
const value = target[prop];
|
|
397
|
+
|
|
398
|
+
// Check if this is a method we should wrap based on the path
|
|
399
|
+
const currentPath = [...path, prop.toString()];
|
|
400
|
+
const shouldWrap =
|
|
401
|
+
(provider === 'openai' && currentPath.join('.') === 'chat.completions.create') ||
|
|
402
|
+
(provider === 'anthropic' &&
|
|
403
|
+
(currentPath.join('.') === 'messages.create' || currentPath.join('.') === 'messages.stream'));
|
|
404
|
+
|
|
405
|
+
// Intercept specific LLM methods
|
|
406
|
+
if (shouldWrap && typeof value === 'function') {
|
|
407
|
+
return async function (this: any, ...args: any[]) {
|
|
408
|
+
const params = args[0] || {};
|
|
409
|
+
// Streaming if: 1) stream param is true, or 2) using the 'stream' method
|
|
410
|
+
const isStreaming = params.stream === true || prop === 'stream';
|
|
411
|
+
|
|
412
|
+
// Start span in the current active context (enables nesting)
|
|
413
|
+
const span = tracer.startSpan(`${provider}.request`, {}, context.active());
|
|
414
|
+
|
|
415
|
+
// Set request attributes (common to both providers)
|
|
416
|
+
span.setAttributes({
|
|
417
|
+
'gen_ai.system': provider,
|
|
418
|
+
'gen_ai.request.model': params.model || 'unknown',
|
|
419
|
+
'gen_ai.operation.name': 'chat',
|
|
420
|
+
...(params.temperature !== undefined && { 'gen_ai.request.temperature': params.temperature }),
|
|
421
|
+
...(params.max_tokens !== undefined && { 'gen_ai.request.max_tokens': params.max_tokens }),
|
|
422
|
+
...(params.top_p !== undefined && { 'gen_ai.request.top_p': params.top_p }),
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Store projectId as span attribute - our custom exporter will inject it
|
|
426
|
+
// into ResourceAttributes before export (where the backend expects it)
|
|
427
|
+
if (projectId) {
|
|
428
|
+
span.setAttribute('scorecard.project_id', projectId);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Set prompt messages
|
|
432
|
+
if (params.messages) {
|
|
433
|
+
span.setAttribute('gen_ai.prompt.messages', JSON.stringify(params.messages));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Execute within the span's context (enables nested spans to be children)
|
|
437
|
+
return context.with(trace.setSpan(context.active(), span), async () => {
|
|
438
|
+
try {
|
|
439
|
+
const result = await value.apply(target, args);
|
|
440
|
+
|
|
441
|
+
if (isStreaming) {
|
|
442
|
+
// For streaming, wrap the stream to collect metadata and end span when consumed
|
|
443
|
+
return new StreamWrapper(result, span, provider, params);
|
|
444
|
+
} else {
|
|
445
|
+
// For non-streaming, set response attributes immediately
|
|
446
|
+
if (provider === 'openai') {
|
|
447
|
+
handleOpenAIResponse(span, result, params);
|
|
448
|
+
} else if (provider === 'anthropic') {
|
|
449
|
+
handleAnthropicResponse(span, result, params);
|
|
450
|
+
}
|
|
451
|
+
return result;
|
|
452
|
+
}
|
|
453
|
+
} catch (error: any) {
|
|
454
|
+
span.recordException(error);
|
|
455
|
+
throw error;
|
|
456
|
+
} finally {
|
|
457
|
+
// Only end span for non-streaming (streaming ends in StreamWrapper)
|
|
458
|
+
if (!isStreaming) {
|
|
459
|
+
span.end();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Recursively proxy nested objects, passing the path along
|
|
467
|
+
if (value && typeof value === 'object') {
|
|
468
|
+
return new Proxy(value, createHandler(value, currentPath));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Return functions and primitives as-is
|
|
472
|
+
if (typeof value === 'function') {
|
|
473
|
+
return value.bind(target);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return value;
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
return new Proxy(client, createHandler(client, [])) as T;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Backwards compatibility aliases
|
|
484
|
+
export const wrapOpenAI = wrap;
|
|
485
|
+
export const wrapAnthropic = wrap;
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '
|
|
1
|
+
export const VERSION = '3.0.0-beta.0'; // x-release-please-version
|
package/version.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "
|
|
1
|
+
export declare const VERSION = "3.0.0-beta.0";
|
|
2
2
|
//# sourceMappingURL=version.d.mts.map
|
package/version.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.d.mts","sourceRoot":"","sources":["src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,
|
|
1
|
+
{"version":3,"file":"version.d.mts","sourceRoot":"","sources":["src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,iBAAiB,CAAC"}
|
package/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "
|
|
1
|
+
export declare const VERSION = "3.0.0-beta.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/version.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,iBAAiB,CAAC"}
|
package/version.js
CHANGED
package/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","sourceRoot":"","sources":["src/version.ts"],"names":[],"mappings":";;;AAAa,QAAA,OAAO,GAAG,
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["src/version.ts"],"names":[],"mappings":";;;AAAa,QAAA,OAAO,GAAG,cAAc,CAAC,CAAC,2BAA2B"}
|
package/version.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = '
|
|
1
|
+
export const VERSION = '3.0.0-beta.0'; // x-release-please-version
|
|
2
2
|
//# sourceMappingURL=version.mjs.map
|
package/version.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.mjs","sourceRoot":"","sources":["src/version.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,OAAO,GAAG,
|
|
1
|
+
{"version":3,"file":"version.mjs","sourceRoot":"","sources":["src/version.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,OAAO,GAAG,cAAc,CAAC,CAAC,2BAA2B"}
|