dd-trace 6.9.0 → 6.11.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/README.md +0 -17
- package/ci/vitest-no-worker-init-setup.mjs +112 -10
- package/index.d.ts +150 -3
- package/initialize.mjs +28 -17
- package/openfeature.js +2 -2
- package/package.json +14 -10
- package/packages/datadog-esbuild/index.js +0 -34
- package/packages/datadog-instrumentations/src/anthropic.js +210 -15
- package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
- package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
- package/packages/datadog-instrumentations/src/bunyan.js +3 -16
- package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
- package/packages/datadog-instrumentations/src/cucumber.js +26 -11
- package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
- package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
- package/packages/datadog-instrumentations/src/cypress.js +2 -0
- package/packages/datadog-instrumentations/src/fastify.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +634 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
- package/packages/datadog-instrumentations/src/jest.js +26 -18
- package/packages/datadog-instrumentations/src/mariadb.js +278 -21
- package/packages/datadog-instrumentations/src/mocha/main.js +540 -56
- package/packages/datadog-instrumentations/src/mocha/utils.js +132 -25
- package/packages/datadog-instrumentations/src/mysql.js +45 -15
- package/packages/datadog-instrumentations/src/mysql2.js +119 -18
- package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
- package/packages/datadog-instrumentations/src/pg.js +116 -92
- package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
- package/packages/datadog-instrumentations/src/playwright.js +149 -2
- package/packages/datadog-instrumentations/src/router.js +2 -0
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
- package/packages/datadog-instrumentations/src/vitest-main.js +180 -20
- package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
- package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
- package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
- package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
- package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +4 -13
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
- package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
- package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
- package/packages/datadog-plugin-bunyan/src/index.js +5 -8
- package/packages/datadog-plugin-cucumber/src/index.js +11 -0
- package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
- package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +82 -30
- package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
- package/packages/datadog-plugin-cypress/src/index.js +12 -6
- package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
- package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
- package/packages/datadog-plugin-electron/src/index.js +2 -0
- package/packages/datadog-plugin-fs/src/index.js +1 -1
- package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
- package/packages/datadog-plugin-jest/src/index.js +11 -4
- package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
- package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
- package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
- package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
- package/packages/datadog-plugin-mocha/src/index.js +606 -40
- package/packages/datadog-plugin-mysql/src/index.js +37 -0
- package/packages/datadog-plugin-nats/src/index.js +1 -1
- package/packages/datadog-plugin-next/src/index.js +1 -19
- package/packages/datadog-plugin-openai/src/tracing.js +41 -30
- package/packages/datadog-plugin-openai-agents/src/integration.js +279 -69
- package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
- package/packages/datadog-plugin-pg/src/index.js +1 -0
- package/packages/datadog-plugin-playwright/src/index.js +42 -13
- package/packages/datadog-plugin-vitest/src/index.js +14 -20
- package/packages/datadog-plugin-ws/src/util.js +2 -1
- package/packages/datadog-webpack/index.js +0 -11
- package/packages/dd-trace/index.js +0 -1
- package/packages/dd-trace/src/agent/info.js +6 -5
- package/packages/dd-trace/src/aiguard/client.js +100 -1
- package/packages/dd-trace/src/aiguard/errors.js +41 -0
- package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
- package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
- package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
- package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
- package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
- package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
- package/packages/dd-trace/src/aiguard/sdk.js +22 -278
- package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
- package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
- package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
- package/packages/dd-trace/src/carrier.js +356 -0
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +22 -6
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
- package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -1
- package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
- package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
- package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +6 -2
- package/packages/dd-trace/src/config/generated-config-types.d.ts +7 -0
- package/packages/dd-trace/src/config/index.js +13 -2
- package/packages/dd-trace/src/config/major-overrides.js +7 -0
- package/packages/dd-trace/src/config/supported-configurations.json +24 -0
- package/packages/dd-trace/src/datastreams/context.js +6 -1
- package/packages/dd-trace/src/datastreams/index.js +2 -0
- package/packages/dd-trace/src/datastreams/manager.js +14 -3
- package/packages/dd-trace/src/datastreams/pathway.js +23 -22
- package/packages/dd-trace/src/datastreams/processor.js +16 -13
- package/packages/dd-trace/src/datastreams/size.js +5 -0
- package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
- package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
- package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
- package/packages/dd-trace/src/exporters/common/writer.js +11 -6
- package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
- package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
- package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
- package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
- package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
- package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
- package/packages/dd-trace/src/llmobs/index.js +34 -2
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
- package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
- package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
- package/packages/dd-trace/src/llmobs/tagger.js +96 -0
- package/packages/dd-trace/src/llmobs/util.js +78 -0
- package/packages/dd-trace/src/noop/proxy.js +3 -4
- package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
- package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
- package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
- package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
- package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
- package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
- package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
- package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
- package/packages/dd-trace/src/opentracing/tracer.js +5 -2
- package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
- package/packages/dd-trace/src/plugin_manager.js +5 -3
- package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +5 -65
- package/packages/dd-trace/src/plugins/util/test.js +93 -14
- package/packages/dd-trace/src/plugins/util/web.js +56 -2
- package/packages/dd-trace/src/profiling/profiler.js +1 -1
- package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
- package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
- package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
- package/packages/dd-trace/src/proxy.js +38 -29
- package/packages/dd-trace/src/ritm.js +9 -2
- package/packages/dd-trace/src/serverless.js +37 -0
- package/packages/dd-trace/src/span_format.js +1 -0
- package/packages/dd-trace/src/span_stats.js +30 -3
- package/packages/dd-trace/src/storage-channels.js +86 -0
- package/packages/dd-trace/src/tracer.js +9 -2
- package/packages/dd-trace/src/tracer_metadata.js +14 -1
- package/packages/dd-trace/src/web-tags-cache.js +132 -0
- package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
- package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
- package/packages/dd-trace/src/feature-registry.js +0 -29
- package/packages/dd-trace/src/openfeature/register.js +0 -35
- package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { channel } = require('dc-polyfill')
|
|
4
|
+
|
|
5
|
+
const log = require('../../log')
|
|
6
|
+
const { getMessagesInputMessages, getMessagesOutputMessages } = require('../messages/anthropic')
|
|
7
|
+
const { SOURCE_AUTO } = require('../tags')
|
|
8
|
+
const { pushEvaluation } = require('./evaluate')
|
|
9
|
+
|
|
10
|
+
const messagesBeforeChannel = channel('dd-trace:anthropic:messages:before')
|
|
11
|
+
const messagesAfterChannel = channel('dd-trace:anthropic:messages:after')
|
|
12
|
+
|
|
13
|
+
let isEnabled = false
|
|
14
|
+
let aiguard
|
|
15
|
+
let opts
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Subscribes AI Guard to Anthropic lifecycle channels.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} aiguardInstance
|
|
21
|
+
* @param {boolean} block
|
|
22
|
+
*/
|
|
23
|
+
function enable (aiguardInstance, block) {
|
|
24
|
+
if (isEnabled) return
|
|
25
|
+
|
|
26
|
+
aiguard = aiguardInstance
|
|
27
|
+
opts = { block, source: SOURCE_AUTO, integration: 'anthropic' }
|
|
28
|
+
|
|
29
|
+
messagesBeforeChannel.subscribe(onMessagesBefore)
|
|
30
|
+
messagesAfterChannel.subscribe(onMessagesAfter)
|
|
31
|
+
|
|
32
|
+
isEnabled = true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function disable () {
|
|
36
|
+
if (!isEnabled) return
|
|
37
|
+
|
|
38
|
+
messagesBeforeChannel.unsubscribe(onMessagesBefore)
|
|
39
|
+
messagesAfterChannel.unsubscribe(onMessagesAfter)
|
|
40
|
+
|
|
41
|
+
aiguard = undefined
|
|
42
|
+
opts = undefined
|
|
43
|
+
isEnabled = false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function onMessagesBefore (ctx) {
|
|
47
|
+
pushEvaluation(ctx, aiguard, getMessagesInputMessages(ctx.args?.[0]), opts)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function onMessagesAfter (ctx) {
|
|
51
|
+
const inputMessages = getMessagesInputMessages(ctx.args?.[0])
|
|
52
|
+
if (!inputMessages?.length) return
|
|
53
|
+
|
|
54
|
+
let body = ctx.body
|
|
55
|
+
if (typeof body === 'string') {
|
|
56
|
+
try {
|
|
57
|
+
body = JSON.parse(body)
|
|
58
|
+
} catch {
|
|
59
|
+
log.error('AIGuard: unable to decode Anthropic response body')
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const outputMessages = getMessagesOutputMessages(body)
|
|
65
|
+
if (!outputMessages.length) return
|
|
66
|
+
|
|
67
|
+
pushEvaluation(ctx, aiguard, [...inputMessages, ...outputMessages], opts)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { enable, disable }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const anthropic = require('./anthropic')
|
|
3
4
|
const openai = require('./openai')
|
|
4
5
|
const vercelAi = require('./vercel-ai')
|
|
5
6
|
|
|
@@ -14,6 +15,7 @@ let isEnabled = false
|
|
|
14
15
|
function enable (aiguard, block) {
|
|
15
16
|
if (isEnabled) return
|
|
16
17
|
|
|
18
|
+
anthropic.enable(aiguard, block)
|
|
17
19
|
openai.enable(aiguard, block)
|
|
18
20
|
vercelAi.enable(aiguard, block)
|
|
19
21
|
|
|
@@ -23,12 +25,14 @@ function enable (aiguard, block) {
|
|
|
23
25
|
function disable () {
|
|
24
26
|
vercelAi.disable()
|
|
25
27
|
openai.disable()
|
|
28
|
+
anthropic.disable()
|
|
26
29
|
isEnabled = false
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
module.exports = {
|
|
30
33
|
enable,
|
|
31
34
|
disable,
|
|
35
|
+
anthropic,
|
|
32
36
|
openai,
|
|
33
37
|
vercelAi,
|
|
34
38
|
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { FILE_FALLBACK, IMAGE_FALLBACK, stringifyOrEmpty } = require('./utils')
|
|
4
|
+
/**
|
|
5
|
+
* Converts an Anthropic image block to an `image_url` content part.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} block
|
|
8
|
+
* @returns {{type: 'image_url', image_url: {url: string}}|undefined}
|
|
9
|
+
*/
|
|
10
|
+
function convertAnthropicImageBlock (block) {
|
|
11
|
+
const source = block.source
|
|
12
|
+
if (!source || typeof source !== 'object') return
|
|
13
|
+
if (source.type === 'url' && typeof source.url === 'string') {
|
|
14
|
+
return { type: 'image_url', image_url: { url: source.url } }
|
|
15
|
+
}
|
|
16
|
+
if (source.type === 'base64' && typeof source.data === 'string' && typeof source.media_type === 'string') {
|
|
17
|
+
return { type: 'image_url', image_url: { url: `data:${source.media_type};base64,${source.data}` } }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extracts the body content from a document block's source.
|
|
23
|
+
* Inline `text` and `content` sources are normalized to their actual text; URL sources return the
|
|
24
|
+
* URL; base64 / unknown sources return undefined (no readable body).
|
|
25
|
+
*
|
|
26
|
+
* @param {object|undefined} source
|
|
27
|
+
* @returns {string|Array<object>|undefined}
|
|
28
|
+
*/
|
|
29
|
+
function extractDocumentSource (source) {
|
|
30
|
+
if (!source) return
|
|
31
|
+
// PlainTextSource stores inline text in `data`, not `text`.
|
|
32
|
+
if (source.type === 'text' && typeof source.data === 'string') return source.data
|
|
33
|
+
if (source.type === 'url' && typeof source.url === 'string') return source.url
|
|
34
|
+
if (source.type === 'content') {
|
|
35
|
+
// ContentBlockSource.content is string | Array<ContentBlockSourceContent>.
|
|
36
|
+
if (typeof source.content === 'string') return source.content
|
|
37
|
+
if (Array.isArray(source.content)) return convertAnthropicBlocksToContent(source.content)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Combines model-visible metadata strings with an extracted body. When the body has images the
|
|
43
|
+
* metadata becomes leading text parts and an array is returned; otherwise the metadata and a string
|
|
44
|
+
* body are newline-joined. Returns undefined when there is nothing to include.
|
|
45
|
+
*
|
|
46
|
+
* @param {Array<string>} metadata
|
|
47
|
+
* @param {string|Array<object>|undefined} body
|
|
48
|
+
* @returns {string|Array<object>|undefined}
|
|
49
|
+
*/
|
|
50
|
+
function combineMetadataWithBody (metadata, body) {
|
|
51
|
+
if (Array.isArray(body)) {
|
|
52
|
+
const parts = metadata.map(text => ({ type: 'text', text }))
|
|
53
|
+
for (const part of body) parts.push(part)
|
|
54
|
+
return parts
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const lines = [...metadata]
|
|
58
|
+
if (typeof body === 'string') lines.push(body)
|
|
59
|
+
return lines.length ? lines.join('\n') : undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extracts text from an Anthropic document block. The model-visible `title` and `context` are
|
|
64
|
+
* combined with the source body so prompt-injections placed in document metadata — not just its
|
|
65
|
+
* content — reach AI Guard. base64 / unknown sources fall back to the metadata or [file].
|
|
66
|
+
*
|
|
67
|
+
* @param {object} block
|
|
68
|
+
* @returns {string|Array<object>}
|
|
69
|
+
*/
|
|
70
|
+
function convertAnthropicDocumentBlock (block) {
|
|
71
|
+
const metadata = []
|
|
72
|
+
if (typeof block.title === 'string' && block.title) metadata.push(block.title)
|
|
73
|
+
if (typeof block.context === 'string' && block.context) metadata.push(block.context)
|
|
74
|
+
|
|
75
|
+
return combineMetadataWithBody(metadata, extractDocumentSource(block.source)) ?? FILE_FALLBACK
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {Array<object>} items
|
|
80
|
+
* @param {string|Array<object>|undefined} content
|
|
81
|
+
*/
|
|
82
|
+
function appendContent (items, content) {
|
|
83
|
+
if (Array.isArray(content)) {
|
|
84
|
+
for (const part of content) items.push(part)
|
|
85
|
+
} else if (content !== undefined) {
|
|
86
|
+
items.push({ type: 'text', text: content })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Walks Anthropic content blocks once and emits normalized content and tool events in source order.
|
|
92
|
+
*
|
|
93
|
+
* `search_result` blocks have their text inside `content: Array<TextBlockParam>`,
|
|
94
|
+
* not a top-level `text` field; they are walked explicitly so RAG-injected text
|
|
95
|
+
* reaches AI Guard for evaluation.
|
|
96
|
+
* `thinking` / `redacted_thinking` are dropped — internal reasoning must not reach
|
|
97
|
+
* AI Guard. Unknown block types fall through to a best-effort `text`-field extraction;
|
|
98
|
+
* purely structural blocks without a `text` field are dropped silently.
|
|
99
|
+
*
|
|
100
|
+
* @param {Array<object>} blocks
|
|
101
|
+
* @returns {Array<object>}
|
|
102
|
+
*/
|
|
103
|
+
function walkContentBlocks (blocks) {
|
|
104
|
+
const items = []
|
|
105
|
+
if (!Array.isArray(blocks)) return items
|
|
106
|
+
|
|
107
|
+
for (const block of blocks) {
|
|
108
|
+
if (!block || typeof block !== 'object') continue
|
|
109
|
+
switch (block.type) {
|
|
110
|
+
case 'text':
|
|
111
|
+
if (typeof block.text === 'string') items.push({ type: 'text', text: block.text })
|
|
112
|
+
break
|
|
113
|
+
case 'image': {
|
|
114
|
+
const image = convertAnthropicImageBlock(block)
|
|
115
|
+
if (image) {
|
|
116
|
+
items.push(image)
|
|
117
|
+
} else {
|
|
118
|
+
items.push({ type: 'text', text: IMAGE_FALLBACK })
|
|
119
|
+
}
|
|
120
|
+
break
|
|
121
|
+
}
|
|
122
|
+
case 'document': {
|
|
123
|
+
appendContent(items, convertAnthropicDocumentBlock(block))
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
case 'tool_use':
|
|
127
|
+
case 'server_tool_use':
|
|
128
|
+
case 'mcp_tool_use':
|
|
129
|
+
// server_tool_use / mcp_tool_use are the built-in- and MCP-tool counterparts of tool_use.
|
|
130
|
+
items.push({
|
|
131
|
+
kind: 'tool_call',
|
|
132
|
+
value: {
|
|
133
|
+
id: block.id ?? block.name,
|
|
134
|
+
function: {
|
|
135
|
+
name: block.name,
|
|
136
|
+
arguments: stringifyOrEmpty(block.input),
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
break
|
|
141
|
+
case 'tool_result':
|
|
142
|
+
items.push({
|
|
143
|
+
kind: 'tool_result',
|
|
144
|
+
value: {
|
|
145
|
+
role: 'tool',
|
|
146
|
+
tool_call_id: block.tool_use_id,
|
|
147
|
+
content: convertAnthropicToolResultContent(block.content),
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
break
|
|
151
|
+
case 'search_result': {
|
|
152
|
+
// title and source are required, model-visible strings; include them so a prompt injection
|
|
153
|
+
// placed in the metadata (not only the content) reaches AI Guard for evaluation.
|
|
154
|
+
const metadata = []
|
|
155
|
+
if (typeof block.title === 'string' && block.title) metadata.push(block.title)
|
|
156
|
+
if (typeof block.source === 'string' && block.source) metadata.push(block.source)
|
|
157
|
+
appendContent(items, combineMetadataWithBody(metadata, convertAnthropicBlocksToContent(block.content)))
|
|
158
|
+
break
|
|
159
|
+
}
|
|
160
|
+
case 'web_fetch_tool_result': {
|
|
161
|
+
// Emit as a tool result (like other *_tool_result blocks) so the call -> result -> final-text
|
|
162
|
+
// timeline is preserved and the fetched document isn't merged into the assistant answer.
|
|
163
|
+
const content = block.content
|
|
164
|
+
const resultContent = content?.type === 'web_fetch_result' && content.content
|
|
165
|
+
? convertAnthropicDocumentBlock(content.content)
|
|
166
|
+
: convertServerToolResultContent(content)
|
|
167
|
+
items.push({
|
|
168
|
+
kind: 'tool_result',
|
|
169
|
+
value: {
|
|
170
|
+
role: 'tool',
|
|
171
|
+
tool_call_id: block.tool_use_id,
|
|
172
|
+
content: resultContent,
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
break
|
|
176
|
+
}
|
|
177
|
+
case 'thinking':
|
|
178
|
+
case 'redacted_thinking':
|
|
179
|
+
break
|
|
180
|
+
default:
|
|
181
|
+
if (typeof block.type === 'string' && block.type.endsWith('_tool_result')) {
|
|
182
|
+
items.push({
|
|
183
|
+
kind: 'tool_result',
|
|
184
|
+
value: {
|
|
185
|
+
role: 'tool',
|
|
186
|
+
tool_call_id: block.tool_use_id,
|
|
187
|
+
content: convertServerToolResultContent(block.content),
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
} else if (typeof block.text === 'string') {
|
|
191
|
+
items.push({ type: 'text', text: block.text })
|
|
192
|
+
}
|
|
193
|
+
break
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return items
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Reduces walker `parts` to normalized message content: a plain string when
|
|
201
|
+
* only text is present, an array of content parts when images are present,
|
|
202
|
+
* or `undefined` when there is nothing to render.
|
|
203
|
+
*
|
|
204
|
+
* @param {Array<object>} parts
|
|
205
|
+
* @param {boolean} hasImages
|
|
206
|
+
* @returns {string|Array<object>|undefined}
|
|
207
|
+
*/
|
|
208
|
+
function partsToContent (parts, hasImages) {
|
|
209
|
+
if (!parts.length) return
|
|
210
|
+
if (hasImages) return parts
|
|
211
|
+
return parts.map(p => p.text).join('\n')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {Array<object>} parts
|
|
216
|
+
* @returns {boolean}
|
|
217
|
+
*/
|
|
218
|
+
function hasImageParts (parts) {
|
|
219
|
+
return parts.some(part => part.type === 'image_url')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Converts Anthropic top-level `system` to a normalized system message.
|
|
224
|
+
*
|
|
225
|
+
* @param {string|Array<object>|undefined} system
|
|
226
|
+
* @returns {{role: 'system', content: string|Array<object>}|undefined}
|
|
227
|
+
*/
|
|
228
|
+
function convertAnthropicSystem (system) {
|
|
229
|
+
if (typeof system === 'string') {
|
|
230
|
+
return system.length ? { role: 'system', content: system } : undefined
|
|
231
|
+
}
|
|
232
|
+
const content = convertAnthropicBlocksToContent(system)
|
|
233
|
+
if (content != null) return { role: 'system', content }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Converts a plain string or array of Anthropic content blocks into normalized message content.
|
|
238
|
+
*
|
|
239
|
+
* @param {string|Array<object>|undefined} blocks
|
|
240
|
+
* @returns {string|Array<object>|undefined}
|
|
241
|
+
*/
|
|
242
|
+
function convertAnthropicBlocksToContent (blocks) {
|
|
243
|
+
if (typeof blocks === 'string') return blocks
|
|
244
|
+
const parts = []
|
|
245
|
+
for (const item of walkContentBlocks(blocks)) {
|
|
246
|
+
if (!item.kind) parts.push(item)
|
|
247
|
+
}
|
|
248
|
+
return partsToContent(parts, hasImageParts(parts))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Converts an Anthropic tool_result block's content into a message content value.
|
|
253
|
+
*
|
|
254
|
+
* @param {string|Array<object>|undefined} content
|
|
255
|
+
* @returns {string|Array<object>}
|
|
256
|
+
*/
|
|
257
|
+
function convertAnthropicToolResultContent (content) {
|
|
258
|
+
return convertAnthropicBlocksToContent(content) ?? stringifyOrEmpty(content)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {unknown} content
|
|
263
|
+
* @returns {string}
|
|
264
|
+
*/
|
|
265
|
+
function convertServerToolResultContent (content) {
|
|
266
|
+
if (typeof content === 'string') return content || '[tool result]'
|
|
267
|
+
if (!content || typeof content !== 'object') return '[tool result]'
|
|
268
|
+
if (typeof content.error_code === 'string') {
|
|
269
|
+
return content.error_message ? `${content.error_code}: ${content.error_message}` : content.error_code
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const lines = []
|
|
273
|
+
if (Array.isArray(content)) {
|
|
274
|
+
for (const item of content) {
|
|
275
|
+
if (!item || typeof item !== 'object') continue
|
|
276
|
+
// text blocks (MCP / generic), then web-search title + url.
|
|
277
|
+
if (typeof item.text === 'string') lines.push(item.text)
|
|
278
|
+
if (typeof item.title === 'string') lines.push(item.title)
|
|
279
|
+
if (typeof item.url === 'string') lines.push(item.url)
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
if (typeof content.stdout === 'string' && content.stdout) lines.push(content.stdout)
|
|
283
|
+
if (typeof content.stderr === 'string' && content.stderr) lines.push(content.stderr)
|
|
284
|
+
if (typeof content.content === 'string' && content.content) lines.push(content.content)
|
|
285
|
+
if (Array.isArray(content.lines)) {
|
|
286
|
+
for (const line of content.lines) {
|
|
287
|
+
if (typeof line === 'string') lines.push(line)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return lines.join('\n') || '[tool result]'
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* @param {Array<object>} messages
|
|
297
|
+
* @param {string} role
|
|
298
|
+
* @param {Array<object>} parts
|
|
299
|
+
* @param {Array<object>} toolCalls
|
|
300
|
+
*/
|
|
301
|
+
function appendPendingMessage (messages, role, parts, toolCalls) {
|
|
302
|
+
const content = partsToContent(parts, hasImageParts(parts))
|
|
303
|
+
if (content == null && !toolCalls.length) return
|
|
304
|
+
|
|
305
|
+
const message = { role }
|
|
306
|
+
if (content != null) message.content = content
|
|
307
|
+
if (toolCalls.length) message.tool_calls = toolCalls
|
|
308
|
+
messages.push(message)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Converts one turn's worth of content blocks (with no mid_conv_system among them) to normalized
|
|
313
|
+
* messages under a single role.
|
|
314
|
+
*
|
|
315
|
+
* Contiguous tool calls are grouped into one assistant message for parallel calls. Each tool result
|
|
316
|
+
* with pending calls flushes those calls first, preserving sequential server-tool cycles. Standalone
|
|
317
|
+
* user-supplied tool results remain ahead of accompanying user text.
|
|
318
|
+
*
|
|
319
|
+
* @param {string} role
|
|
320
|
+
* @param {Array<object>} blocks
|
|
321
|
+
* @returns {Array<object>}
|
|
322
|
+
*/
|
|
323
|
+
function convertContentSegment (role, blocks) {
|
|
324
|
+
const messages = []
|
|
325
|
+
let parts = []
|
|
326
|
+
let toolCalls = []
|
|
327
|
+
|
|
328
|
+
for (const item of walkContentBlocks(blocks)) {
|
|
329
|
+
if (item.kind === 'tool_call') {
|
|
330
|
+
toolCalls.push(item.value)
|
|
331
|
+
} else if (item.kind === 'tool_result') {
|
|
332
|
+
if (toolCalls.length) {
|
|
333
|
+
appendPendingMessage(messages, role, parts, toolCalls)
|
|
334
|
+
parts = []
|
|
335
|
+
toolCalls = []
|
|
336
|
+
}
|
|
337
|
+
messages.push(item.value)
|
|
338
|
+
} else {
|
|
339
|
+
parts.push(item)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
appendPendingMessage(messages, role, parts, toolCalls)
|
|
344
|
+
return messages
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Converts a single Anthropic message to zero or more normalized messages.
|
|
349
|
+
*
|
|
350
|
+
* `mid_conv_system` blocks carry system-level instructions inserted at a point in the turn; each
|
|
351
|
+
* becomes its own `{ role: 'system' }` message in place, so the content around it keeps its role
|
|
352
|
+
* and chronology instead of the instruction being folded into (and mis-scored as) user/assistant
|
|
353
|
+
* text. See {@link convertContentSegment} for how each surrounding segment is converted.
|
|
354
|
+
*
|
|
355
|
+
* @param {{role: string, content: string|Array<object>}} message
|
|
356
|
+
* @returns {Array<object>}
|
|
357
|
+
*/
|
|
358
|
+
function convertAnthropicMessage (message) {
|
|
359
|
+
if (!message || typeof message !== 'object') return []
|
|
360
|
+
const { role, content } = message
|
|
361
|
+
|
|
362
|
+
if (typeof content === 'string') {
|
|
363
|
+
return content.length ? [{ role, content }] : []
|
|
364
|
+
}
|
|
365
|
+
if (!Array.isArray(content)) return []
|
|
366
|
+
|
|
367
|
+
const messages = []
|
|
368
|
+
let segment = []
|
|
369
|
+
for (const block of content) {
|
|
370
|
+
if (block?.type === 'mid_conv_system') {
|
|
371
|
+
for (const converted of convertContentSegment(role, segment)) messages.push(converted)
|
|
372
|
+
segment = []
|
|
373
|
+
const systemContent = convertAnthropicBlocksToContent(block.content)
|
|
374
|
+
if (systemContent != null) messages.push({ role: 'system', content: systemContent })
|
|
375
|
+
} else {
|
|
376
|
+
segment.push(block)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
for (const converted of convertContentSegment(role, segment)) messages.push(converted)
|
|
380
|
+
return messages
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Extracts input messages from an Anthropic `messages.create` call.
|
|
385
|
+
*
|
|
386
|
+
* @param {{system?: string|Array<object>, messages?: Array<object>}|undefined} callArgs
|
|
387
|
+
* @returns {Array<object>|undefined}
|
|
388
|
+
*/
|
|
389
|
+
function getMessagesInputMessages (callArgs) {
|
|
390
|
+
const raw = callArgs?.messages
|
|
391
|
+
if (!Array.isArray(raw)) return
|
|
392
|
+
|
|
393
|
+
const result = []
|
|
394
|
+
const system = convertAnthropicSystem(callArgs.system)
|
|
395
|
+
if (system) result.push(system)
|
|
396
|
+
|
|
397
|
+
for (const message of raw) {
|
|
398
|
+
const converted = convertAnthropicMessage(message)
|
|
399
|
+
for (const m of converted) result.push(m)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return result.length ? result : undefined
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Extracts output messages from an Anthropic `messages.create` parsed response body.
|
|
407
|
+
*
|
|
408
|
+
* @param {{role?: string, content?: Array<object>}|undefined} body
|
|
409
|
+
* @returns {Array<object>}
|
|
410
|
+
*/
|
|
411
|
+
function getMessagesOutputMessages (body) {
|
|
412
|
+
if (!body || typeof body !== 'object') return []
|
|
413
|
+
const role = body.role || 'assistant'
|
|
414
|
+
return convertAnthropicMessage({ role, content: body.content })
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
module.exports = {
|
|
418
|
+
convertAnthropicSystem,
|
|
419
|
+
convertAnthropicBlocksToContent,
|
|
420
|
+
convertAnthropicMessage,
|
|
421
|
+
getMessagesInputMessages,
|
|
422
|
+
getMessagesOutputMessages,
|
|
423
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const FILE_FALLBACK = '
|
|
4
|
-
const IMAGE_FALLBACK = '[image]'
|
|
3
|
+
const { FILE_FALLBACK, IMAGE_FALLBACK, stringifyOrEmpty } = require('./utils')
|
|
5
4
|
|
|
6
5
|
const OPENAI_RESPONSE_TOOL_CALL_TYPES = new Set([
|
|
7
6
|
'apply_patch_call',
|
|
@@ -26,28 +25,6 @@ const OPENAI_RESPONSE_TOOL_OUTPUT_TYPES = new Set([
|
|
|
26
25
|
'shell_call_output',
|
|
27
26
|
])
|
|
28
27
|
|
|
29
|
-
/**
|
|
30
|
-
* Returns the value as a string, JSON-stringifying it when it is not already a string.
|
|
31
|
-
* Returns the value unchanged when it is `null` or `undefined`.
|
|
32
|
-
*
|
|
33
|
-
* @param {unknown} value
|
|
34
|
-
* @returns {string|undefined|null}
|
|
35
|
-
*/
|
|
36
|
-
function stringifyIfNeeded (value) {
|
|
37
|
-
if (value == null) return value
|
|
38
|
-
return typeof value === 'string' ? value : JSON.stringify(value)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Returns a stringified value, falling back to an empty string for absent values.
|
|
43
|
-
*
|
|
44
|
-
* @param {unknown} value
|
|
45
|
-
* @returns {string}
|
|
46
|
-
*/
|
|
47
|
-
function stringifyOrEmpty (value) {
|
|
48
|
-
return stringifyIfNeeded(value) ?? ''
|
|
49
|
-
}
|
|
50
|
-
|
|
51
28
|
/**
|
|
52
29
|
* Converts OpenAI chat-completions messages to the message format expected by AI Guard.
|
|
53
30
|
*
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const FILE_FALLBACK = '[file]'
|
|
4
|
+
const IMAGE_FALLBACK = '[image]'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {unknown} value
|
|
8
|
+
* @returns {string|undefined|null}
|
|
9
|
+
*/
|
|
10
|
+
function stringifyIfNeeded (value) {
|
|
11
|
+
if (value == null) return value
|
|
12
|
+
return typeof value === 'string' ? value : JSON.stringify(value)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {unknown} value
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
function stringifyOrEmpty (value) {
|
|
20
|
+
return stringifyIfNeeded(value) ?? ''
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
FILE_FALLBACK,
|
|
25
|
+
IMAGE_FALLBACK,
|
|
26
|
+
stringifyIfNeeded,
|
|
27
|
+
stringifyOrEmpty,
|
|
28
|
+
}
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
* Returns the value as a string, JSON-stringifying it when it is not already a string.
|
|
5
|
-
* Returns the value unchanged when it is `null` or `undefined`.
|
|
6
|
-
*
|
|
7
|
-
* @param {unknown} value
|
|
8
|
-
* @returns {string|undefined|null}
|
|
9
|
-
*/
|
|
10
|
-
function stringifyIfNeeded (value) {
|
|
11
|
-
if (value == null) return value
|
|
12
|
-
return typeof value === 'string' ? value : JSON.stringify(value)
|
|
13
|
-
}
|
|
3
|
+
const { stringifyIfNeeded } = require('./utils')
|
|
14
4
|
|
|
15
5
|
/**
|
|
16
6
|
* Converts a LanguageModelV2FilePart with an image mediaType to an AI Guard style image_url content part.
|