diff --git a/lib/instrumentation/aws-sdk/v3/bedrock.js b/lib/instrumentation/aws-sdk/v3/bedrock.js index 341604225c..66e31aa3af 100644 --- a/lib/instrumentation/aws-sdk/v3/bedrock.js +++ b/lib/instrumentation/aws-sdk/v3/bedrock.js @@ -8,7 +8,7 @@ const { LlmChatCompletionMessage, LlmChatCompletionSummary, LlmEmbedding, - LlmError, + LlmErrorMessage, BedrockCommand, BedrockResponse, StreamHandler @@ -174,8 +174,13 @@ function recordChatCompletionMessages({ recordEvent({ agent, type: 'LlmChatCompletionSummary', msg: summary }) if (err) { - const llmError = new LlmError({ bedrockResponse, err, summary }) - agent.errors.add(transaction, err, llmError) + const llmErrorMessage = new LlmErrorMessage( + { response: bedrockResponse, + cause: err, + summary, + useNameAsCode: true } + ) + agent.errors.add(transaction, err, llmErrorMessage) } } @@ -221,8 +226,13 @@ function recordEmbeddingMessage({ } if (err) { - const llmError = new LlmError({ bedrockResponse, err, embedding: embeddings.length === 1 ? embeddings[0] : undefined }) - agent.errors.add(transaction, err, llmError) + const llmErrorMessage = new LlmErrorMessage( + { response: bedrockResponse, + cause: err, + embedding: embeddings.length === 1 ? embeddings[0] : undefined, + useNameAsCode: true } + ) + agent.errors.add(transaction, err, llmErrorMessage) } } diff --git a/lib/llm-events/aws-bedrock/error.js b/lib/llm-events/aws-bedrock/error.js deleted file mode 100644 index 991913f33e..0000000000 --- a/lib/llm-events/aws-bedrock/error.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2024 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -/** - * Represents an error object, to be tracked via `agent.errors`, that is the - * result of some error returned from AWS Bedrock. - */ -module.exports = class LlmError { - /** - * @param {object} params Constructor parameters - * @param {BedrockResponse} [params.bedrockResponse] Instance of an incoming message. - * @param {object} [params.err] AWS error object - * @param {LlmChatCompletionSummary} [params.summary] Details about the - * conversation if it was a chat completion conversation. - * @param {LlmEmbedding} [params.embedding] Details about the conversation - * if it was an embedding conversation. - */ - constructor({ bedrockResponse = {}, err = {}, summary = {}, embedding = {} } = {}) { - this['http.statusCode'] = bedrockResponse.statusCode - this['error.message'] = err.message - this['error.code'] = err.name - this.completion_id = summary.id - this.embedding_id = embedding.id - } -} diff --git a/lib/llm-events/aws-bedrock/index.js b/lib/llm-events/aws-bedrock/index.js index db311521d9..daa6ef15df 100644 --- a/lib/llm-events/aws-bedrock/index.js +++ b/lib/llm-events/aws-bedrock/index.js @@ -12,7 +12,7 @@ module.exports = { LlmChatCompletionSummary: require('./chat-completion-summary'), LlmEmbedding: require('./embedding'), LlmEvent: require('./event'), - LlmError: require('./error'), + LlmErrorMessage: require('../error-message'), StreamHandler: require('./stream-handler'), ConverseStreamHandler: require('./converse-stream-handler') } diff --git a/lib/llm-events/error-message.js b/lib/llm-events/error-message.js index 2f716b792d..535fb14c89 100644 --- a/lib/llm-events/error-message.js +++ b/lib/llm-events/error-message.js @@ -12,8 +12,8 @@ module.exports = class LlmErrorMessage { /** * @param {object} params Constructor parameters - * @param {object} [params.response] Instance of an incoming message. - * @param {object} [params.cause] An instance of the OpenAI error object. + * @param {object} params.response Instance of an incoming message. + * @param {object} params.cause An instance of the LLM error object. * @param {LlmChatCompletionSummary} [params.summary] Details about the * conversation if it was a chat completion conversation. * @param {LlmEmbedding} [params.embedding] Details about the conversation @@ -21,34 +21,46 @@ module.exports = class LlmErrorMessage { * @param {LlmVectorStoreSearch} [params.vectorsearch] Details about the vector * search if it was a vector search event. * @param {LlmTool} [params.tool] Details about the tool event if it was a tool event. + * @param {boolean} [params.useNameAsCode] defaults to false, only Bedrock sets it to true so far */ - constructor({ response, cause, summary, embedding, vectorsearch, tool } = {}) { - // For @google/genai only, cause does not have the `error` or `status` fields, - // but it does have `message` with the info we need. So, we need to parse - // the relevant fields from cause.message to get `status` and `error`. - let parsedError - const isGeminiVendor = embedding?.vendor === 'gemini' || summary?.vendor === 'gemini' - if (isGeminiVendor && cause?.message) { - try { - // Extract the JSON portion of the cause.message - const jsonStartIndex = cause.message.indexOf('{') - const jsonString = cause.message.substring(jsonStartIndex) - parsedError = JSON.parse(jsonString)?.error - } catch { - parsedError = undefined - } - } - this['http.statusCode'] = response?.status ?? cause?.status ?? parsedError?.code + constructor({ response, cause, summary = {}, embedding = {}, vectorsearch = {}, tool = {}, useNameAsCode = false } = {}) { + this['http.statusCode'] = response?.statusCode ?? response?.status ?? cause?.status this['error.message'] = cause?.message - this['error.code'] = response?.code ?? cause?.error?.code ?? parsedError?.code + this['error.code'] = response?.code ?? cause?.error?.code + if (useNameAsCode) { + this['error.code'] = cause?.name + } this['error.param'] = response?.param ?? cause?.error?.param this.completion_id = summary?.id this.embedding_id = embedding?.id this.vector_store_id = vectorsearch?.id this.tool_id = tool?.id + + if (embedding?.vendor === 'gemini' || summary?.vendor === 'gemini') { + this._handleGemini(cause) + } } get [Symbol.toStringTag]() { return 'LlmErrorMessage' } + + /** + * For `@google/genai` only, `cause` does not have the `error` or `status` fields, + * but it does have `message` with the info we need. So, we need to parse + * the relevant fields from cause.message to get `status` and `error`. + * @param {object} cause error object + */ + _handleGemini(cause) { + if (cause?.message) { + try { + const jsonStartIndex = cause.message.indexOf('{') + const jsonString = cause.message.substring(jsonStartIndex) + const parsedError = JSON.parse(jsonString)?.error + + this['http.statusCode'] = parsedError?.code + this['error.code'] = parsedError?.code + } catch { } + } + } } diff --git a/test/unit/llm-events/aws-bedrock/error.test.js b/test/unit/llm-events/aws-bedrock/error.test.js index e589b384ba..095a310675 100644 --- a/test/unit/llm-events/aws-bedrock/error.test.js +++ b/test/unit/llm-events/aws-bedrock/error.test.js @@ -7,15 +7,16 @@ const test = require('node:test') const assert = require('node:assert') -const LlmError = require('../../../../lib/llm-events/aws-bedrock/error') +const LlmErrorMessage = require('#agentlib/llm-events/error-message.js') test.beforeEach((ctx) => { ctx.nr = {} - ctx.nr.bedrockResponse = { + ctx.nr.response = { statusCode: 400 } - ctx.nr.err = { + // The cause of the error. + ctx.nr.cause = { message: 'No soup for you', name: 'SoupRule' } @@ -23,10 +24,12 @@ test.beforeEach((ctx) => { ctx.nr.summary = { id: 'completion-id' } + + ctx.nr.useNameAsCode = true }) test('create creates a new instance', (t) => { - const err = new LlmError(t.nr) + const err = new LlmErrorMessage(t.nr) assert.equal(err['http.statusCode'], 400) assert.equal(err['error.message'], 'No soup for you') assert.equal(err['error.code'], 'SoupRule') @@ -37,7 +40,7 @@ test('create creates a new instance', (t) => { test('create error with embedding_id', (t) => { delete t.nr.summary t.nr.embedding = { id: 'embedding-id' } - const err = new LlmError(t.nr) + const err = new LlmErrorMessage(t.nr) assert.equal(err['http.statusCode'], 400) assert.equal(err['error.message'], 'No soup for you') assert.equal(err['error.code'], 'SoupRule') @@ -46,7 +49,7 @@ test('create error with embedding_id', (t) => { }) test('empty error', () => { - const err = new LlmError() + const err = new LlmErrorMessage() assert.ok(!err['http.statusCode']) assert.ok(!err['error.message']) assert.ok(!err['error.code'])