dd-trace 6.3.0 → 6.5.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.electron.md +7 -0
- package/README.md +17 -0
- package/ci/diagnose.js +2100 -0
- package/ci/init.js +23 -2
- package/ci/runbook.md +198 -0
- package/ci/test-optimization-validation/approval-artifacts.js +143 -0
- package/ci/test-optimization-validation/approval.js +299 -0
- package/ci/test-optimization-validation/artifact-id.js +20 -0
- package/ci/test-optimization-validation/bounded-json.js +121 -0
- package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
- package/ci/test-optimization-validation/ci-discovery.js +181 -0
- package/ci/test-optimization-validation/ci-remediation.js +210 -0
- package/ci/test-optimization-validation/cli.js +903 -0
- package/ci/test-optimization-validation/command-blocker.js +81 -0
- package/ci/test-optimization-validation/command-output-policy.js +206 -0
- package/ci/test-optimization-validation/command-runner.js +707 -0
- package/ci/test-optimization-validation/command-suitability.js +471 -0
- package/ci/test-optimization-validation/executable-approval.js +48 -0
- package/ci/test-optimization-validation/executable.js +480 -0
- package/ci/test-optimization-validation/generated-file-policy.js +63 -0
- package/ci/test-optimization-validation/generated-files.js +351 -0
- package/ci/test-optimization-validation/generated-verifier.js +196 -0
- package/ci/test-optimization-validation/init-probe-preload.js +163 -0
- package/ci/test-optimization-validation/init-probe.js +246 -0
- package/ci/test-optimization-validation/late-initialization.js +63 -0
- package/ci/test-optimization-validation/local-command.js +163 -0
- package/ci/test-optimization-validation/manifest-loader.js +133 -0
- package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
- package/ci/test-optimization-validation/manifest-schema.js +862 -0
- package/ci/test-optimization-validation/offline-fixtures.js +327 -0
- package/ci/test-optimization-validation/offline-output.js +406 -0
- package/ci/test-optimization-validation/payload-normalizer.js +72 -0
- package/ci/test-optimization-validation/plan-writer.js +1120 -0
- package/ci/test-optimization-validation/preflight-runner.js +114 -0
- package/ci/test-optimization-validation/redaction.js +331 -0
- package/ci/test-optimization-validation/report-writer.js +1508 -0
- package/ci/test-optimization-validation/safe-files.js +203 -0
- package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
- package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
- package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
- package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
- package/ci/test-optimization-validation/setup-runner.js +97 -0
- package/ci/test-optimization-validation/static-diagnosis.js +159 -0
- package/ci/test-optimization-validation/test-output.js +129 -0
- package/ci/test-optimization-validation-manifest.schema.json +1 -0
- package/ci/validate-test-optimization.js +3 -0
- package/ext/exporters.js +1 -0
- package/index.d.ts +121 -8
- package/index.electron.js +3 -0
- package/init.js +18 -0
- package/initialize.mjs +1 -1
- package/loader-hook.mjs +28 -18
- package/openfeature.d.ts +1 -0
- package/openfeature.js +4 -0
- package/package.json +15 -8
- package/packages/datadog-instrumentations/src/ai.js +37 -26
- package/packages/datadog-instrumentations/src/child_process.js +1 -1
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
- package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
- package/packages/datadog-instrumentations/src/cucumber.js +24 -10
- package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
- package/packages/datadog-instrumentations/src/express.js +20 -2
- package/packages/datadog-instrumentations/src/fastify.js +7 -11
- package/packages/datadog-instrumentations/src/grpc/server.js +18 -0
- package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
- package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
- package/packages/datadog-instrumentations/src/helpers/router-helper.js +8 -8
- package/packages/datadog-instrumentations/src/http2/server.js +143 -17
- package/packages/datadog-instrumentations/src/jest.js +36 -4
- package/packages/datadog-instrumentations/src/mariadb.js +1 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
- package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
- package/packages/datadog-instrumentations/src/mongodb.js +3 -3
- package/packages/datadog-instrumentations/src/mongoose.js +3 -3
- package/packages/datadog-instrumentations/src/mquery.js +2 -2
- package/packages/datadog-instrumentations/src/mysql.js +1 -1
- package/packages/datadog-instrumentations/src/next.js +24 -0
- package/packages/datadog-instrumentations/src/nyc.js +0 -2
- package/packages/datadog-instrumentations/src/oracledb.js +2 -2
- package/packages/datadog-instrumentations/src/pg.js +2 -2
- package/packages/datadog-instrumentations/src/playwright.js +159 -22
- package/packages/datadog-instrumentations/src/process.js +3 -0
- package/packages/datadog-instrumentations/src/router.js +191 -106
- package/packages/datadog-instrumentations/src/selenium.js +52 -26
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
- package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
- package/packages/datadog-instrumentations/src/ws.js +2 -1
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +41 -16
- package/packages/datadog-plugin-aws-sdk/src/services/sns.js +21 -21
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +14 -8
- package/packages/datadog-plugin-aws-sdk/src/util.js +2 -24
- package/packages/datadog-plugin-cucumber/src/index.js +1 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +24 -62
- package/packages/datadog-plugin-cypress/src/index.js +6 -1
- package/packages/datadog-plugin-cypress/src/support.js +92 -26
- package/packages/datadog-plugin-electron/src/ipc.js +1 -1
- package/packages/datadog-plugin-graphql/src/execute.js +177 -59
- package/packages/datadog-plugin-graphql/src/parse.js +22 -1
- package/packages/datadog-plugin-graphql/src/request.js +32 -1
- package/packages/datadog-plugin-graphql/src/utils.js +42 -0
- package/packages/datadog-plugin-graphql/src/validate.js +15 -3
- package/packages/datadog-plugin-http2/src/client.js +1 -1
- package/packages/datadog-plugin-http2/src/server.js +38 -6
- package/packages/datadog-plugin-jest/src/util.js +21 -6
- package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
- package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
- package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
- package/packages/datadog-plugin-playwright/src/index.js +177 -64
- package/packages/datadog-plugin-router/src/index.js +11 -2
- package/packages/datadog-plugin-selenium/src/index.js +5 -36
- package/packages/datadog-plugin-vitest/src/index.js +4 -2
- package/packages/datadog-plugin-ws/src/close.js +2 -1
- package/packages/datadog-plugin-ws/src/producer.js +2 -1
- package/packages/datadog-plugin-ws/src/receiver.js +2 -1
- package/packages/datadog-plugin-ws/src/server.js +2 -1
- package/packages/dd-trace/index.electron.js +3 -0
- package/packages/dd-trace/index.js +2 -37
- package/packages/dd-trace/src/aiguard/sdk.js +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
- package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
- package/packages/dd-trace/src/azure_metadata.js +2 -1
- package/packages/dd-trace/src/bootstrap.js +39 -0
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +55 -5
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +22 -6
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +5 -0
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +62 -7
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -2
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
- package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
- package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +173 -9
- package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +20 -0
- package/packages/dd-trace/src/config/helper.js +1 -0
- package/packages/dd-trace/src/config/index.js +10 -0
- package/packages/dd-trace/src/config/supported-configurations.json +83 -0
- package/packages/dd-trace/src/constants.js +7 -0
- package/packages/dd-trace/src/datastreams/pathway.js +6 -4
- package/packages/dd-trace/src/exporter.js +2 -0
- package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
- package/packages/dd-trace/src/exporters/common/request.js +59 -30
- package/packages/dd-trace/src/exporters/common/url.js +15 -1
- package/packages/dd-trace/src/feature-registry.js +29 -0
- package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
- package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
- package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
- package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
- package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
- package/packages/dd-trace/src/llmobs/index.js +10 -2
- package/packages/dd-trace/src/llmobs/noop.js +6 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
- package/packages/dd-trace/src/llmobs/sdk.js +17 -0
- package/packages/dd-trace/src/llmobs/tagger.js +79 -7
- package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
- package/packages/dd-trace/src/llmobs/util.js +32 -0
- package/packages/dd-trace/src/noop/proxy.js +5 -4
- package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
- package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
- package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
- package/packages/dd-trace/src/openfeature/index.js +0 -2
- package/packages/dd-trace/src/openfeature/noop.js +1 -28
- package/packages/dd-trace/src/openfeature/register.js +35 -0
- package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
- package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
- package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
- package/packages/dd-trace/src/opentelemetry/span.js +5 -0
- package/packages/dd-trace/src/opentracing/propagation/log.js +11 -2
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +80 -14
- package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +10 -1
- package/packages/dd-trace/src/opentracing/tracer.js +7 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
- package/packages/dd-trace/src/plugins/util/git.js +3 -2
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
- package/packages/dd-trace/src/plugins/util/test.js +65 -8
- package/packages/dd-trace/src/plugins/util/web.js +12 -0
- package/packages/dd-trace/src/proxy.js +99 -10
- package/packages/dd-trace/src/span_processor.js +5 -0
- package/packages/dd-trace/src/standalone/index.js +0 -36
- package/packages/dd-trace/src/telemetry/send-data.js +2 -2
- package/packages/dd-trace/src/util.js +26 -0
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/shell-quote/index.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const id = require('../../id')
|
|
4
|
+
|
|
5
|
+
const { API_BASE_PATH } = require('./client')
|
|
6
|
+
const { Row, ExperimentResult } = require('./result')
|
|
7
|
+
|
|
8
|
+
// Mirrors dd-trace-py's _generate_metric_from_evaluation: plain objects are
|
|
9
|
+
// json, everything else falls through to the lowercased categorical fallback.
|
|
10
|
+
function inferMetricType (value) {
|
|
11
|
+
if (typeof value === 'boolean') return 'boolean'
|
|
12
|
+
if (typeof value === 'number' && Number.isFinite(value)) return 'score'
|
|
13
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) return 'json'
|
|
14
|
+
return 'categorical'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function stringify (value) {
|
|
18
|
+
if (value === null || value === undefined) return ''
|
|
19
|
+
if (typeof value === 'string') return value.toLowerCase()
|
|
20
|
+
if (typeof value === 'object') return JSON.stringify(value).toLowerCase()
|
|
21
|
+
return String(value).toLowerCase()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Build the tag list, letting auto tags win over user tags on key conflict.
|
|
25
|
+
function buildTags (userTags, autoTags) {
|
|
26
|
+
const tags = new Map()
|
|
27
|
+
for (const [k, v] of Object.entries(userTags ?? {})) {
|
|
28
|
+
tags.set(k, `${k}:${v}`)
|
|
29
|
+
}
|
|
30
|
+
for (const [k, v] of Object.entries(autoTags)) {
|
|
31
|
+
tags.set(k, `${k}:${v}`)
|
|
32
|
+
}
|
|
33
|
+
return [...tags.values()]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// One span per experiment row (LLM Obs experiment span wire format).
|
|
37
|
+
function toSpan (row, metadata, ids, spanName, userTags) {
|
|
38
|
+
const meta = {
|
|
39
|
+
input: row.input ?? null,
|
|
40
|
+
output: row.output ?? null,
|
|
41
|
+
expected_output: row.expectedOutput ?? null,
|
|
42
|
+
}
|
|
43
|
+
if (metadata && Object.keys(metadata).length > 0) {
|
|
44
|
+
meta.metadata = metadata
|
|
45
|
+
}
|
|
46
|
+
if (row.isError) {
|
|
47
|
+
meta.error = { type: row.errorType ?? '', message: row.errorMessage ?? '', stack: '' }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
span_id: row.spanId,
|
|
52
|
+
trace_id: row.traceId,
|
|
53
|
+
project_id: ids.projectId,
|
|
54
|
+
dataset_id: ids.datasetId,
|
|
55
|
+
name: spanName,
|
|
56
|
+
start_ns: row.startNs,
|
|
57
|
+
duration: row.durationNs,
|
|
58
|
+
status: row.isError ? 'error' : 'ok',
|
|
59
|
+
meta,
|
|
60
|
+
tags: buildTags(userTags, {
|
|
61
|
+
experiment_id: ids.experimentId,
|
|
62
|
+
dataset_id: ids.datasetId,
|
|
63
|
+
dataset_record_id: ids.datasetRecordId,
|
|
64
|
+
}),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// One metric per evaluator per row.
|
|
69
|
+
function toMetric (label, value, errorMessage, spanId, timestampMs, experimentId, userTags) {
|
|
70
|
+
const metric = {
|
|
71
|
+
label,
|
|
72
|
+
span_id: spanId,
|
|
73
|
+
timestamp_ms: timestampMs,
|
|
74
|
+
tags: buildTags(userTags, { experiment_id: experimentId }),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (errorMessage !== null) {
|
|
78
|
+
metric.metric_type = 'categorical'
|
|
79
|
+
metric.error = { message: errorMessage }
|
|
80
|
+
return metric
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const type = inferMetricType(value)
|
|
84
|
+
metric.metric_type = type
|
|
85
|
+
if (type === 'boolean') metric.boolean_value = value
|
|
86
|
+
else if (type === 'score') metric.score_value = value
|
|
87
|
+
else if (type === 'json') metric.json_value = value
|
|
88
|
+
else metric.categorical_value = stringify(value)
|
|
89
|
+
return metric
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Builder + run() orchestration: runs rows sequentially, emits one root span
|
|
93
|
+
// per dataset row, and posts all spans + metrics in a single events call.
|
|
94
|
+
class Experiment {
|
|
95
|
+
#client
|
|
96
|
+
#name
|
|
97
|
+
#description
|
|
98
|
+
#dataset
|
|
99
|
+
#task
|
|
100
|
+
#evaluators
|
|
101
|
+
#config
|
|
102
|
+
#tags
|
|
103
|
+
#experimentId
|
|
104
|
+
|
|
105
|
+
constructor (client, options = {}) {
|
|
106
|
+
if (!options.name) throw new Error('Experiment name is required')
|
|
107
|
+
if (!options.dataset) throw new Error('Experiment dataset is required')
|
|
108
|
+
if (typeof options.task !== 'function') throw new Error('Experiment task is required')
|
|
109
|
+
|
|
110
|
+
this.#client = client
|
|
111
|
+
this.#name = options.name
|
|
112
|
+
this.#description = options.description ?? ''
|
|
113
|
+
this.#dataset = options.dataset
|
|
114
|
+
this.#task = options.task
|
|
115
|
+
this.#evaluators = new Map(Object.entries(options.evaluators ?? {}))
|
|
116
|
+
this.#config = { ...options.config }
|
|
117
|
+
this.#tags = { ...options.tags }
|
|
118
|
+
this.#experimentId = null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
name () {
|
|
122
|
+
return this.#name
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
experimentId () {
|
|
126
|
+
return this.#experimentId
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
url () {
|
|
130
|
+
if (this.#experimentId === null) return null
|
|
131
|
+
return `${this.#client.appBase}/llm/experiments/${this.#experimentId}`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async run () {
|
|
135
|
+
const projectId = await this.#client.ensureProjectId()
|
|
136
|
+
|
|
137
|
+
await this.#dataset.ensureCreatedAndPushed(projectId)
|
|
138
|
+
const datasetId = this.#dataset.id()
|
|
139
|
+
if (datasetId === null) {
|
|
140
|
+
throw new Error(`Dataset '${this.#dataset.name()}' has no id after push`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Create the experiment. ensure_unique makes the backend mint a fresh
|
|
144
|
+
// experiment under the project on every run.
|
|
145
|
+
const attributes = {
|
|
146
|
+
name: this.#name,
|
|
147
|
+
project_id: projectId,
|
|
148
|
+
dataset_id: datasetId,
|
|
149
|
+
description: this.#description,
|
|
150
|
+
ensure_unique: true,
|
|
151
|
+
}
|
|
152
|
+
if (Object.keys(this.#config).length > 0) attributes.config = this.#config
|
|
153
|
+
|
|
154
|
+
let created
|
|
155
|
+
try {
|
|
156
|
+
created = await this.#client.request('POST', `${API_BASE_PATH}/experiments`, {
|
|
157
|
+
data: { type: 'experiments', attributes },
|
|
158
|
+
})
|
|
159
|
+
} catch (err) {
|
|
160
|
+
throw new Error(`Failed to create experiment '${this.#name}': ${err.message}`)
|
|
161
|
+
}
|
|
162
|
+
this.#experimentId = created?.data?.id ?? null
|
|
163
|
+
const experimentId = this.#experimentId
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const records = this.#dataset.records()
|
|
167
|
+
const recordIds = this.#dataset.recordIds()
|
|
168
|
+
const rows = []
|
|
169
|
+
const spans = []
|
|
170
|
+
const metrics = []
|
|
171
|
+
let hasRowError = false
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < records.length; i++) {
|
|
174
|
+
const record = records[i]
|
|
175
|
+
const datasetRecordId = i < recordIds.length ? recordIds[i] : ''
|
|
176
|
+
const startNs = Date.now() * 1e6
|
|
177
|
+
const startHr = process.hrtime.bigint()
|
|
178
|
+
|
|
179
|
+
// Root-span id generation, same convention as opentracing/span.js: a single
|
|
180
|
+
// random 64-bit id for the span, reused as the trace id's low 64 bits with a
|
|
181
|
+
// start-time-derived high 64 bits (like the `_dd.p.tid` 128-bit trace id tag).
|
|
182
|
+
const spanIdentifier = id()
|
|
183
|
+
const traceIdHigh = Math.floor(startNs / 1e9).toString(16).padStart(8, '0').padEnd(16, '0')
|
|
184
|
+
const spanId = spanIdentifier.toString(16).padStart(16, '0')
|
|
185
|
+
const traceId = spanIdentifier.toTraceIdHex(traceIdHigh).padStart(32, '0')
|
|
186
|
+
|
|
187
|
+
let output = null
|
|
188
|
+
let errorType = null
|
|
189
|
+
let errorMessage = null
|
|
190
|
+
try {
|
|
191
|
+
// Rows run one at a time by design.
|
|
192
|
+
// eslint-disable-next-line no-await-in-loop
|
|
193
|
+
output = await this.#task(record.input, this.#config)
|
|
194
|
+
} catch (err) {
|
|
195
|
+
errorType = err.name || 'Error'
|
|
196
|
+
errorMessage = err.message ?? String(err)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const durationNs = Number(process.hrtime.bigint() - startHr)
|
|
200
|
+
const evaluations = {}
|
|
201
|
+
const evaluationErrors = {}
|
|
202
|
+
const timestampMs = Date.now()
|
|
203
|
+
|
|
204
|
+
for (const [label, evaluator] of this.#evaluators) {
|
|
205
|
+
if (errorType !== null) {
|
|
206
|
+
const msg = 'task error; evaluation skipped'
|
|
207
|
+
evaluationErrors[label] = msg
|
|
208
|
+
metrics.push(toMetric(label, null, msg, spanId, timestampMs, experimentId, this.#tags))
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
// eslint-disable-next-line no-await-in-loop
|
|
213
|
+
const value = await evaluator(record.input, output, record.expectedOutput)
|
|
214
|
+
evaluations[label] = value
|
|
215
|
+
metrics.push(toMetric(label, value, null, spanId, timestampMs, experimentId, this.#tags))
|
|
216
|
+
} catch (err) {
|
|
217
|
+
const msg = err.message ?? String(err)
|
|
218
|
+
evaluationErrors[label] = msg
|
|
219
|
+
metrics.push(toMetric(label, null, msg, spanId, timestampMs, experimentId, this.#tags))
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const row = new Row({
|
|
224
|
+
index: i,
|
|
225
|
+
spanId,
|
|
226
|
+
traceId,
|
|
227
|
+
startNs,
|
|
228
|
+
durationNs,
|
|
229
|
+
input: record.input,
|
|
230
|
+
output,
|
|
231
|
+
expectedOutput: record.expectedOutput,
|
|
232
|
+
errorType,
|
|
233
|
+
errorMessage,
|
|
234
|
+
evaluations,
|
|
235
|
+
evaluationErrors,
|
|
236
|
+
})
|
|
237
|
+
rows.push(row)
|
|
238
|
+
if (row.isError || Object.keys(evaluationErrors).length > 0) hasRowError = true
|
|
239
|
+
spans.push(toSpan(row, record.metadata, {
|
|
240
|
+
experimentId,
|
|
241
|
+
projectId,
|
|
242
|
+
datasetId,
|
|
243
|
+
datasetRecordId,
|
|
244
|
+
}, this.#name, this.#tags))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await this.#postEvents(experimentId, spans, metrics)
|
|
248
|
+
// A row error doesn't abort the run, but the experiment didn't succeed cleanly.
|
|
249
|
+
await this.#updateStatus(
|
|
250
|
+
experimentId,
|
|
251
|
+
hasRowError ? 'failed' : 'completed',
|
|
252
|
+
hasRowError ? 'one or more rows failed' : null
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return new ExperimentResult(experimentId, rows, this.url())
|
|
256
|
+
} catch (err) {
|
|
257
|
+
await this.#updateStatus(experimentId, 'failed', err.message ?? String(err))
|
|
258
|
+
throw err
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async #postEvents (experimentId, spans, metrics) {
|
|
263
|
+
await this.#client.request('POST', `${API_BASE_PATH}/experiments/${experimentId}/events`, {
|
|
264
|
+
data: { type: 'experiments', attributes: { spans, metrics } },
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async #updateStatus (experimentId, status, error) {
|
|
269
|
+
if (!experimentId) return
|
|
270
|
+
// The experiment-update model has no status field, so this is a direct PATCH.
|
|
271
|
+
const attributes = { status }
|
|
272
|
+
if (error !== null) attributes.error = error
|
|
273
|
+
try {
|
|
274
|
+
await this.#client.request('PATCH', `${API_BASE_PATH}/experiments/${experimentId}`, {
|
|
275
|
+
data: { type: 'experiments', attributes },
|
|
276
|
+
})
|
|
277
|
+
} catch {
|
|
278
|
+
// Status update is best-effort; never let it mask the real result/error.
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = { Experiment }
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const log = require('../../log')
|
|
4
|
+
const { ExperimentsClient, API_BASE_PATH } = require('./client')
|
|
5
|
+
const { Dataset, DatasetRecord } = require('./dataset')
|
|
6
|
+
const { Experiment } = require('./experiment')
|
|
7
|
+
const NoopExperiments = require('./noop')
|
|
8
|
+
|
|
9
|
+
// Poll `attempt` with exponential backoff until it returns true or the time
|
|
10
|
+
// budget is spent. Used for eventually-consistent reads (pullDataset).
|
|
11
|
+
async function retryWithBackoff (attempt, { maxTotalMs = 30_000, baseDelayMs = 250, maxDelayMs = 8000 } = {}) {
|
|
12
|
+
const start = Date.now()
|
|
13
|
+
let delay = baseDelayMs
|
|
14
|
+
for (;;) {
|
|
15
|
+
// eslint-disable-next-line no-await-in-loop
|
|
16
|
+
if (await attempt()) return true
|
|
17
|
+
const remaining = maxTotalMs - (Date.now() - start)
|
|
18
|
+
if (remaining <= 0) return false
|
|
19
|
+
// eslint-disable-next-line no-await-in-loop
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delay, maxDelayMs, remaining)))
|
|
21
|
+
delay *= 2
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Entry point exposed as `tracer.llmobs.experiments`. Builds datasets and runs
|
|
26
|
+
// experiments against the LLM Obs backend using the tracer's own config.
|
|
27
|
+
class Experiments {
|
|
28
|
+
#client
|
|
29
|
+
#projectName
|
|
30
|
+
|
|
31
|
+
constructor (config) {
|
|
32
|
+
this.#projectName = config.llmobs?.mlApp || config.service
|
|
33
|
+
this.#client = new ExperimentsClient({
|
|
34
|
+
apiKey: config.DD_API_KEY,
|
|
35
|
+
appKey: config.DD_APP_KEY,
|
|
36
|
+
site: config.site,
|
|
37
|
+
projectName: this.#projectName,
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Create a local dataset buffer. Pushed remotely on first experiment run.
|
|
42
|
+
createDataset (name, description = '') {
|
|
43
|
+
return new Dataset(this.#client, name, description)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Pull an existing dataset by name (with its records). Polls with exponential
|
|
47
|
+
// backoff to absorb read-after-write lag; pass `expectedRecordCount` to also
|
|
48
|
+
// wait until that many records are readable.
|
|
49
|
+
async pullDataset (name, options = {}) {
|
|
50
|
+
const { expectedRecordCount, maxWaitMs = 30_000 } = options
|
|
51
|
+
const projectId = await this.#client.ensureProjectId()
|
|
52
|
+
|
|
53
|
+
let datasetId = null
|
|
54
|
+
let description = ''
|
|
55
|
+
let records = []
|
|
56
|
+
let recordIds = []
|
|
57
|
+
let lastError = ''
|
|
58
|
+
|
|
59
|
+
const succeeded = await retryWithBackoff(async () => {
|
|
60
|
+
try {
|
|
61
|
+
if (datasetId === null) {
|
|
62
|
+
const listed = await this.#client.request(
|
|
63
|
+
'GET',
|
|
64
|
+
`${API_BASE_PATH}/${projectId}/datasets?filter[name]=${encodeURIComponent(name)}`
|
|
65
|
+
)
|
|
66
|
+
for (const item of listed?.data ?? []) {
|
|
67
|
+
if (item?.attributes?.name === name) {
|
|
68
|
+
datasetId = String(item?.id ?? '')
|
|
69
|
+
description = String(item?.attributes?.description ?? '')
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (datasetId === null) return false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const recs = []
|
|
77
|
+
const ids = []
|
|
78
|
+
let cursor = ''
|
|
79
|
+
// Follow the meta.after / page[cursor] pagination until the last page.
|
|
80
|
+
for (;;) {
|
|
81
|
+
const query = cursor ? `?page[cursor]=${encodeURIComponent(cursor)}` : ''
|
|
82
|
+
// eslint-disable-next-line no-await-in-loop
|
|
83
|
+
const resp = await this.#client.request(
|
|
84
|
+
'GET',
|
|
85
|
+
`${API_BASE_PATH}/${projectId}/datasets/${datasetId}/records${query}`
|
|
86
|
+
)
|
|
87
|
+
for (const item of resp?.data ?? []) {
|
|
88
|
+
const attrs = item?.attributes ?? item
|
|
89
|
+
recs.push(new DatasetRecord(attrs?.input ?? null, attrs?.expected_output ?? null, attrs?.metadata ?? {}))
|
|
90
|
+
ids.push(String(item?.id ?? ''))
|
|
91
|
+
}
|
|
92
|
+
cursor = resp?.meta?.after ?? ''
|
|
93
|
+
if (!cursor) break
|
|
94
|
+
}
|
|
95
|
+
records = recs
|
|
96
|
+
recordIds = ids
|
|
97
|
+
lastError = ''
|
|
98
|
+
|
|
99
|
+
return expectedRecordCount == null || recs.length >= expectedRecordCount
|
|
100
|
+
} catch (err) {
|
|
101
|
+
lastError = err.message
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
}, { maxTotalMs: maxWaitMs })
|
|
105
|
+
|
|
106
|
+
if (datasetId === null && lastError) {
|
|
107
|
+
throw new Error(`Failed to list datasets in project '${this.#projectName}': ${lastError}`)
|
|
108
|
+
}
|
|
109
|
+
if (datasetId === null) {
|
|
110
|
+
throw new Error(`Dataset '${name}' not found in project '${this.#projectName}' (after ${maxWaitMs}ms)`)
|
|
111
|
+
}
|
|
112
|
+
if (!succeeded && lastError) {
|
|
113
|
+
throw new Error(`Failed to fetch records for dataset '${name}' in project '${this.#projectName}': ${lastError}`)
|
|
114
|
+
}
|
|
115
|
+
if (!succeeded && expectedRecordCount != null) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Dataset '${name}' has ${records.length} record(s) after ${maxWaitMs}ms, expected ${expectedRecordCount} ` +
|
|
118
|
+
'— backend may not have finished ingesting the push'
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return Dataset.fromExisting(this.#client, name, description, datasetId, projectId, records, recordIds)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Build an experiment: { name, dataset, task, evaluators, description?, config?, tags? }.
|
|
126
|
+
experiment (options) {
|
|
127
|
+
return new Experiment(this.#client, options)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Factory used by the LLMObs SDK: returns a real Experiments instance when
|
|
132
|
+
// enabled and credentialed, otherwise a no-op that explains what's missing.
|
|
133
|
+
function createExperiments (config) {
|
|
134
|
+
if (!config.llmobs?.DD_LLMOBS_ENABLED) {
|
|
135
|
+
return new NoopExperiments('LLM Observability is not enabled')
|
|
136
|
+
}
|
|
137
|
+
if (!(config.DD_API_KEY) || !config.DD_APP_KEY) {
|
|
138
|
+
log.warn('LLMObs experiments: missing api and/or app keys, set DD_API_KEY and DD_APP_KEY')
|
|
139
|
+
return new NoopExperiments('DD_API_KEY and DD_APP_KEY are required for experiments')
|
|
140
|
+
}
|
|
141
|
+
if (!config.llmobs?.mlApp && !config.service) {
|
|
142
|
+
log.warn('LLMObs experiments: no project name configured, set DD_LLMOBS_ML_APP or DD_SERVICE')
|
|
143
|
+
return new NoopExperiments(
|
|
144
|
+
'no project name configured; set the DD_LLMOBS_ML_APP environment variable (or llmobs.mlApp in ' +
|
|
145
|
+
'tracer.init()) to name the LLM Obs project, or DD_SERVICE (or service in tracer.init()) as a fallback, ' +
|
|
146
|
+
'then retry'
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
return new Experiments(config)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { Experiments, createExperiments }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// No-op Experiments used when LLM Observability is disabled or the API/APP keys
|
|
4
|
+
// are not configured. Every operation throws a clear, actionable error rather
|
|
5
|
+
// than silently doing nothing, so misconfiguration surfaces immediately.
|
|
6
|
+
class NoopExperiments {
|
|
7
|
+
#reason
|
|
8
|
+
|
|
9
|
+
constructor (reason) {
|
|
10
|
+
this.#reason = reason || 'LLMObs experiments are not available'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#unavailable () {
|
|
14
|
+
return new Error(`LLMObs experiments unavailable: ${this.#reason}`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
createDataset () {
|
|
18
|
+
throw this.#unavailable()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pullDataset () {
|
|
22
|
+
return Promise.reject(this.#unavailable())
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
experiment () {
|
|
26
|
+
throw this.#unavailable()
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = NoopExperiments
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// One row of an experiment run.
|
|
4
|
+
class Row {
|
|
5
|
+
constructor (fields) {
|
|
6
|
+
this.index = fields.index
|
|
7
|
+
this.spanId = fields.spanId
|
|
8
|
+
this.traceId = fields.traceId
|
|
9
|
+
this.startNs = fields.startNs
|
|
10
|
+
this.durationNs = fields.durationNs
|
|
11
|
+
this.input = fields.input
|
|
12
|
+
this.output = fields.output
|
|
13
|
+
this.expectedOutput = fields.expectedOutput
|
|
14
|
+
this.errorType = fields.errorType
|
|
15
|
+
this.errorMessage = fields.errorMessage
|
|
16
|
+
this.evaluations = fields.evaluations
|
|
17
|
+
this.evaluationErrors = fields.evaluationErrors
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get isError () {
|
|
21
|
+
return this.errorType !== null
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Returned by Experiment.run().
|
|
26
|
+
class ExperimentResult {
|
|
27
|
+
constructor (experimentId, rows, url) {
|
|
28
|
+
this.experimentId = experimentId
|
|
29
|
+
this.rows = rows
|
|
30
|
+
this.url = url
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { Row, ExperimentResult }
|
|
@@ -7,8 +7,11 @@ const { DD_MAJOR } = require('../../../../version')
|
|
|
7
7
|
const startupLogs = require('../startup-log')
|
|
8
8
|
const {
|
|
9
9
|
ML_APP,
|
|
10
|
+
SESSION_ID,
|
|
11
|
+
SESSION_ID_TRACE_DEFAULT_KEY,
|
|
10
12
|
PROPAGATED_ML_APP_KEY,
|
|
11
13
|
PROPAGATED_PARENT_ID_KEY,
|
|
14
|
+
PROPAGATED_SESSION_ID_KEY,
|
|
12
15
|
SAMPLE_RATE,
|
|
13
16
|
SAMPLING_DECISION,
|
|
14
17
|
PROPAGATED_SAMPLE_RATE_KEY,
|
|
@@ -110,7 +113,7 @@ function disable () {
|
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
// since LLMObs traces can extend between services and be the same trace,
|
|
113
|
-
// we need to propagate the parent id, mlApp, and sampling rate/decision.
|
|
116
|
+
// we need to propagate the parent id, mlApp, session id, and sampling rate/decision.
|
|
114
117
|
function handleLLMObsInjection ({ carrier }) {
|
|
115
118
|
// Respect the standard propagator's gate: when trace tag propagation is
|
|
116
119
|
// disabled, don't write `x-datadog-tags` for LLMObs either.
|
|
@@ -130,8 +133,12 @@ function handleLLMObsInjection ({ carrier }) {
|
|
|
130
133
|
mlObsSpanTags?.[SAMPLE_RATE] ?? parentContext?._trace?.tags?.[PROPAGATED_SAMPLE_RATE_KEY]
|
|
131
134
|
const samplingDecision =
|
|
132
135
|
mlObsSpanTags?.[SAMPLING_DECISION] ?? parentContext?._trace?.tags?.[PROPAGATED_SAMPLING_DECISION_KEY]
|
|
136
|
+
const sessionId =
|
|
137
|
+
mlObsSpanTags?.[SESSION_ID] ??
|
|
138
|
+
parentContext?._trace?.tags?.[SESSION_ID_TRACE_DEFAULT_KEY] ??
|
|
139
|
+
parentContext?._trace?.tags?.[PROPAGATED_SESSION_ID_KEY]
|
|
133
140
|
|
|
134
|
-
if (!parentId && !mlApp && samplingDecision == null) return
|
|
141
|
+
if (!parentId && !mlApp && samplingDecision == null && !sessionId) return
|
|
135
142
|
|
|
136
143
|
// `_injectTags` only writes `x-datadog-tags` when the trace has `_dd.p.*`
|
|
137
144
|
// tags, so it may be undefined here — coalesce before appending.
|
|
@@ -139,6 +146,7 @@ function handleLLMObsInjection ({ carrier }) {
|
|
|
139
146
|
let tags = existing || ''
|
|
140
147
|
if (parentId) tags += `${tags ? ',' : ''}${PROPAGATED_PARENT_ID_KEY}=${parentId}`
|
|
141
148
|
if (mlApp) tags += `${tags ? ',' : ''}${PROPAGATED_ML_APP_KEY}=${mlApp}`
|
|
149
|
+
if (sessionId) tags += `${tags ? ',' : ''}${PROPAGATED_SESSION_ID_KEY}=${sessionId}`
|
|
142
150
|
if (sampleRate != null) tags += `${tags ? ',' : ''}${PROPAGATED_SAMPLE_RATE_KEY}=${sampleRate}`
|
|
143
151
|
if (samplingDecision != null) tags += `${tags ? ',' : ''}${PROPAGATED_SAMPLING_DECISION_KEY}=${samplingDecision}`
|
|
144
152
|
if (tags !== existing) carrier['x-datadog-tags'] = tags
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const NoopExperiments = require('./experiments/noop')
|
|
4
|
+
|
|
3
5
|
class NoopLLMObs {
|
|
4
6
|
constructor (noopTracer) {
|
|
5
7
|
this._tracer = noopTracer
|
|
@@ -9,6 +11,10 @@ class NoopLLMObs {
|
|
|
9
11
|
return false
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
get experiments () {
|
|
15
|
+
return new NoopExperiments('LLM Observability is not enabled')
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
enable (options) {}
|
|
13
19
|
|
|
14
20
|
disable () {}
|
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
const SPAN_NAME_TO_KIND_MAPPING = {
|
|
14
14
|
// embeddings
|
|
15
15
|
embed: 'embedding',
|
|
16
|
+
embedMany: 'embedding',
|
|
16
17
|
// text generation
|
|
17
18
|
generateText: 'workflow',
|
|
18
19
|
streamText: 'workflow',
|
|
@@ -237,6 +238,7 @@ class VercelAiTelemetryPlugin extends BaseLLMObsPlugin {
|
|
|
237
238
|
|
|
238
239
|
switch (operation) {
|
|
239
240
|
case 'embed':
|
|
241
|
+
case 'embedMany':
|
|
240
242
|
this.setEmbeddingTags(span, ctx)
|
|
241
243
|
break
|
|
242
244
|
case 'generateText':
|
|
@@ -260,14 +262,29 @@ class VercelAiTelemetryPlugin extends BaseLLMObsPlugin {
|
|
|
260
262
|
setEmbeddingTags (span, ctx) {
|
|
261
263
|
const { event, result } = ctx
|
|
262
264
|
|
|
265
|
+
// works for both embed and embedMany, either single string or array of strings
|
|
263
266
|
const input = event.value
|
|
264
|
-
|
|
265
|
-
|
|
267
|
+
|
|
268
|
+
if (!result) {
|
|
269
|
+
this._tagger.tagEmbeddingIO(span, input)
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// embed returns single embedding, embedMany returns an array of embeddings
|
|
274
|
+
const singleEmbedding = result.embedding
|
|
275
|
+
const multiEmbedding = result.embeddings
|
|
276
|
+
|
|
277
|
+
let embeddingTextResult = ''
|
|
278
|
+
if (singleEmbedding) {
|
|
279
|
+
embeddingTextResult = `[1 embedding(s) returned with size ${singleEmbedding.length}]`
|
|
280
|
+
} else if (Array.isArray(multiEmbedding)) {
|
|
281
|
+
embeddingTextResult = `[${multiEmbedding.length} embedding(s) returned with size ${multiEmbedding[0]?.length}]`
|
|
282
|
+
}
|
|
266
283
|
|
|
267
284
|
this._tagger.tagEmbeddingIO(span, input, embeddingTextResult)
|
|
268
285
|
|
|
269
286
|
this._tagger.tagMetrics(span, {
|
|
270
|
-
inputTokens: result
|
|
287
|
+
inputTokens: result.usage?.tokens,
|
|
271
288
|
})
|
|
272
289
|
}
|
|
273
290
|
|
|
@@ -59,7 +59,7 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
|
|
|
59
59
|
const { delta } = chunk
|
|
60
60
|
if (!delta) continue
|
|
61
61
|
|
|
62
|
-
const lastBlock = response.content
|
|
62
|
+
const lastBlock = response.content.at(-1)
|
|
63
63
|
if (!lastBlock) continue
|
|
64
64
|
|
|
65
65
|
if (delta.type === 'thinking_delta') {
|
|
@@ -77,7 +77,7 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
|
|
|
77
77
|
break
|
|
78
78
|
}
|
|
79
79
|
case 'content_block_stop': {
|
|
80
|
-
const lastBlock = response.content
|
|
80
|
+
const lastBlock = response.content.at(-1)
|
|
81
81
|
if (!lastBlock) break
|
|
82
82
|
if (lastBlock.type === 'tool_use') {
|
|
83
83
|
const input = lastBlock.input ?? '{}'
|
|
@@ -74,7 +74,7 @@ class NextStreamLLMObsPlugin extends LLMObsPlugin {
|
|
|
74
74
|
|
|
75
75
|
const { streamInputs: inputs, chunks } = streamData
|
|
76
76
|
const input = inputs == null ? undefined : formatIO(inputs)
|
|
77
|
-
const lastChunk = chunks.length > 0 ? chunks
|
|
77
|
+
const lastChunk = chunks.length > 0 ? chunks.at(-1) : undefined
|
|
78
78
|
const output = !hasError && lastChunk != null ? formatIO(lastChunk) : undefined
|
|
79
79
|
|
|
80
80
|
this._tagger.tagTextIO(span, input, output)
|
|
@@ -6,6 +6,12 @@ const INPUT_TYPE_TEXT = 'input_text'
|
|
|
6
6
|
|
|
7
7
|
const IMAGE_FALLBACK = '[image]'
|
|
8
8
|
const FILE_FALLBACK = '[file]'
|
|
9
|
+
const AUDIO_FALLBACK = '[audio]'
|
|
10
|
+
|
|
11
|
+
// OpenAI audio `format` values that don't map cleanly to `audio/<format>`.
|
|
12
|
+
const AUDIO_MIME_TYPES = {
|
|
13
|
+
mp3: 'audio/mpeg',
|
|
14
|
+
}
|
|
9
15
|
|
|
10
16
|
module.exports = {
|
|
11
17
|
INPUT_TYPE_IMAGE,
|
|
@@ -13,4 +19,6 @@ module.exports = {
|
|
|
13
19
|
INPUT_TYPE_TEXT,
|
|
14
20
|
IMAGE_FALLBACK,
|
|
15
21
|
FILE_FALLBACK,
|
|
22
|
+
AUDIO_FALLBACK,
|
|
23
|
+
AUDIO_MIME_TYPES,
|
|
16
24
|
}
|