Path: blob/main/extensions/copilot/src/platform/otel/common/test/byokProviderSpans.spec.ts
13406 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { describe, expect, it } from 'vitest';6import { GenAiAttr, GenAiOperationName } from '../genAiAttributes';7import { emitInferenceDetailsEvent } from '../genAiEvents';8import { SpanKind, SpanStatusCode } from '../otelService';9import { CapturingOTelService } from './capturingOTelService';1011/**12* Tests BYOK-style span emission patterns — verifying that chat spans13* are created with correct kind, attributes, status codes, and that14* content capture is properly gated on config.captureContent.15*16* These validate the instrumentation patterns used in anthropicProvider,17* geminiNativeProvider, and chatMLFetcher.18*/19describe('BYOK Provider Span Emission', () => {20it('creates chat span with CLIENT kind and model attributes', () => {21const otel = new CapturingOTelService();2223const span = otel.startSpan('chat claude-sonnet-4-20250514', {24kind: SpanKind.CLIENT,25attributes: {26[GenAiAttr.OPERATION_NAME]: GenAiOperationName.CHAT,27[GenAiAttr.PROVIDER_NAME]: 'anthropic',28[GenAiAttr.REQUEST_MODEL]: 'claude-sonnet-4-20250514',29[GenAiAttr.AGENT_NAME]: 'AnthropicBYOK',30},31});32span.setAttributes({33[GenAiAttr.USAGE_INPUT_TOKENS]: 2000,34[GenAiAttr.USAGE_OUTPUT_TOKENS]: 500,35[GenAiAttr.RESPONSE_MODEL]: 'claude-sonnet-4-20250514',36[GenAiAttr.RESPONSE_ID]: 'msg_abc123',37});38span.setStatus(SpanStatusCode.OK);39span.end();4041expect(otel.spans).toHaveLength(1);42const s = otel.spans[0];43expect(s.kind).toBe(SpanKind.CLIENT);44expect(s.attributes[GenAiAttr.OPERATION_NAME]).toBe('chat');45expect(s.attributes[GenAiAttr.PROVIDER_NAME]).toBe('anthropic');46expect(s.attributes[GenAiAttr.USAGE_INPUT_TOKENS]).toBe(2000);47expect(s.attributes[GenAiAttr.USAGE_OUTPUT_TOKENS]).toBe(500);48expect(s.statusCode).toBe(SpanStatusCode.OK);49expect(s.ended).toBe(true);50});5152it('sets ERROR status and error.type on failure', () => {53const otel = new CapturingOTelService();5455const span = otel.startSpan('chat gemini-2.0-flash', {56kind: SpanKind.CLIENT,57attributes: {58[GenAiAttr.OPERATION_NAME]: GenAiOperationName.CHAT,59[GenAiAttr.PROVIDER_NAME]: 'gemini',60[GenAiAttr.REQUEST_MODEL]: 'gemini-2.0-flash',61},62});63span.setStatus(SpanStatusCode.ERROR, 'Rate limit exceeded');64span.setAttribute('error.type', 'RateLimitError');65span.recordException(new Error('Rate limit exceeded'));66span.end();6768const s = otel.spans[0];69expect(s.statusCode).toBe(SpanStatusCode.ERROR);70expect(s.statusMessage).toBe('Rate limit exceeded');71expect(s.attributes['error.type']).toBe('RateLimitError');72expect(s.exceptions).toHaveLength(1);73});7475it('does NOT capture content when captureContent is false', () => {76const otel = new CapturingOTelService({ captureContent: false });7778// Simulate the input capture gating pattern used in BYOK providers79const span = otel.startSpan('chat gpt-4o', { kind: SpanKind.CLIENT, attributes: {} });80if (otel.config.captureContent) {81span.setAttribute(GenAiAttr.INPUT_MESSAGES, 'should not appear');82}83span.end();8485expect(otel.spans[0].attributes[GenAiAttr.INPUT_MESSAGES]).toBeUndefined();86});8788it('captures content when captureContent is true', () => {89const otel = new CapturingOTelService({ captureContent: true });9091const span = otel.startSpan('chat gpt-4o', { kind: SpanKind.CLIENT, attributes: {} });92if (otel.config.captureContent) {93span.setAttribute(GenAiAttr.INPUT_MESSAGES, '[{"role":"user","parts":[{"type":"text","content":"hello"}]}]');94span.setAttribute(GenAiAttr.OUTPUT_MESSAGES, '[{"role":"assistant","parts":[{"type":"text","content":"hi"}]}]');95}96span.end();9798expect(otel.spans[0].attributes[GenAiAttr.INPUT_MESSAGES]).toBeDefined();99expect(otel.spans[0].attributes[GenAiAttr.OUTPUT_MESSAGES]).toBeDefined();100});101102it('emits inference details event with request/response data', () => {103const otel = new CapturingOTelService();104105emitInferenceDetailsEvent(106otel,107{ model: 'claude-sonnet-4-20250514', temperature: 0.1, maxTokens: 4096 },108{ id: 'msg_123', model: 'claude-sonnet-4-20250514', finishReasons: ['stop'], inputTokens: 2000, outputTokens: 500 },109);110111expect(otel.logRecords).toHaveLength(1);112const attrs = otel.logRecords[0].attributes!;113expect(attrs['event.name']).toBe('gen_ai.client.inference.operation.details');114expect(attrs[GenAiAttr.REQUEST_MODEL]).toBe('claude-sonnet-4-20250514');115expect(attrs[GenAiAttr.USAGE_INPUT_TOKENS]).toBe(2000);116expect(attrs[GenAiAttr.USAGE_OUTPUT_TOKENS]).toBe(500);117});118119it('uses parentTraceContext for CAPI → BYOK trace linking', () => {120const otel = new CapturingOTelService();121const parentCtx = { traceId: '11112222333344445555666677778888', spanId: 'aabbccddeeff0011' };122123const span = otel.startSpan('chat gpt-4o', {124kind: SpanKind.CLIENT,125attributes: { [GenAiAttr.OPERATION_NAME]: GenAiOperationName.CHAT },126parentTraceContext: parentCtx,127});128span.end();129130expect(otel.spans[0].parentTraceContext).toEqual(parentCtx);131});132});133134135