@vfarcic/dot-ai
Version:
AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance
125 lines (124 loc) • 5.67 kB
JavaScript
;
/**
* AI Provider Tracing Utilities
*
* Generic wrapper for instrumenting AI provider calls with OpenTelemetry.
* Uses official GenAI semantic conventions for AI/LLM operations.
*
* Supports:
* - chat operations (sendMessage)
* - tool_loop operations (toolLoop with agentic tool calling)
* - embeddings operations (generateEmbedding, generateEmbeddings)
*
* Reference: https://opentelemetry.io/docs/specs/semconv/gen-ai/
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.withAITracing = withAITracing;
const api_1 = require("@opentelemetry/api");
/**
* Generic wrapper for AI provider calls
*
* Creates CLIENT spans with official gen_ai.* semantic conventions.
* The auto-instrumented HTTP span becomes a child of this span.
*
* @param options AI operation configuration
* @param handler Function that performs the actual AI call
* @param extractMetrics Function to extract metrics from the result
* @returns Result from the handler function
*
* @example Chat operation
* const response = await withAITracing(
* { provider: 'anthropic', model: 'claude-sonnet-4-6', operation: 'chat' },
* async () => await client.messages.create(...),
* (result) => ({ inputTokens: result.usage.input_tokens, outputTokens: result.usage.output_tokens })
* );
*
* @example Tool loop operation
* const result = await withAITracing(
* { provider: 'anthropic', model: 'claude-sonnet-4-6', operation: 'tool_loop' },
* async () => await provider.toolLoop(...),
* (result) => ({ inputTokens: result.totalTokens.input, outputTokens: result.totalTokens.output })
* );
*
* @example Embeddings operation
* const embedding = await withAITracing(
* { provider: 'openai', model: 'text-embedding-3-small', operation: 'embeddings' },
* async () => await client.embeddings.create(...),
* (result) => ({ embeddingCount: 1, embeddingDimensions: 1536 })
* );
*/
async function withAITracing(options, handler, extractMetrics) {
// Get tracer (returns no-op if tracing disabled)
const tracer = api_1.trace.getTracer('dot-ai-mcp');
// Span name format: "{operation} {model}"
// Examples: "chat claude-sonnet-4-6", "tool_loop claude-sonnet-4-6", "embeddings text-embedding-3-small"
const spanName = `${options.operation} ${options.model}`;
return await tracer.startActiveSpan(spanName, {
kind: api_1.SpanKind.CLIENT,
attributes: {
// Required GenAI attributes (per OpenTelemetry spec)
'gen_ai.operation.name': options.operation,
'gen_ai.provider.name': options.provider,
'gen_ai.request.model': options.model,
// Optional request parameters (only for chat/tool_loop)
...(options.maxTokens !== undefined && {
'gen_ai.request.max_tokens': options.maxTokens,
}),
},
}, async (span) => {
const startTime = Date.now();
try {
// Execute the actual AI call within this span's context
// Auto-instrumented HTTP spans will be children of this span
const result = await api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), handler);
// Extract metrics from the result
const metrics = extractMetrics(result);
// Add response model (usually same as request)
span.setAttribute('gen_ai.response.model', options.model);
span.setAttribute('gen_ai.ai.duration_ms', Date.now() - startTime);
// Add operation-specific metrics
if (options.operation === 'chat' || options.operation === 'tool_loop') {
// Token-based metrics for chat/tool_loop operations
if (metrics.inputTokens !== undefined) {
span.setAttribute('gen_ai.usage.input_tokens', metrics.inputTokens);
}
if (metrics.outputTokens !== undefined) {
span.setAttribute('gen_ai.usage.output_tokens', metrics.outputTokens);
}
// Cache metrics (Anthropic-specific for chat operations)
if (metrics.cacheReadTokens !== undefined &&
metrics.cacheReadTokens > 0) {
span.setAttribute('gen_ai.usage.cache_read_tokens', metrics.cacheReadTokens);
}
if (metrics.cacheCreationTokens !== undefined &&
metrics.cacheCreationTokens > 0) {
span.setAttribute('gen_ai.usage.cache_creation_tokens', metrics.cacheCreationTokens);
}
}
else if (options.operation === 'embeddings') {
// Embedding-specific metrics
if (metrics.embeddingCount !== undefined) {
span.setAttribute('gen_ai.embeddings.count', metrics.embeddingCount);
}
if (metrics.embeddingDimensions !== undefined) {
span.setAttribute('gen_ai.embeddings.dimensions', metrics.embeddingDimensions);
}
}
span.setStatus({ code: api_1.SpanStatusCode.OK });
return result;
}
catch (error) {
// Record exception with full details
span.recordException(error);
span.setStatus({
code: api_1.SpanStatusCode.ERROR,
message: error instanceof Error ? error.message : String(error),
});
span.setAttribute('error.type', error instanceof Error ? error.constructor.name : 'unknown');
throw error;
}
finally {
span.end();
}
});
}