diff --git a/packages/core/src/tracing/anthropic-ai/streaming.ts b/packages/core/src/tracing/anthropic-ai/streaming.ts index 86f5c25baa8a..940ec53e8030 100644 --- a/packages/core/src/tracing/anthropic-ai/streaming.ts +++ b/packages/core/src/tracing/anthropic-ai/streaming.ts @@ -11,6 +11,7 @@ import { } from '../ai/gen-ai-attributes'; import { setTokenUsageAttributes } from '../ai/utils'; import type { AnthropicAiStreamingEvent } from './types'; +import { mapAnthropicErrorToStatusMessage } from './utils'; /** * State object used to accumulate information from a stream of Anthropic AI events. @@ -59,7 +60,7 @@ function isErrorEvent(event: AnthropicAiStreamingEvent, span: Span): boolean { // If the event is an error, set the span status and capture the error // These error events are not rejected by the API by default, but are sent as metadata of the response if (event.type === 'error') { - span.setStatus({ code: SPAN_STATUS_ERROR, message: event.error?.type ?? 'internal_error' }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: mapAnthropicErrorToStatusMessage(event.error?.type) }); captureException(event.error, { mechanism: { handled: false, @@ -377,7 +378,7 @@ export function instrumentMessageStream }); if (span.isRecording()) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'stream_error' }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); span.end(); } }); diff --git a/packages/core/src/tracing/anthropic-ai/utils.ts b/packages/core/src/tracing/anthropic-ai/utils.ts index b9cf31b4aeea..e2cadcec331b 100644 --- a/packages/core/src/tracing/anthropic-ai/utils.ts +++ b/packages/core/src/tracing/anthropic-ai/utils.ts @@ -1,6 +1,7 @@ import { captureException } from '../../exports'; import { SPAN_STATUS_ERROR } from '../../tracing'; import type { Span } from '../../types-hoist/span'; +import type { SpanStatusType } from '../../types-hoist/spanStatus'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, @@ -41,13 +42,35 @@ export function setMessagesAttribute(span: Span, messages: unknown): void { }); } +const ANTHROPIC_ERROR_TYPE_TO_SPAN_STATUS: Record = { + invalid_request_error: 'invalid_argument', + authentication_error: 'unauthenticated', + permission_error: 'permission_denied', + not_found_error: 'not_found', + request_too_large: 'failed_precondition', + rate_limit_error: 'resource_exhausted', + api_error: 'internal_error', + overloaded_error: 'unavailable', +}; + +/** + * Map an Anthropic API error type to a SpanStatusType value. + * @see https://docs.anthropic.com/en/api/errors#error-shapes + */ +export function mapAnthropicErrorToStatusMessage(errorType: string | undefined): SpanStatusType { + if (!errorType) { + return 'internal_error'; + } + return ANTHROPIC_ERROR_TYPE_TO_SPAN_STATUS[errorType] || 'internal_error'; +} + /** * Capture error information from the response * @see https://docs.anthropic.com/en/api/errors#error-shapes */ export function handleResponseError(span: Span, response: AnthropicAiResponse): void { if (response.error) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: response.error.type || 'internal_error' }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: mapAnthropicErrorToStatusMessage(response.error.type) }); captureException(response.error, { mechanism: { diff --git a/packages/core/src/tracing/google-genai/streaming.ts b/packages/core/src/tracing/google-genai/streaming.ts index b9462e8c90dd..d3f6598b8fd7 100644 --- a/packages/core/src/tracing/google-genai/streaming.ts +++ b/packages/core/src/tracing/google-genai/streaming.ts @@ -46,7 +46,7 @@ function isErrorChunk(chunk: GoogleGenAIResponse, span: Span): boolean { const feedback = chunk?.promptFeedback; if (feedback?.blockReason) { const message = feedback.blockReasonMessage ?? feedback.blockReason; - span.setStatus({ code: SPAN_STATUS_ERROR, message: `Content blocked: ${message}` }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); captureException(`Content blocked: ${message}`, { mechanism: { handled: false, type: 'auto.ai.google_genai' }, }); diff --git a/packages/core/src/tracing/langchain/index.ts b/packages/core/src/tracing/langchain/index.ts index 54b581af9f2d..0984131e4296 100644 --- a/packages/core/src/tracing/langchain/index.ts +++ b/packages/core/src/tracing/langchain/index.ts @@ -171,7 +171,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): handleLLMError(error: Error, runId: string) { const span = spanMap.get(runId); if (span?.isRecording()) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'llm_error' }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); exitSpan(runId); } @@ -239,7 +239,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): handleChainError(error: Error, runId: string) { const span = spanMap.get(runId); if (span?.isRecording()) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'chain_error' }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); exitSpan(runId); } @@ -298,7 +298,7 @@ export function createLangChainCallbackHandler(options: LangChainOptions = {}): handleToolError(error: Error, runId: string) { const span = spanMap.get(runId); if (span?.isRecording()) { - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'tool_error' }); + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); exitSpan(runId); } diff --git a/packages/core/src/types-hoist/spanStatus.ts b/packages/core/src/types-hoist/spanStatus.ts index 8347d37adb1a..151530b5cb69 100644 --- a/packages/core/src/types-hoist/spanStatus.ts +++ b/packages/core/src/types-hoist/spanStatus.ts @@ -1,4 +1,4 @@ -type SpanStatusType = +export type SpanStatusType = /** The operation completed successfully. */ | 'ok' /** Deadline expired before operation could complete. */ diff --git a/packages/core/test/lib/utils/anthropic-utils.test.ts b/packages/core/test/lib/utils/anthropic-utils.test.ts index 797bb9bc8186..b912af40e35e 100644 --- a/packages/core/test/lib/utils/anthropic-utils.test.ts +++ b/packages/core/test/lib/utils/anthropic-utils.test.ts @@ -1,8 +1,34 @@ import { describe, expect, it } from 'vitest'; -import { messagesFromParams, setMessagesAttribute, shouldInstrument } from '../../../src/tracing/anthropic-ai/utils'; +import { + mapAnthropicErrorToStatusMessage, + messagesFromParams, + setMessagesAttribute, + shouldInstrument, +} from '../../../src/tracing/anthropic-ai/utils'; import type { Span } from '../../../src/types-hoist/span'; describe('anthropic-ai-utils', () => { + describe('mapAnthropicErrorToStatusMessage', () => { + it('maps known Anthropic error types to SpanStatusType values', () => { + expect(mapAnthropicErrorToStatusMessage('invalid_request_error')).toBe('invalid_argument'); + expect(mapAnthropicErrorToStatusMessage('authentication_error')).toBe('unauthenticated'); + expect(mapAnthropicErrorToStatusMessage('permission_error')).toBe('permission_denied'); + expect(mapAnthropicErrorToStatusMessage('not_found_error')).toBe('not_found'); + expect(mapAnthropicErrorToStatusMessage('request_too_large')).toBe('failed_precondition'); + expect(mapAnthropicErrorToStatusMessage('rate_limit_error')).toBe('resource_exhausted'); + expect(mapAnthropicErrorToStatusMessage('api_error')).toBe('internal_error'); + expect(mapAnthropicErrorToStatusMessage('overloaded_error')).toBe('unavailable'); + }); + + it('falls back to internal_error for unknown error types', () => { + expect(mapAnthropicErrorToStatusMessage('some_new_error')).toBe('internal_error'); + }); + + it('falls back to internal_error for undefined', () => { + expect(mapAnthropicErrorToStatusMessage(undefined)).toBe('internal_error'); + }); + }); + describe('shouldInstrument', () => { it('should instrument known methods', () => { expect(shouldInstrument('models.get')).toBe(true);