@struct-ai/sdk 0.3.17 → 0.4.2
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 +62 -17
- package/dist/commonjs/context.d.ts +19 -0
- package/dist/commonjs/context.js +58 -0
- package/dist/commonjs/core.js +104 -8
- package/dist/commonjs/events.d.ts +17 -6
- package/dist/commonjs/events.js +82 -59
- package/dist/commonjs/genai-content.d.ts +52 -0
- package/dist/commonjs/genai-content.js +143 -0
- package/dist/commonjs/instrument.d.ts +47 -0
- package/dist/commonjs/instrument.js +158 -0
- package/dist/commonjs/integrations/anthropic-content.js +18 -6
- package/dist/commonjs/integrations/anthropic.d.ts +6 -1
- package/dist/commonjs/integrations/anthropic.js +113 -125
- package/dist/commonjs/integrations/index.js +8 -0
- package/dist/commonjs/integrations/langchain-callback.d.ts +3 -0
- package/dist/commonjs/integrations/langchain-callback.js +84 -6
- package/dist/commonjs/integrations/langchain-content.js +1 -1
- package/dist/commonjs/integrations/openai-content.d.ts +34 -0
- package/dist/commonjs/integrations/openai-content.js +375 -0
- package/dist/commonjs/integrations/openai.d.ts +39 -0
- package/dist/commonjs/integrations/openai.js +305 -0
- package/dist/commonjs/semconv.d.ts +1 -0
- package/dist/commonjs/semconv.js +1 -0
- package/dist/commonjs/truncation.d.ts +29 -0
- package/dist/commonjs/truncation.js +184 -10
- package/dist/commonjs/version.d.ts +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/context.d.ts +19 -0
- package/dist/esm/context.js +56 -0
- package/dist/esm/core.js +104 -8
- package/dist/esm/events.d.ts +17 -6
- package/dist/esm/events.js +82 -61
- package/dist/esm/genai-content.d.ts +52 -0
- package/dist/esm/genai-content.js +137 -0
- package/dist/esm/instrument.d.ts +47 -0
- package/dist/esm/instrument.js +155 -0
- package/dist/esm/integrations/anthropic-content.js +19 -7
- package/dist/esm/integrations/anthropic.d.ts +6 -1
- package/dist/esm/integrations/anthropic.js +112 -126
- package/dist/esm/integrations/index.js +8 -0
- package/dist/esm/integrations/langchain-callback.d.ts +3 -0
- package/dist/esm/integrations/langchain-callback.js +85 -7
- package/dist/esm/integrations/langchain-content.js +1 -1
- package/dist/esm/integrations/openai-content.d.ts +34 -0
- package/dist/esm/integrations/openai-content.js +360 -0
- package/dist/esm/integrations/openai.d.ts +39 -0
- package/dist/esm/integrations/openai.js +296 -0
- package/dist/esm/semconv.d.ts +1 -0
- package/dist/esm/semconv.js +1 -0
- package/dist/esm/truncation.d.ts +29 -0
- package/dist/esm/truncation.js +182 -10
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +8 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports._setChatRequestAttrsForTest = exports._detectProviderForTest = void 0;
|
|
3
4
|
exports.patch = patch;
|
|
4
5
|
exports.unpatch = unpatch;
|
|
5
6
|
exports.wrapCreate = wrapCreate;
|
|
@@ -16,9 +17,11 @@ function childParentContext() {
|
|
|
16
17
|
: api_1.context.active();
|
|
17
18
|
}
|
|
18
19
|
const core_js_1 = require("../core.js");
|
|
20
|
+
const genai_content_js_1 = require("../genai-content.js");
|
|
19
21
|
const events_js_1 = require("../events.js");
|
|
22
|
+
const genai_content_js_2 = require("../genai-content.js");
|
|
23
|
+
const instrument_js_1 = require("../instrument.js");
|
|
20
24
|
const semconv_js_1 = require("../semconv.js");
|
|
21
|
-
const truncation_js_1 = require("../truncation.js");
|
|
22
25
|
const anthropic_content_js_1 = require("./anthropic-content.js");
|
|
23
26
|
const STRUCT_WRAPPED = Symbol.for("struct.wrapped");
|
|
24
27
|
const STRUCT_ORIGINAL = Symbol.for("struct.original");
|
|
@@ -106,113 +109,96 @@ function restoreMethod(proto, methodName) {
|
|
|
106
109
|
proto[methodName] = current[STRUCT_ORIGINAL];
|
|
107
110
|
}
|
|
108
111
|
}
|
|
112
|
+
// Exact client class names per platform (prototype chain covers subclasses)
|
|
113
|
+
// and official endpoint host shapes only — a generic cloud host (API-gateway,
|
|
114
|
+
// storage, arbitrary *.amazonaws.com/*.googleapis.com proxies) is NOT
|
|
115
|
+
// platform routing and must stay on the fallback. Parity: python anthropic.py.
|
|
116
|
+
const CLASS_NAME_RULES = [
|
|
117
|
+
// Mantle flavor extends BaseMantleClient, NOT the plain Bedrock base, so it
|
|
118
|
+
// needs its own names; default endpoint bedrock-mantle.{region}.api.aws.
|
|
119
|
+
[
|
|
120
|
+
new Set([
|
|
121
|
+
"AnthropicBedrock",
|
|
122
|
+
"BaseAnthropicBedrock",
|
|
123
|
+
"AnthropicBedrockMantle",
|
|
124
|
+
"BaseMantleClient",
|
|
125
|
+
]),
|
|
126
|
+
"aws.bedrock",
|
|
127
|
+
],
|
|
128
|
+
[new Set(["AnthropicVertex", "BaseAnthropicVertex"]), "gcp.vertex_ai"],
|
|
129
|
+
];
|
|
130
|
+
function isBedrockHost(host) {
|
|
131
|
+
if ((host.startsWith("bedrock-runtime.") ||
|
|
132
|
+
host.startsWith("bedrock-runtime-fips.")) &&
|
|
133
|
+
(host.endsWith(".amazonaws.com") || host.endsWith(".amazonaws.com.cn"))) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
// Bedrock Mantle default endpoint: bedrock-mantle.{region}.api.aws
|
|
137
|
+
return host.startsWith("bedrock-mantle.") && host.endsWith(".api.aws");
|
|
138
|
+
}
|
|
139
|
+
function isVertexHost(host) {
|
|
140
|
+
return (host === "aiplatform.googleapis.com" ||
|
|
141
|
+
host.endsWith("-aiplatform.googleapis.com"));
|
|
142
|
+
}
|
|
143
|
+
const HOST_RULES = [
|
|
144
|
+
[isBedrockHost, "aws.bedrock"],
|
|
145
|
+
[isVertexHost, "gcp.vertex_ai"],
|
|
146
|
+
];
|
|
147
|
+
function detectProvider(resource) {
|
|
148
|
+
return (0, genai_content_js_1.detectProviderFromResource)(resource, CLASS_NAME_RULES, HOST_RULES, "anthropic");
|
|
149
|
+
}
|
|
150
|
+
/** @internal */
|
|
151
|
+
exports._detectProviderForTest = detectProvider;
|
|
152
|
+
/** @internal */
|
|
153
|
+
const _setChatRequestAttrsForTest = (span, params, sdk, logger, provider) => setChatRequestAttrs(span, params, sdk, logger, provider);
|
|
154
|
+
exports._setChatRequestAttrsForTest = _setChatRequestAttrsForTest;
|
|
109
155
|
function wrapCreate(original) {
|
|
110
156
|
return function wrappedCreate(params, opts) {
|
|
111
157
|
const patchCtx = activePatchCtx.value;
|
|
112
158
|
if (!patchCtx) {
|
|
113
159
|
return original.call(this, params, opts);
|
|
114
160
|
}
|
|
161
|
+
// CLASS RULE: detection => propagation, immediately and on EVERY path
|
|
162
|
+
// (see openai.ts). Guarded; write-once makes repeats harmless.
|
|
163
|
+
const provider = detectProvider(this);
|
|
164
|
+
(0, context_js_1.propagateProviderToParent)(provider);
|
|
115
165
|
if ((0, context_js_1.isGenAiSuppressed)()) {
|
|
116
166
|
// A framework layer owns this chat span — run the call, emit no span.
|
|
117
167
|
return original.call(this, params, opts);
|
|
118
168
|
}
|
|
169
|
+
// Preflight reads touch CALLER-owned `params` — a Proxy / lazy request
|
|
170
|
+
// object can have throwing getters; a throw here would prevent the API
|
|
171
|
+
// call. Degrade: exactly one plain, uninstrumented original.call.
|
|
172
|
+
let streaming = false;
|
|
173
|
+
let model = "unknown";
|
|
174
|
+
try {
|
|
175
|
+
streaming = !!(params && params.stream === true);
|
|
176
|
+
model = params?.model ?? "unknown";
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return original.call(this, params, opts);
|
|
180
|
+
}
|
|
119
181
|
// If params.stream is true, Messages.create delegates to the streaming path.
|
|
120
182
|
// We still need to instrument the returned stream like we do in messages.stream.
|
|
121
|
-
if (
|
|
183
|
+
if (streaming) {
|
|
122
184
|
return wrapStream(original).call(this, params, opts);
|
|
123
185
|
}
|
|
124
186
|
const { tracer, sdk, logger } = patchCtx;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const liveSpan = span;
|
|
141
|
-
const liveCtx = ctx;
|
|
142
|
-
// Enter the span's context BEFORE emitting request-side log records so
|
|
143
|
-
// `logger.emit({ context: otelContext.active() })` picks up the chat
|
|
144
|
-
// span as the log's TraceId / SpanId. Without this the UI can't link
|
|
145
|
-
// user/system message logs back to the chat span.
|
|
146
|
-
return api_1.context.with(liveCtx, () => {
|
|
147
|
-
(0, core_js_1.safe)(() => setChatRequestAttrs(liveSpan, params, sdk, logger), "anthropic.create.set_request_attrs", internalLogger);
|
|
148
|
-
let rawResult;
|
|
149
|
-
try {
|
|
150
|
-
rawResult = original.call(this, params, opts);
|
|
151
|
-
}
|
|
152
|
-
catch (err) {
|
|
153
|
-
// Sync throw from the original call (e.g. a validation error thrown
|
|
154
|
-
// before any request goes out) would otherwise leak the span — no
|
|
155
|
-
// promise is ever created to drive the .then/.catch below.
|
|
156
|
-
(0, core_js_1.safe)(() => recordErrorOnSpan(liveSpan, err), "anthropic.create.record_error", internalLogger);
|
|
157
|
-
(0, core_js_1.safe)(() => liveSpan.end(), "anthropic.create.span_end_on_error", internalLogger);
|
|
158
|
-
throw err;
|
|
159
|
-
}
|
|
160
|
-
// Observe the SDK's own return value (Anthropic's APIPromise) for
|
|
161
|
-
// telemetry, then hand it BACK UNCHANGED — replacing it with a native
|
|
162
|
-
// `Promise.resolve(...).then(...)` strips APIPromise methods
|
|
163
|
-
// (.withResponse/.asResponse), breaking customer code only when
|
|
164
|
-
// instrumentation is enabled. Our observer chain must not create an
|
|
165
|
-
// unhandled rejection. (The prior code returned a native Promise here;
|
|
166
|
-
// this is adjacent hardening of pre-existing behavior — the identity
|
|
167
|
-
// hazard predates the streaming work.)
|
|
168
|
-
//
|
|
169
|
-
// Both the `.then` PROPERTY READ (`rt.then`) and the `.then(...)` CALL
|
|
170
|
-
// that registers our observer run SYNCHRONOUSLY on the success path of
|
|
171
|
-
// a customer's `create()` call, and were previously unguarded — a
|
|
172
|
-
// hostile thenable (a `.then` accessor that throws on read, or a
|
|
173
|
-
// `.then` function that throws synchronously when invoked) would turn
|
|
174
|
-
// a SUCCESSFUL Anthropic request into a synchronous exception thrown
|
|
175
|
-
// out of `wrappedCreate`. Wrap the read + registration so a broken
|
|
176
|
-
// thenable degrades instead of crashing the host: close the span with
|
|
177
|
-
// no telemetry and hand back rawResult UNCHANGED — never
|
|
178
|
-
// `Promise.resolve(...)`, which would also strip APIPromise
|
|
179
|
-
// identity/methods.
|
|
180
|
-
try {
|
|
181
|
-
const rt = rawResult;
|
|
182
|
-
if (rt && typeof rt.then === "function") {
|
|
183
|
-
rawResult.then((result) => {
|
|
184
|
-
(0, core_js_1.safe)(() => setChatResponseAttrs(liveSpan, sdk, result, logger), "anthropic.create.set_response_attrs", internalLogger);
|
|
185
|
-
(0, core_js_1.safe)(() => liveSpan.setStatus({ code: api_1.SpanStatusCode.OK }), "anthropic.create.set_ok_status", internalLogger);
|
|
186
|
-
(0, core_js_1.safe)(() => liveSpan.end(), "anthropic.create.span_end", internalLogger);
|
|
187
|
-
}, (err) => {
|
|
188
|
-
(0, core_js_1.safe)(() => recordErrorOnSpan(liveSpan, err), "anthropic.create.record_error", internalLogger);
|
|
189
|
-
(0, core_js_1.safe)(() => liveSpan.end(), "anthropic.create.span_end_on_error", internalLogger);
|
|
190
|
-
// Observer branch only — do NOT rethrow; the original APIPromise
|
|
191
|
-
// still rejects to the customer independently.
|
|
192
|
-
});
|
|
193
|
-
return rawResult;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
catch {
|
|
197
|
-
// Reading `.then` or invoking it threw (hostile thenable). The
|
|
198
|
-
// underlying call already succeeded (rawResult exists) — this is an
|
|
199
|
-
// instrumentation-only failure, not an API error. Degrade like the
|
|
200
|
-
// other host-crash guards in this file (e.g. wrapStream's dispatch
|
|
201
|
-
// region below): close the span with no telemetry, hand back
|
|
202
|
-
// rawResult untouched.
|
|
203
|
-
(0, core_js_1.safe)(() => liveSpan.setStatus({ code: api_1.SpanStatusCode.OK }), "anthropic.create.set_ok_status", internalLogger);
|
|
204
|
-
(0, core_js_1.safe)(() => liveSpan.end(), "anthropic.create.span_end", internalLogger);
|
|
205
|
-
return rawResult;
|
|
206
|
-
}
|
|
207
|
-
// Non-thenable return (a synchronous / mocked result, or any SDK shape
|
|
208
|
-
// that isn't a promise): run the response telemetry inline on it, close
|
|
209
|
-
// the span, and hand it back untouched — matching the prior
|
|
210
|
-
// `Promise.resolve(...).then(...)` behavior, which unified thenable and
|
|
211
|
-
// non-thenable returns.
|
|
212
|
-
(0, core_js_1.safe)(() => setChatResponseAttrs(liveSpan, sdk, rawResult, logger), "anthropic.create.set_response_attrs", internalLogger);
|
|
213
|
-
(0, core_js_1.safe)(() => liveSpan.setStatus({ code: api_1.SpanStatusCode.OK }), "anthropic.create.set_ok_status", internalLogger);
|
|
214
|
-
(0, core_js_1.safe)(() => liveSpan.end(), "anthropic.create.span_end", internalLogger);
|
|
215
|
-
return rawResult;
|
|
187
|
+
// All host-boundary safety lives in the shared instrumentCall harness: the
|
|
188
|
+
// host call runs exactly once, nothing host-controllable wraps it, and every
|
|
189
|
+
// telemetry side-effect degrades via safe(). Log-record→span linkage is
|
|
190
|
+
// carried by the explicit span passed to the emitters, so no
|
|
191
|
+
// `otelContext.with(...)` is needed around the call.
|
|
192
|
+
return (0, instrument_js_1.instrumentCall)(() => original.call(this, params, opts), {
|
|
193
|
+
tracer,
|
|
194
|
+
spanName: `chat ${model}`,
|
|
195
|
+
spanKind: api_1.SpanKind.CLIENT,
|
|
196
|
+
parentContext: childParentContext,
|
|
197
|
+
sitePrefix: "anthropic.create",
|
|
198
|
+
internalLogger: sdk.getInternalLogger(),
|
|
199
|
+
onStart: (span) => setChatRequestAttrs(span, params, sdk, logger, provider),
|
|
200
|
+
onSuccess: (span, result) => setChatResponseAttrs(span, sdk, result, logger, provider),
|
|
201
|
+
onError: (span, err) => recordErrorOnSpan(span, err),
|
|
216
202
|
});
|
|
217
203
|
};
|
|
218
204
|
}
|
|
@@ -222,13 +208,26 @@ function wrapStream(original) {
|
|
|
222
208
|
if (!patchCtx) {
|
|
223
209
|
return original.call(this, params, opts);
|
|
224
210
|
}
|
|
211
|
+
// CLASS RULE: detection => propagation, immediately and on EVERY path —
|
|
212
|
+
// when suppressed (langchain owns the span), the raw client here is the
|
|
213
|
+
// only layer that can detect the real platform for the agent span.
|
|
214
|
+
const streamProvider = detectProvider(this);
|
|
215
|
+
(0, context_js_1.propagateProviderToParent)(streamProvider);
|
|
225
216
|
if ((0, context_js_1.isGenAiSuppressed)()) {
|
|
226
217
|
// A framework layer owns this chat span — run the call, emit no span.
|
|
227
218
|
return original.call(this, params, opts);
|
|
228
219
|
}
|
|
229
220
|
const { tracer, sdk, logger } = patchCtx;
|
|
230
221
|
const internalLogger = sdk.getInternalLogger();
|
|
231
|
-
|
|
222
|
+
// Preflight read of CALLER-owned params — a throwing `model` getter must
|
|
223
|
+
// degrade to one plain uninstrumented call, never block the stream.
|
|
224
|
+
let model = "unknown";
|
|
225
|
+
try {
|
|
226
|
+
model = params?.model ?? "unknown";
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return original.call(this, params, opts);
|
|
230
|
+
}
|
|
232
231
|
// Span creation itself can fail. If it does, fall through to calling
|
|
233
232
|
// `original` directly so the user's stream call always runs. Mirrors
|
|
234
233
|
// the Python `_wrap_stream` fall-through pattern.
|
|
@@ -255,7 +254,7 @@ function wrapStream(original) {
|
|
|
255
254
|
(0, core_js_1.safe)(() => recordErrorOnSpan(liveSpan, err), "anthropic.stream.record_error", internalLogger);
|
|
256
255
|
}
|
|
257
256
|
else if (result) {
|
|
258
|
-
(0, core_js_1.safe)(() => api_1.context.with(liveCtx, () => setChatResponseAttrs(liveSpan, sdk, result, logger)), "anthropic.stream.set_response_attrs", internalLogger);
|
|
257
|
+
(0, core_js_1.safe)(() => api_1.context.with(liveCtx, () => setChatResponseAttrs(liveSpan, sdk, result, logger, streamProvider)), "anthropic.stream.set_response_attrs", internalLogger);
|
|
259
258
|
(0, core_js_1.safe)(() => liveSpan.setStatus({ code: api_1.SpanStatusCode.OK }), "anthropic.stream.set_ok_status", internalLogger);
|
|
260
259
|
}
|
|
261
260
|
else {
|
|
@@ -294,10 +293,14 @@ function wrapStream(original) {
|
|
|
294
293
|
// runs with a clean (unsuppressed) ALS store.
|
|
295
294
|
let stream;
|
|
296
295
|
try {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
296
|
+
(0, core_js_1.safe)(() => setChatRequestAttrs(liveSpan, params, sdk, logger, streamProvider), "anthropic.stream.set_request_attrs", internalLogger);
|
|
297
|
+
// `suppressGenAi` is ALS-based (`als.run`), NOT host-controllable, so it
|
|
298
|
+
// may wrap the original stream call. We deliberately do NOT wrap it in
|
|
299
|
+
// `otelContext.with(liveCtx, ...)`: a customer's global ContextManager
|
|
300
|
+
// could throw there and block the stream (host-fault). Log-record→span
|
|
301
|
+
// linkage is carried by the explicit span passed inside
|
|
302
|
+
// setChatRequestAttrs, not by ambient context.
|
|
303
|
+
stream = (0, context_js_1.runWithContext)({ suppressGenAi: true }, () => original.call(this, params, opts));
|
|
301
304
|
}
|
|
302
305
|
catch (err) {
|
|
303
306
|
// Sync throw from the original call would otherwise leak the span —
|
|
@@ -667,9 +670,9 @@ function wrapStream(original) {
|
|
|
667
670
|
}
|
|
668
671
|
};
|
|
669
672
|
}
|
|
670
|
-
function setChatRequestAttrs(span, params, sdk, logger) {
|
|
673
|
+
function setChatRequestAttrs(span, params, sdk, logger, provider = "anthropic") {
|
|
671
674
|
span.setAttribute(semconv_js_1.GEN_AI.OPERATION_NAME, "chat");
|
|
672
|
-
span.setAttribute(semconv_js_1.GEN_AI.PROVIDER_NAME,
|
|
675
|
+
span.setAttribute(semconv_js_1.GEN_AI.PROVIDER_NAME, provider);
|
|
673
676
|
const model = params?.model ?? "unknown";
|
|
674
677
|
span.setAttribute(semconv_js_1.GEN_AI.REQUEST_MODEL, model);
|
|
675
678
|
const sessionId = (0, context_js_1.getSessionId)();
|
|
@@ -693,10 +696,15 @@ function setChatRequestAttrs(span, params, sdk, logger) {
|
|
|
693
696
|
}
|
|
694
697
|
if (Array.isArray(params?.messages)) {
|
|
695
698
|
span.setAttribute(semconv_js_1.STRUCT.INPUT_MESSAGE_COUNT, params.messages.length);
|
|
696
|
-
|
|
699
|
+
// Parent-prompt preview is CONTENT — never emit it in ContentCaptureMode.None.
|
|
700
|
+
// Deliberately kept in EventOnly (the default): shipped behavior in both
|
|
701
|
+
// SDKs and what the waterfall UI reads.
|
|
702
|
+
if (sdk.captureContent) {
|
|
703
|
+
(0, genai_content_js_2.propagateUserPromptToParent)((0, anthropic_content_js_1.lastUserMessageParts)(params.messages));
|
|
704
|
+
}
|
|
697
705
|
}
|
|
698
706
|
if (sdk.emitEvents && logger && Array.isArray(params?.messages)) {
|
|
699
|
-
(0, events_js_1.emitAnthropicMessageEvents)(logger, params.messages, params.system);
|
|
707
|
+
(0, events_js_1.emitAnthropicMessageEvents)(logger, params.messages, params.system, span, provider);
|
|
700
708
|
}
|
|
701
709
|
if (sdk.emitSpanContent) {
|
|
702
710
|
if (Array.isArray(params?.messages)) {
|
|
@@ -710,7 +718,7 @@ function setChatRequestAttrs(span, params, sdk, logger) {
|
|
|
710
718
|
}
|
|
711
719
|
}
|
|
712
720
|
}
|
|
713
|
-
function setChatResponseAttrs(span, sdk, response, logger) {
|
|
721
|
+
function setChatResponseAttrs(span, sdk, response, logger, provider = "anthropic") {
|
|
714
722
|
const usage = response.usage;
|
|
715
723
|
if (usage) {
|
|
716
724
|
const inputTokens = usage.input_tokens ?? 0;
|
|
@@ -740,7 +748,7 @@ function setChatResponseAttrs(span, sdk, response, logger) {
|
|
|
740
748
|
// Always runs — populates pending queue regardless of content-capture mode.
|
|
741
749
|
recordPendingToolCalls(response.content);
|
|
742
750
|
if (sdk.emitEvents && logger) {
|
|
743
|
-
(0, events_js_1.emitAnthropicChoiceEvent)(logger, response.content, response.stop_reason);
|
|
751
|
+
(0, events_js_1.emitAnthropicChoiceEvent)(logger, response.content, response.stop_reason, span, provider);
|
|
744
752
|
}
|
|
745
753
|
if (sdk.emitSpanContent) {
|
|
746
754
|
span.setAttribute(semconv_js_1.GEN_AI.OUTPUT_MESSAGES, (0, anthropic_content_js_1.toOutputMessages)(response.content, response.stop_reason));
|
|
@@ -756,26 +764,6 @@ function recordPendingToolCalls(contentBlocks) {
|
|
|
756
764
|
(0, context_js_1.ensurePendingToolCallsSlot)();
|
|
757
765
|
(0, context_js_1.pushPendingToolCalls)(pairs);
|
|
758
766
|
}
|
|
759
|
-
function propagateUserPromptToParent(messages) {
|
|
760
|
-
try {
|
|
761
|
-
const agentSpan = (0, context_js_1.getAgentSpan)();
|
|
762
|
-
if (!agentSpan)
|
|
763
|
-
return;
|
|
764
|
-
// The SDK span type doesn't expose `attributes` — we use a brand check on
|
|
765
|
-
// the SDK's ReadableSpan-like shape.
|
|
766
|
-
const agentAttrs = agentSpan
|
|
767
|
-
.attributes;
|
|
768
|
-
if (agentAttrs && agentAttrs[semconv_js_1.GEN_AI.INPUT_MESSAGES])
|
|
769
|
-
return;
|
|
770
|
-
const parts = (0, anthropic_content_js_1.lastUserMessageParts)(messages);
|
|
771
|
-
if (!parts)
|
|
772
|
-
return;
|
|
773
|
-
agentSpan.setAttribute(semconv_js_1.GEN_AI.INPUT_MESSAGES, (0, truncation_js_1.truncateAndSerialize)([{ role: "user", parts }]));
|
|
774
|
-
}
|
|
775
|
-
catch {
|
|
776
|
-
/* never fail the application for telemetry */
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
767
|
function recordErrorOnSpan(span, err) {
|
|
780
768
|
const errorType = err instanceof Error ? err.constructor.name : typeof err;
|
|
781
769
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -16,6 +16,14 @@ const INTEGRATIONS = [
|
|
|
16
16
|
await mod.patch(sdk);
|
|
17
17
|
},
|
|
18
18
|
},
|
|
19
|
+
{
|
|
20
|
+
name: "openai",
|
|
21
|
+
probe: () => import("openai"),
|
|
22
|
+
apply: async (sdk) => {
|
|
23
|
+
const mod = await import("./openai.js");
|
|
24
|
+
await mod.patch(sdk);
|
|
25
|
+
},
|
|
26
|
+
},
|
|
19
27
|
{
|
|
20
28
|
name: "langchain",
|
|
21
29
|
probe: () => import("@langchain/core/language_models/chat_models"),
|
|
@@ -282,5 +282,8 @@ export declare class StructCallbackHandler {
|
|
|
282
282
|
private resolveAgentSessionId;
|
|
283
283
|
private propagateUserPrompt;
|
|
284
284
|
}
|
|
285
|
+
declare function detectProviderFromSerialized(llm: Serialized): string;
|
|
286
|
+
/** @internal */
|
|
287
|
+
export declare const _detectProviderFromSerializedForTest: typeof detectProviderFromSerialized;
|
|
285
288
|
export {};
|
|
286
289
|
//# sourceMappingURL=langchain-callback.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.StructCallbackHandler = void 0;
|
|
3
|
+
exports._detectProviderFromSerializedForTest = exports.StructCallbackHandler = void 0;
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const api_1 = require("@opentelemetry/api");
|
|
6
6
|
const context_js_1 = require("../context.js");
|
|
@@ -330,7 +330,9 @@ class StructCallbackHandler {
|
|
|
330
330
|
const startedSpan = span;
|
|
331
331
|
(0, core_js_1.safe)(() => {
|
|
332
332
|
startedSpan.setAttribute(semconv_js_1.GEN_AI.OPERATION_NAME, "invoke_agent");
|
|
333
|
-
|
|
333
|
+
// gen_ai.provider.name is stamped later by the first chat run under
|
|
334
|
+
// this agent (write-once) — "langchain" is a framework, not a
|
|
335
|
+
// provider, and the real one isn't known at chain start.
|
|
334
336
|
startedSpan.setAttribute(semconv_js_1.GEN_AI.AGENT_NAME, String(agentName));
|
|
335
337
|
// Do NOT set gen_ai.agent.id from sessionId — that conflates agent
|
|
336
338
|
// identity (spec: stable agent-definition id) with a per-invocation
|
|
@@ -423,6 +425,17 @@ class StructCallbackHandler {
|
|
|
423
425
|
const parentCtx = parentSpan
|
|
424
426
|
? api_1.trace.setSpan(api_1.context.active(), parentSpan)
|
|
425
427
|
: api_1.context.active();
|
|
428
|
+
// CLASS RULE: detection => propagation BEFORE any child-span telemetry —
|
|
429
|
+
// a throwing tracer or failing chat-span write must not leave the healthy
|
|
430
|
+
// ancestor agent provider-less. ("langchain" is the unknown-model
|
|
431
|
+
// fallback sentinel, never propagated.) Parity: python
|
|
432
|
+
// on_chat_model_start stamp_ancestor.
|
|
433
|
+
(0, core_js_1.safe)(() => {
|
|
434
|
+
const providerAncestor = this.inheritedAgentSpan(parentRunId);
|
|
435
|
+
if (providerAncestor && provider !== "langchain") {
|
|
436
|
+
(0, context_js_1.stampProviderOnce)(providerAncestor, provider);
|
|
437
|
+
}
|
|
438
|
+
}, "langchain.handleChatModelStart.stamp_provider", this.internalLogger);
|
|
426
439
|
let span;
|
|
427
440
|
(0, core_js_1.safe)(() => {
|
|
428
441
|
span = this.tracer.startSpan(`chat ${model}`, { kind: api_1.SpanKind.CLIENT }, parentCtx);
|
|
@@ -574,7 +587,8 @@ class StructCallbackHandler {
|
|
|
574
587
|
const queueOwnership = this.resolveQueueOwnership(parentRunId);
|
|
575
588
|
(0, core_js_1.safe)(() => {
|
|
576
589
|
startedSpan.setAttribute(semconv_js_1.GEN_AI.OPERATION_NAME, "execute_tool");
|
|
577
|
-
|
|
590
|
+
// No gen_ai.provider.name: the spec's execute_tool span does not
|
|
591
|
+
// define that attribute.
|
|
578
592
|
startedSpan.setAttribute(semconv_js_1.GEN_AI.TOOL_NAME, String(toolName));
|
|
579
593
|
// Tool call id from metadata (LangChain passes it there for ToolCall
|
|
580
594
|
// inputs) or fallback to the pending queue populated by the LLM's
|
|
@@ -658,7 +672,18 @@ class StructCallbackHandler {
|
|
|
658
672
|
span.setAttribute(semconv_js_1.GEN_AI.TOOL_CALL_RESULT, (0, truncation_js_1.safeJsonStringify)(output).slice(0, 8192));
|
|
659
673
|
}
|
|
660
674
|
}, "langchain.handleToolEnd.set_result", this.internalLogger);
|
|
661
|
-
(0, core_js_1.safe)(() =>
|
|
675
|
+
(0, core_js_1.safe)(() => {
|
|
676
|
+
if (toolOutputSignalsError(output)) {
|
|
677
|
+
span.setAttribute(semconv_js_1.ERROR_TYPE, "tool_error");
|
|
678
|
+
span.setStatus({
|
|
679
|
+
code: api_1.SpanStatusCode.ERROR,
|
|
680
|
+
message: "tool returned an error result",
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
span.setStatus({ code: api_1.SpanStatusCode.OK });
|
|
685
|
+
}
|
|
686
|
+
}, "langchain.handleToolEnd.set_status", this.internalLogger);
|
|
662
687
|
(0, core_js_1.safe)(() => span.end(), "langchain.handleToolEnd.span_end", this.internalLogger);
|
|
663
688
|
};
|
|
664
689
|
handleToolError = (err, runId) => {
|
|
@@ -691,7 +716,8 @@ class StructCallbackHandler {
|
|
|
691
716
|
const sessionId = this.resolveSessionId(parentRunId, metadata);
|
|
692
717
|
(0, core_js_1.safe)(() => {
|
|
693
718
|
startedSpan.setAttribute(semconv_js_1.GEN_AI.OPERATION_NAME, "retrieval");
|
|
694
|
-
|
|
719
|
+
// No gen_ai.provider.name: not an inference span, and "langchain"
|
|
720
|
+
// isn't a provider.
|
|
695
721
|
startedSpan.setAttribute(semconv_js_1.GEN_AI.DATA_SOURCE_ID, String(name));
|
|
696
722
|
if (sessionId)
|
|
697
723
|
startedSpan.setAttribute(semconv_js_1.GEN_AI.CONVERSATION_ID, sessionId);
|
|
@@ -910,6 +936,11 @@ class StructCallbackHandler {
|
|
|
910
936
|
}
|
|
911
937
|
propagateUserPrompt(parentSpan, messages) {
|
|
912
938
|
try {
|
|
939
|
+
// Parent-prompt preview is CONTENT — never emit it in ContentCaptureMode
|
|
940
|
+
// .None (same gate as the anthropic/openai provider paths; kept in
|
|
941
|
+
// EventOnly deliberately: shipped behavior the waterfall UI reads).
|
|
942
|
+
if (!this.sdk.captureContent)
|
|
943
|
+
return;
|
|
913
944
|
const attrs = parentSpan
|
|
914
945
|
.attributes;
|
|
915
946
|
if (attrs && attrs[semconv_js_1.GEN_AI.INPUT_MESSAGES])
|
|
@@ -1116,8 +1147,30 @@ function extractClassName(obj) {
|
|
|
1116
1147
|
function detectProviderFromSerialized(llm) {
|
|
1117
1148
|
const cls = extractClassName(llm) ?? "";
|
|
1118
1149
|
const fake = { constructor: { name: cls }, _llmType: undefined };
|
|
1119
|
-
|
|
1150
|
+
const byClass = (0, langchain_content_js_1.detectProvider)(fake);
|
|
1151
|
+
if (byClass !== "langchain")
|
|
1152
|
+
return byClass;
|
|
1153
|
+
// Module-path fallback (parity with python's _detect_provider_from_serialized):
|
|
1154
|
+
// an unknown/wrapped class under a known provider module (e.g.
|
|
1155
|
+
// langchain_openai) still resolves to the real provider.
|
|
1156
|
+
const ids = llm.id;
|
|
1157
|
+
const modulePath = Array.isArray(ids) && typeof ids[0] === "string" ? ids[0] : "";
|
|
1158
|
+
// SEGMENT-exact matching, scoped to langchain partner packages — substring
|
|
1159
|
+
// classified lookalikes (langchain_notopenai -> openai), and the value gets
|
|
1160
|
+
// write-once stamped onto agent spans, so a false positive is sticky.
|
|
1161
|
+
if (modulePath === "langchain" ||
|
|
1162
|
+
modulePath.startsWith("langchain_") ||
|
|
1163
|
+
modulePath.startsWith("langchain.")) {
|
|
1164
|
+
const segments = modulePath.split(/[._]/).filter(Boolean);
|
|
1165
|
+
for (const [key, provider] of Object.entries(langchain_content_js_1.MODULE_PROVIDER_MAP)) {
|
|
1166
|
+
if (segments.includes(key))
|
|
1167
|
+
return provider;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return "langchain";
|
|
1120
1171
|
}
|
|
1172
|
+
/** @internal */
|
|
1173
|
+
exports._detectProviderFromSerializedForTest = detectProviderFromSerialized;
|
|
1121
1174
|
function extractParam(obj, key) {
|
|
1122
1175
|
if (!obj || typeof obj !== "object")
|
|
1123
1176
|
return undefined;
|
|
@@ -1344,4 +1397,29 @@ function recordError(span, err) {
|
|
|
1344
1397
|
if (err instanceof Error)
|
|
1345
1398
|
span.recordException(err);
|
|
1346
1399
|
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Whether a LangChain tool OUTPUT signals in-band failure: a
|
|
1402
|
+
* ToolMessage with status "error", or an MCP-style isError/is_error
|
|
1403
|
+
* boolean-true flag on the top-level object. Mirrors python
|
|
1404
|
+
* langchain.on_tool_end semantics — keep in lockstep.
|
|
1405
|
+
*/
|
|
1406
|
+
function toolOutputSignalsError(output) {
|
|
1407
|
+
if (typeof output !== "object" || output === null)
|
|
1408
|
+
return false;
|
|
1409
|
+
// Per-key probe isolation: a hostile getter on one key must not mask a
|
|
1410
|
+
// readable sibling. Mirrors python _tool_output_signals_error.
|
|
1411
|
+
return (probe(output, "status") === "error" ||
|
|
1412
|
+
probe(output, "isError") === true ||
|
|
1413
|
+
probe(output, "is_error") === true);
|
|
1414
|
+
}
|
|
1415
|
+
/** Isolated single-property read of untrusted host data (see core.ts
|
|
1416
|
+
* safeProbe — duplicated here because it is module-private there). */
|
|
1417
|
+
function probe(obj, key) {
|
|
1418
|
+
try {
|
|
1419
|
+
return obj[key];
|
|
1420
|
+
}
|
|
1421
|
+
catch {
|
|
1422
|
+
return undefined;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1347
1425
|
//# sourceMappingURL=langchain-callback.js.map
|
|
@@ -15,7 +15,7 @@ const truncation_js_1 = require("../truncation.js");
|
|
|
15
15
|
const semconv_js_1 = require("../semconv.js");
|
|
16
16
|
exports.PROVIDER_MAP = {
|
|
17
17
|
ChatOpenAI: "openai",
|
|
18
|
-
AzureChatOpenAI: "azure.openai",
|
|
18
|
+
AzureChatOpenAI: "azure.ai.openai",
|
|
19
19
|
ChatAnthropic: "anthropic",
|
|
20
20
|
ChatGoogleGenerativeAI: "gcp.generative_ai",
|
|
21
21
|
ChatVertexAI: "gcp.vertex_ai",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Part } from "../genai-content.js";
|
|
2
|
+
/** Map a Responses `message.content` (string or list) to spec parts. */
|
|
3
|
+
export declare function messageContentToParts(content: unknown): Part[];
|
|
4
|
+
/** Map ONE Responses `input` item to `[eventName, role, parts]`, or null. */
|
|
5
|
+
export declare function inputItemToEvent(item: unknown): [string, string, Part[]] | null;
|
|
6
|
+
/** Map ONE `response.output` item to the assistant's choice parts. */
|
|
7
|
+
export declare function outputItemToChoiceParts(item: unknown): Part[];
|
|
8
|
+
/** Normalize the `input` kwarg to a list of items (bare string → one user message). */
|
|
9
|
+
export declare function normalizeInput(input: unknown): unknown[];
|
|
10
|
+
/** Spec parts of the LAST `user` message in `input` (for parent propagation). */
|
|
11
|
+
export declare function lastUserParts(input: unknown): Part[] | undefined;
|
|
12
|
+
/** Every function_call `(name, call_id)` from a response.output (for tool linkage). */
|
|
13
|
+
export declare function iterFunctionCalls(output: unknown): Array<[string, string]>;
|
|
14
|
+
/** Derive a raw finish-reason string from a Responses response. */
|
|
15
|
+
export declare function deriveFinishReason(response: unknown): string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* True when a Responses response is in a terminal state (generation finished).
|
|
18
|
+
* A `background: true` request can return non-terminal (`"queued"` /
|
|
19
|
+
* `"in_progress"`) with no assistant message yet — we must NOT emit a terminal
|
|
20
|
+
* `gen_ai.choice` / finish reason for those. A response with no status is
|
|
21
|
+
* treated as terminal so we never silently drop telemetry for the common
|
|
22
|
+
* synchronous call (or minimal mocks).
|
|
23
|
+
*/
|
|
24
|
+
export declare function isTerminalResponse(response: unknown): boolean;
|
|
25
|
+
/** Map a raw Responses status/derived reason to a spec choice finish reason. */
|
|
26
|
+
export declare function mapChoiceFinishReason(raw: string | undefined): string;
|
|
27
|
+
/** Serialize `input` → spec `gen_ai.input.messages` JSON string. */
|
|
28
|
+
export declare function toInputMessages(input: unknown): string;
|
|
29
|
+
/** Serialize Responses `instructions` → spec `system_instructions` JSON string. */
|
|
30
|
+
export declare function toSystemInstructions(instructions: unknown): string;
|
|
31
|
+
/** Serialize `response.output` → spec `gen_ai.output.messages` JSON string. */
|
|
32
|
+
export declare function toOutputMessages(output: unknown, finishReason: string | undefined): string;
|
|
33
|
+
export declare function safeJsonForTool(obj: unknown): string;
|
|
34
|
+
//# sourceMappingURL=openai-content.d.ts.map
|