dd-trace 5.108.0 → 5.109.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/index.d.ts +22 -1
- package/package.json +2 -1
- package/packages/datadog-instrumentations/src/ai.js +43 -48
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js-context-methods.js +18 -0
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +111 -0
- package/packages/datadog-instrumentations/src/aws-sdk.js +3 -1
- package/packages/datadog-instrumentations/src/electron.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/aws-durable-execution-sdk-js.js +31 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/http/client.js +12 -2
- package/packages/datadog-instrumentations/src/ioredis.js +0 -1
- package/packages/datadog-instrumentations/src/iovalkey.js +1 -2
- package/packages/datadog-instrumentations/src/next.js +34 -0
- package/packages/datadog-instrumentations/src/openai.js +77 -18
- package/packages/datadog-instrumentations/src/redis.js +0 -1
- package/packages/datadog-instrumentations/src/vitest.js +60 -1
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/checkpoint.js +31 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +55 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +114 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +128 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/index.js +19 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +224 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +43 -0
- package/packages/datadog-plugin-aws-sdk/src/base.js +1 -7
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +100 -37
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +44 -27
- package/packages/datadog-plugin-bullmq/src/filter.js +35 -0
- package/packages/datadog-plugin-bullmq/src/producer.js +84 -4
- package/packages/datadog-plugin-fs/src/index.js +1 -0
- package/packages/datadog-plugin-redis/src/index.js +1 -2
- package/packages/datadog-plugin-vitest/src/index.js +4 -1
- package/packages/dd-trace/src/aiguard/channels.js +0 -1
- package/packages/dd-trace/src/aiguard/index.js +11 -49
- package/packages/dd-trace/src/aiguard/integrations/evaluate.js +46 -0
- package/packages/dd-trace/src/aiguard/integrations/openai.js +66 -0
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +78 -0
- package/packages/{datadog-instrumentations/src/helpers/ai-messages.js → dd-trace/src/aiguard/messages/openai.js} +85 -193
- package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +185 -0
- package/packages/dd-trace/src/appsec/channels.js +1 -0
- package/packages/dd-trace/src/appsec/downstream_requests.js +111 -58
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +54 -12
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +5 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +29 -4
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +19 -11
- package/packages/dd-trace/src/config/generated-config-types.d.ts +3 -0
- package/packages/dd-trace/src/config/supported-configurations.json +24 -2
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -0
- package/packages/dd-trace/src/dogstatsd.js +15 -8
- package/packages/dd-trace/src/exporters/agentless/index.js +7 -5
- package/packages/dd-trace/src/exporters/agentless/intake.js +43 -0
- package/packages/dd-trace/src/exporters/agentless/writer.js +5 -4
- package/packages/dd-trace/src/openfeature/flagging_provider.js +8 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +27 -2
- package/packages/dd-trace/src/plugins/index.js +3 -0
- package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
- package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +0 -284
|
@@ -7,6 +7,7 @@ const tracerVersion = require('../../../../../package.json').version
|
|
|
7
7
|
|
|
8
8
|
const BaseWriter = require('../common/writer')
|
|
9
9
|
const { AgentlessJSONEncoder } = require('../../encode/agentless-json')
|
|
10
|
+
const { computeIntakeUrl, INTAKE_PATH } = require('./intake')
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Writer for agentless APM trace intake.
|
|
@@ -28,7 +29,7 @@ class AgentlessWriter extends BaseWriter {
|
|
|
28
29
|
|
|
29
30
|
if (!url) {
|
|
30
31
|
try {
|
|
31
|
-
this._url = new URL(
|
|
32
|
+
this._url = new URL(computeIntakeUrl(site))
|
|
32
33
|
} catch (err) {
|
|
33
34
|
log.error(
|
|
34
35
|
'Invalid site value for agentless intake: %s. Cannot construct URL. Error: %s',
|
|
@@ -121,7 +122,7 @@ class AgentlessWriter extends BaseWriter {
|
|
|
121
122
|
this.#apiKeyMissing = false
|
|
122
123
|
|
|
123
124
|
const options = {
|
|
124
|
-
path:
|
|
125
|
+
path: INTAKE_PATH,
|
|
125
126
|
method: 'POST',
|
|
126
127
|
headers: {
|
|
127
128
|
'Content-Type': 'application/json',
|
|
@@ -140,7 +141,7 @@ class AgentlessWriter extends BaseWriter {
|
|
|
140
141
|
|
|
141
142
|
request(data, options, (err, res, statusCode) => {
|
|
142
143
|
if (err) {
|
|
143
|
-
this
|
|
144
|
+
this.#logRequestError(err, statusCode, count)
|
|
144
145
|
done()
|
|
145
146
|
return
|
|
146
147
|
}
|
|
@@ -156,7 +157,7 @@ class AgentlessWriter extends BaseWriter {
|
|
|
156
157
|
* @param {number} statusCode - HTTP status code (if available)
|
|
157
158
|
* @param {number} count - Number of traces that were being sent
|
|
158
159
|
*/
|
|
159
|
-
|
|
160
|
+
#logRequestError (err, statusCode, count) {
|
|
160
161
|
if (statusCode === 401 || statusCode === 403) {
|
|
161
162
|
log.error(
|
|
162
163
|
'Authentication failed sending %d trace(s) (status %s). Verify DD_API_KEY is valid.',
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { DatadogNodeServerProvider } = require('@datadog/openfeature-node-server')
|
|
4
3
|
const { channel } = require('dc-polyfill')
|
|
5
4
|
const log = require('../log')
|
|
6
5
|
const { EXPOSURE_CHANNEL } = require('./constants/constants')
|
|
7
6
|
const EvalMetricsHook = require('./eval-metrics-hook')
|
|
8
7
|
const SpanEnrichmentHook = require('./span-enrichment-hook')
|
|
9
8
|
|
|
9
|
+
// Bundler-opaque require for the optional peer chain
|
|
10
|
+
// `@datadog/openfeature-node-server` -> `@openfeature/server-sdk` ->
|
|
11
|
+
// `@openfeature/core`. Same shape as `helpers/rewriter/compiler.js`.
|
|
12
|
+
// Refs: https://github.com/DataDog/dd-trace-js/issues/8635
|
|
13
|
+
// eslint-disable-next-line camelcase, no-undef
|
|
14
|
+
const runtimeRequire = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require
|
|
15
|
+
const { DatadogNodeServerProvider } = runtimeRequire(['@datadog/openfeature', 'node', 'server'].join('-'))
|
|
16
|
+
|
|
10
17
|
/**
|
|
11
18
|
* OpenFeature provider that integrates with Datadog's feature flagging system.
|
|
12
19
|
* Extends DatadogNodeServerProvider to add tracer integration and configuration management.
|
|
@@ -442,6 +442,28 @@ module.exports = class CiPlugin extends Plugin {
|
|
|
442
442
|
}
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Updates repository-root-dependent state when a worker receives the root from
|
|
447
|
+
* the coordinator process after plugin configuration.
|
|
448
|
+
*
|
|
449
|
+
* @param {string|undefined} repositoryRoot - Repository root discovered by the coordinator process.
|
|
450
|
+
* @param {Array<{ pattern: string, owners: string[] }>|null|undefined} codeOwnersEntries
|
|
451
|
+
* Parsed CODEOWNERS entries discovered by the coordinator process.
|
|
452
|
+
* @returns {void}
|
|
453
|
+
*/
|
|
454
|
+
_setRepositoryRoot (repositoryRoot, codeOwnersEntries) {
|
|
455
|
+
if (codeOwnersEntries !== undefined) {
|
|
456
|
+
this.codeOwnersEntries = codeOwnersEntries
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (!repositoryRoot || repositoryRoot === this.repositoryRoot) return
|
|
460
|
+
|
|
461
|
+
this.repositoryRoot = repositoryRoot
|
|
462
|
+
if (codeOwnersEntries === undefined) {
|
|
463
|
+
this.codeOwnersEntries = getCodeOwnersFileEntries(this.repositoryRoot)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
445
467
|
/**
|
|
446
468
|
* Returns request error tags from the test session span for propagation to module, suite and test spans.
|
|
447
469
|
* @returns {Record<string, string>}
|
|
@@ -610,6 +632,8 @@ module.exports = class CiPlugin extends Plugin {
|
|
|
610
632
|
const workerTestFramework = WORKER_EXPORTER_TO_TEST_FRAMEWORK[exporter]
|
|
611
633
|
this.shouldSkipGitMetadataExtraction = workerTestFramework &&
|
|
612
634
|
TEST_FRAMEWORKS_TO_SKIP_GIT_METADATA_EXTRACTION.has(workerTestFramework)
|
|
635
|
+
const shouldDeferCodeOwnersEntries = workerTestFramework === 'vitest'
|
|
636
|
+
const shouldDeferRepositoryRoot = workerTestFramework === 'vitest'
|
|
613
637
|
|
|
614
638
|
this.testEnvironmentMetadata = getTestEnvironmentMetadata(
|
|
615
639
|
this.constructor.id,
|
|
@@ -635,9 +659,10 @@ module.exports = class CiPlugin extends Plugin {
|
|
|
635
659
|
[GIT_COMMIT_HEAD_MESSAGE]: commitHeadMessage,
|
|
636
660
|
} = this.testEnvironmentMetadata
|
|
637
661
|
|
|
638
|
-
this.repositoryRoot = repositoryRoot ||
|
|
662
|
+
this.repositoryRoot = repositoryRoot ||
|
|
663
|
+
(shouldDeferRepositoryRoot ? process.cwd() : getRepositoryRoot() || process.cwd())
|
|
639
664
|
|
|
640
|
-
this.codeOwnersEntries = getCodeOwnersFileEntries(this.repositoryRoot)
|
|
665
|
+
this.codeOwnersEntries = shouldDeferCodeOwnersEntries ? null : getCodeOwnersFileEntries(this.repositoryRoot)
|
|
641
666
|
|
|
642
667
|
this.ciProviderName = ciProviderName
|
|
643
668
|
|
|
@@ -8,6 +8,7 @@ const plugins = {
|
|
|
8
8
|
get '@azure/event-hubs' () { return require('../../../datadog-plugin-azure-event-hubs/src') },
|
|
9
9
|
get '@azure/functions' () { return require('../../../datadog-plugin-azure-functions/src') },
|
|
10
10
|
get '@modelcontextprotocol/sdk' () { return require('../../../datadog-plugin-modelcontextprotocol-sdk/src') },
|
|
11
|
+
get '@aws/durable-execution-sdk-js' () { return require('../../../datadog-plugin-aws-durable-execution-sdk-js/src') },
|
|
11
12
|
get 'durable-functions' () { return require('../../../datadog-plugin-azure-durable-functions/src') },
|
|
12
13
|
get '@azure/service-bus' () { return require('../../../datadog-plugin-azure-service-bus/src') },
|
|
13
14
|
get '@cucumber/cucumber' () { return require('../../../datadog-plugin-cucumber/src') },
|
|
@@ -55,6 +56,7 @@ const plugins = {
|
|
|
55
56
|
get express () { return require('../../../datadog-plugin-express/src') },
|
|
56
57
|
get fastify () { return require('../../../datadog-plugin-fastify/src') },
|
|
57
58
|
get 'find-my-way' () { return require('../../../datadog-plugin-find-my-way/src') },
|
|
59
|
+
get fs () { return require('../../../datadog-plugin-fs/src') },
|
|
58
60
|
get 'global:fetch' () { return require('../../../datadog-plugin-fetch/src') },
|
|
59
61
|
get graphql () { return require('../../../datadog-plugin-graphql/src') },
|
|
60
62
|
get grpc () { return require('../../../datadog-plugin-grpc/src') },
|
|
@@ -97,6 +99,7 @@ const plugins = {
|
|
|
97
99
|
get net () { return require('../../../datadog-plugin-net/src') },
|
|
98
100
|
get next () { return require('../../../datadog-plugin-next/src') },
|
|
99
101
|
get 'node:dns' () { return require('../../../datadog-plugin-dns/src') },
|
|
102
|
+
get 'node:fs' () { return require('../../../datadog-plugin-fs/src') },
|
|
100
103
|
get 'node:http' () { return require('../../../datadog-plugin-http/src') },
|
|
101
104
|
get 'node:http2' () { return require('../../../datadog-plugin-http2/src') },
|
|
102
105
|
get 'node:https' () { return require('../../../datadog-plugin-http/src') },
|
|
@@ -13,6 +13,18 @@ const serverless = {
|
|
|
13
13
|
serviceName: identityService,
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
|
+
client: {
|
|
17
|
+
'aws-durable-execution-sdk-js': {
|
|
18
|
+
opName: () => 'aws.durable.invoke',
|
|
19
|
+
serviceName: identityService,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
internal: {
|
|
23
|
+
'aws-durable-execution-sdk-js': {
|
|
24
|
+
opName: () => 'aws.durable.execute',
|
|
25
|
+
serviceName: identityService,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
16
28
|
}
|
|
17
29
|
|
|
18
30
|
module.exports = serverless
|
|
@@ -13,6 +13,18 @@ const serverless = {
|
|
|
13
13
|
serviceName: identityService,
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
|
+
client: {
|
|
17
|
+
'aws-durable-execution-sdk-js': {
|
|
18
|
+
opName: () => 'aws.durable.invoke',
|
|
19
|
+
serviceName: identityService,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
internal: {
|
|
23
|
+
'aws-durable-execution-sdk-js': {
|
|
24
|
+
opName: () => 'aws.durable.execute',
|
|
25
|
+
serviceName: identityService,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
16
28
|
}
|
|
17
29
|
|
|
18
30
|
module.exports = serverless
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const dc = require('dc-polyfill')
|
|
4
|
-
const shimmer = require('../../../datadog-shimmer')
|
|
5
|
-
const {
|
|
6
|
-
convertOpenAIResponseItemsToMessages,
|
|
7
|
-
convertOpenAIResponsePromptToMessages,
|
|
8
|
-
normalizeOpenAIChatMessages,
|
|
9
|
-
} = require('./ai-messages')
|
|
10
|
-
|
|
11
|
-
// TODO: this channel name is incorrect, instrumentations publish with THEIR name, not with their subscribers names.
|
|
12
|
-
const aiguardChannel = dc.channel('dd-trace:ai:aiguard')
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @typedef {object} ResourceHandler
|
|
16
|
-
* @property {(callArgs: object) => (Array<object>|undefined)} getInputMessages
|
|
17
|
-
* @property {(body: object) => Array<object>} getOutputMessages
|
|
18
|
-
* @property {(inputMessages: Array<object>, outputMessages: Array<object>, parentSpan?: object)
|
|
19
|
-
* => Promise<unknown>} publishOutputEvaluation
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @typedef {object} Guard
|
|
24
|
-
* @property {ResourceHandler} handler
|
|
25
|
-
* @property {Array<object>} inputMessages
|
|
26
|
-
* @property {() => Promise<void>} getInputEval
|
|
27
|
-
* @property {object} [parentSpan] - LLM span (`openai.request`) to nest `ai_guard` spans under.
|
|
28
|
-
* Set by the instrumentation once the LLM span is active.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Publishes already-converted AI-style messages to the AI Guard evaluation channel.
|
|
33
|
-
*
|
|
34
|
-
* Subscribers push async work into `pending` and abort `abortController` to block.
|
|
35
|
-
*
|
|
36
|
-
* @param {Array<object>} messages - AI-style messages to evaluate.
|
|
37
|
-
* @param {object} [parentSpan] - LLM span to use as the `ai_guard` span's parent.
|
|
38
|
-
* @returns {Promise<void>}
|
|
39
|
-
*/
|
|
40
|
-
function publishEvaluation (messages, parentSpan) {
|
|
41
|
-
const abortController = new AbortController()
|
|
42
|
-
const ctx = { messages, integration: 'openai', parentSpan, abortController, pending: [] }
|
|
43
|
-
|
|
44
|
-
aiguardChannel.publish(ctx)
|
|
45
|
-
|
|
46
|
-
return Promise.all(ctx.pending).then(() => {
|
|
47
|
-
if (abortController.signal.aborted) {
|
|
48
|
-
throw abortController.signal.reason
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Extracts OpenAI input messages from a `chat.completions.create` call.
|
|
55
|
-
*
|
|
56
|
-
* @param {object} callArgs - First argument passed to the wrapped method
|
|
57
|
-
* @returns {Array<object>|undefined}
|
|
58
|
-
*/
|
|
59
|
-
function getChatCompletionsInputMessages (callArgs) {
|
|
60
|
-
return normalizeOpenAIChatMessages(callArgs?.messages)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Extracts OpenAI output messages from a `chat.completions.create` parsed body.
|
|
65
|
-
* Includes any choice whose message carries content (including empty string),
|
|
66
|
-
* `tool_calls`, a `refusal` field, or the deprecated `function_call` field. GPT-4o
|
|
67
|
-
* emits `{content: null, refusal: "..."}` on policy refusals, and pre-tool-call
|
|
68
|
-
* SDK paths still produce `function_call`-only output — AI Guard must still see them.
|
|
69
|
-
*
|
|
70
|
-
* @param {object} body - Parsed response body
|
|
71
|
-
* @returns {Array<object>}
|
|
72
|
-
*/
|
|
73
|
-
function getChatCompletionsOutputMessages (body) {
|
|
74
|
-
const eligible = []
|
|
75
|
-
const choices = Array.isArray(body?.choices) ? body.choices : []
|
|
76
|
-
for (const choice of choices) {
|
|
77
|
-
const message = choice?.message
|
|
78
|
-
if (
|
|
79
|
-
message?.content != null ||
|
|
80
|
-
message?.tool_calls?.length ||
|
|
81
|
-
message?.refusal != null ||
|
|
82
|
-
message?.function_call != null
|
|
83
|
-
) {
|
|
84
|
-
eligible.push(message)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return normalizeOpenAIChatMessages(eligible) ?? []
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Publishes AI Guard After Model evaluation for `chat.completions` output.
|
|
92
|
-
*
|
|
93
|
-
* Chat completions may return multiple choices when `n > 1`. Screen every choice
|
|
94
|
-
* concurrently so any unsafe assistant output rejects `.parse()`, regardless of
|
|
95
|
-
* which choice the caller ends up using.
|
|
96
|
-
*
|
|
97
|
-
* @param {Array<object>} inputMessages
|
|
98
|
-
* @param {Array<object>} outputMessages - One entry per choice
|
|
99
|
-
* @param {object} [parentSpan]
|
|
100
|
-
* @returns {Promise<Array<void>>}
|
|
101
|
-
*/
|
|
102
|
-
function publishChatCompletionsOutputEvaluation (inputMessages, outputMessages, parentSpan) {
|
|
103
|
-
const evals = []
|
|
104
|
-
for (const message of outputMessages) {
|
|
105
|
-
evals.push(publishEvaluation([...inputMessages, message], parentSpan))
|
|
106
|
-
}
|
|
107
|
-
return Promise.all(evals)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Extracts OpenAI input messages from a `responses.create` call. The `instructions`
|
|
112
|
-
* field is treated as a developer prompt — it directly steers model behavior and the
|
|
113
|
-
* LLMObs OpenAI plugin already surfaces it as one — so AI Guard must screen it too.
|
|
114
|
-
*
|
|
115
|
-
* AI Guard `/evaluate` accepts a single leading system/developer message; if the
|
|
116
|
-
* caller's `input` already begins with one, prepend the `instructions` text to its
|
|
117
|
-
* content rather than emit a second developer turn.
|
|
118
|
-
*
|
|
119
|
-
* @param {object} callArgs - First argument passed to the wrapped method
|
|
120
|
-
* @returns {Array<object>|undefined}
|
|
121
|
-
*/
|
|
122
|
-
function getResponsesInputMessages (callArgs) {
|
|
123
|
-
const messages = [
|
|
124
|
-
...convertOpenAIResponseItemsToMessages(callArgs?.input, 'user'),
|
|
125
|
-
...convertOpenAIResponsePromptToMessages(callArgs?.prompt),
|
|
126
|
-
]
|
|
127
|
-
|
|
128
|
-
const instructions = typeof callArgs?.instructions === 'string' && callArgs.instructions.length
|
|
129
|
-
? callArgs.instructions
|
|
130
|
-
: null
|
|
131
|
-
if (!instructions) return messages.length ? messages : undefined
|
|
132
|
-
|
|
133
|
-
const first = messages[0]
|
|
134
|
-
if (first && (first.role === 'developer' || first.role === 'system')) {
|
|
135
|
-
const merged = { role: 'developer', content: mergeInstructionsWithContent(instructions, first.content) }
|
|
136
|
-
return [merged, ...messages.slice(1)]
|
|
137
|
-
}
|
|
138
|
-
return [{ role: 'developer', content: instructions }, ...messages]
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Merges Responses API instructions with an existing leading developer/system content value.
|
|
143
|
-
*
|
|
144
|
-
* @param {string} instructions
|
|
145
|
-
* @param {string|Array<object>|undefined} content
|
|
146
|
-
* @returns {string|Array<object>}
|
|
147
|
-
*/
|
|
148
|
-
function mergeInstructionsWithContent (instructions, content) {
|
|
149
|
-
if (Array.isArray(content)) return [{ type: 'text', text: instructions }, ...content]
|
|
150
|
-
if (typeof content === 'string' && content.length) return `${instructions}\n\n${content}`
|
|
151
|
-
return instructions
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Extracts OpenAI output messages from a `responses.create` parsed body.
|
|
156
|
-
*
|
|
157
|
-
* @param {object} body - Parsed response body
|
|
158
|
-
* @returns {Array<object>}
|
|
159
|
-
*/
|
|
160
|
-
function getResponsesOutputMessages (body) {
|
|
161
|
-
return convertOpenAIResponseItemsToMessages(body?.output, 'assistant')
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Publishes AI Guard After Model evaluation for `responses` output.
|
|
166
|
-
*
|
|
167
|
-
* The Responses API returns a single conversation turn whose `output` items form one
|
|
168
|
-
* coherent message (reasoning steps + final assistant message + tool calls + ...);
|
|
169
|
-
* they are screened together as a single evaluation.
|
|
170
|
-
*
|
|
171
|
-
* @param {Array<object>} inputMessages
|
|
172
|
-
* @param {Array<object>} outputMessages
|
|
173
|
-
* @param {object} [parentSpan]
|
|
174
|
-
* @returns {Promise<void>}
|
|
175
|
-
*/
|
|
176
|
-
function publishResponsesOutputEvaluation (inputMessages, outputMessages, parentSpan) {
|
|
177
|
-
return publishEvaluation([...inputMessages, ...outputMessages], parentSpan)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Per-resource handlers describing how AI Guard reads inputs and screens outputs for
|
|
182
|
-
* each LLM-prompt-accepting OpenAI endpoint. The keys also serve as the set of
|
|
183
|
-
* resources eligible for AI Guard evaluation.
|
|
184
|
-
*
|
|
185
|
-
* @type {Record<string, ResourceHandler>}
|
|
186
|
-
*/
|
|
187
|
-
const RESOURCE_HANDLERS = {
|
|
188
|
-
'chat.completions': {
|
|
189
|
-
getInputMessages: getChatCompletionsInputMessages,
|
|
190
|
-
getOutputMessages: getChatCompletionsOutputMessages,
|
|
191
|
-
publishOutputEvaluation: publishChatCompletionsOutputEvaluation,
|
|
192
|
-
},
|
|
193
|
-
responses: {
|
|
194
|
-
getInputMessages: getResponsesInputMessages,
|
|
195
|
-
getOutputMessages: getResponsesOutputMessages,
|
|
196
|
-
publishOutputEvaluation: publishResponsesOutputEvaluation,
|
|
197
|
-
},
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Reports whether the AI Guard channel has subscribers. The OpenAI instrumentation
|
|
202
|
-
* uses this to decide whether to take the AI Guard path at all.
|
|
203
|
-
*
|
|
204
|
-
* @returns {boolean}
|
|
205
|
-
*/
|
|
206
|
-
function hasSubscribers () {
|
|
207
|
-
return aiguardChannel.hasSubscribers
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Builds a guard handle when AI Guard is enabled and applicable to this call. The
|
|
212
|
-
* handle binds the per-resource handler so downstream functions never re-dispatch
|
|
213
|
-
* on `baseResource`. Returns null when AI Guard does not apply (no subscribers,
|
|
214
|
-
* non-eligible resource, streaming, or no input messages).
|
|
215
|
-
*
|
|
216
|
-
* @param {string} baseResource - e.g. `'chat.completions'` or `'responses'`
|
|
217
|
-
* @param {object} callArgs - First argument passed to the wrapped OpenAI method
|
|
218
|
-
* @param {boolean} stream - Whether the caller asked for a streamed response
|
|
219
|
-
* @returns {Guard|null}
|
|
220
|
-
*/
|
|
221
|
-
function createGuard (baseResource, callArgs, stream) {
|
|
222
|
-
// Streaming AI Guard support lands in a follow-up PR. For now, provider-level AI
|
|
223
|
-
// Guard only evaluates non-streaming responses.
|
|
224
|
-
if (stream || !aiguardChannel.hasSubscribers) return null
|
|
225
|
-
const handler = RESOURCE_HANDLERS[baseResource]
|
|
226
|
-
if (!handler) return null
|
|
227
|
-
|
|
228
|
-
const inputMessages = handler.getInputMessages(callArgs)
|
|
229
|
-
if (!inputMessages) return null
|
|
230
|
-
|
|
231
|
-
let inputEvalPromise
|
|
232
|
-
const guard = { handler, inputMessages, parentSpan: undefined }
|
|
233
|
-
guard.getInputEval = () => (inputEvalPromise ??= publishEvaluation(inputMessages, guard.parentSpan))
|
|
234
|
-
return guard
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Wraps `apiProm.asResponse` so callers that consume the raw `Response` object still
|
|
239
|
-
* receive the Before Model verdict. After Model evaluation is not performed on this
|
|
240
|
-
* path because the response body has not been parsed.
|
|
241
|
-
*
|
|
242
|
-
* @param {object} apiProm - APIPromise returned from the OpenAI SDK method
|
|
243
|
-
* @param {Guard} guard
|
|
244
|
-
*/
|
|
245
|
-
function wrapAsResponse (apiProm, guard) {
|
|
246
|
-
if (typeof apiProm.asResponse !== 'function') return
|
|
247
|
-
shimmer.wrap(apiProm, 'asResponse', origAsResponse => function (...args) {
|
|
248
|
-
const responsePromise = origAsResponse.apply(this, args)
|
|
249
|
-
return Promise.all([guard.getInputEval(), responsePromise]).then(([, response]) => response)
|
|
250
|
-
})
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Gates the parsed-body promise on Before Model evaluation. Resolves to the SDK's
|
|
255
|
-
* result only once the Before Model verdict is in.
|
|
256
|
-
*
|
|
257
|
-
* @param {Promise<unknown>} parsedPromise
|
|
258
|
-
* @param {Guard} guard
|
|
259
|
-
* @returns {Promise<unknown>}
|
|
260
|
-
*/
|
|
261
|
-
function gateParse (parsedPromise, guard) {
|
|
262
|
-
return Promise.all([guard.getInputEval(), parsedPromise]).then(([, result]) => result)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Runs After Model evaluation against the response body.
|
|
267
|
-
*
|
|
268
|
-
* @param {Guard} guard
|
|
269
|
-
* @param {object} body - Parsed OpenAI response body
|
|
270
|
-
* @returns {Promise<unknown>}
|
|
271
|
-
*/
|
|
272
|
-
function evaluateOutput (guard, body) {
|
|
273
|
-
const outputMessages = guard.handler.getOutputMessages(body)
|
|
274
|
-
if (!outputMessages.length) return Promise.resolve()
|
|
275
|
-
return guard.handler.publishOutputEvaluation(guard.inputMessages, outputMessages, guard.parentSpan)
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
module.exports = {
|
|
279
|
-
hasSubscribers,
|
|
280
|
-
createGuard,
|
|
281
|
-
wrapAsResponse,
|
|
282
|
-
gateParse,
|
|
283
|
-
evaluateOutput,
|
|
284
|
-
}
|