@hue-run/sdk 0.1.4 → 0.2.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/ENVIRONMENTS.md +182 -0
- package/EVALUATIONS.md +12 -0
- package/README.md +204 -21
- package/dist/ai-sdk.d.ts +9 -1
- package/dist/ai-sdk.js +37 -2
- package/dist/client.d.ts +130 -5
- package/dist/client.js +518 -110
- package/dist/config.d.ts +11 -2
- package/dist/config.js +50 -4
- package/dist/environment/client.d.ts +73 -0
- package/dist/environment/client.js +209 -0
- package/dist/environment/tools.d.ts +30 -0
- package/dist/environment/tools.js +24 -0
- package/dist/environment/types.d.ts +429 -0
- package/dist/environment/types.js +1 -0
- package/dist/environment.d.ts +5 -0
- package/dist/environment.js +2 -0
- package/dist/evals/attempt.d.ts +454 -0
- package/dist/evals/attempt.js +687 -0
- package/dist/evals/client.d.ts +99 -5
- package/dist/evals/client.js +136 -7
- package/dist/evals/environment-evidence.d.ts +6 -0
- package/dist/evals/environment-evidence.js +123 -0
- package/dist/evals/environment-json.d.ts +3 -0
- package/dist/evals/environment-json.js +76 -0
- package/dist/evals/json.d.ts +9 -1
- package/dist/evals/json.js +14 -6
- package/dist/evals/runner.d.ts +61 -2
- package/dist/evals/runner.js +71 -9
- package/dist/evals/scorer-publication.d.ts +2 -0
- package/dist/evals/scorer-publication.js +84 -0
- package/dist/evals/scorers.d.ts +11 -0
- package/dist/evals/scorers.js +56 -5
- package/dist/evals/simulation.d.ts +184 -0
- package/dist/evals/simulation.js +603 -0
- package/dist/evals/types.d.ts +304 -0
- package/dist/evals.d.ts +5 -1
- package/dist/evals.js +3 -1
- package/dist/experimental-telemetry.d.ts +8 -0
- package/dist/experimental-telemetry.js +13 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/managed.d.ts +51 -1
- package/dist/managed.js +11 -1
- package/dist/privacy.d.ts +2 -0
- package/dist/privacy.js +54 -21
- package/dist/receipt.d.ts +12 -1
- package/dist/receipt.js +10 -1
- package/dist/safety.d.ts +7 -0
- package/dist/safety.js +179 -0
- package/dist/snapshot.d.ts +12 -0
- package/dist/snapshot.js +200 -0
- package/dist/transport.d.ts +46 -8
- package/dist/transport.js +266 -48
- package/dist/types.d.ts +167 -8
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/package.json +51 -15
package/dist/client.js
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import { context, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
|
|
2
|
+
import { context, isSpanContextValid, ROOT_CONTEXT, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
|
|
3
|
+
import { defaultTextMapGetter, defaultTextMapSetter } from "@opentelemetry/api";
|
|
3
4
|
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
5
|
+
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
4
6
|
import { LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
5
7
|
import { TracerProvider } from "@opentelemetry/sdk-trace";
|
|
6
|
-
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
7
|
-
import {
|
|
8
|
+
import { defaultResource, resourceFromAttributes } from "@opentelemetry/resources";
|
|
9
|
+
import { encodeContent, noopSpan, safeSpan } from "./safety.js";
|
|
8
10
|
import { createHueTransport, HueExportError } from "./transport.js";
|
|
9
11
|
import { verifyTrace } from "./receipt.js";
|
|
12
|
+
import { sdkVersion } from "./version.js";
|
|
13
|
+
/**
|
|
14
|
+
* Thrown by {@link HueClient.checkConnection} and {@link HueClient.verifyTrace} when Hue cannot be
|
|
15
|
+
* reached, rejects the project key or answers unexpectedly. The message is fixed and safe to log;
|
|
16
|
+
* the underlying network, timeout or parsing error, when there is one, is available as `cause`.
|
|
17
|
+
*/
|
|
10
18
|
export class HueConnectionError extends Error {
|
|
11
19
|
status;
|
|
12
|
-
constructor(message,
|
|
13
|
-
|
|
20
|
+
constructor(message,
|
|
21
|
+
/** HTTP status when Hue answered; absent for network, timeout and parsing failures. */
|
|
22
|
+
status, options) {
|
|
23
|
+
super(message, options);
|
|
14
24
|
this.status = status;
|
|
15
25
|
this.name = "HueConnectionError";
|
|
16
26
|
}
|
|
@@ -26,24 +36,72 @@ function identifier(value) {
|
|
|
26
36
|
throw new TypeError("Session/user identifiers must contain 1–4096 valid characters");
|
|
27
37
|
return value;
|
|
28
38
|
}
|
|
39
|
+
/** A usable metadata label: a non-blank string of at most 256 characters. */
|
|
40
|
+
function isLabel(value) {
|
|
41
|
+
return typeof value === "string" && value.trim() !== "" && value.length <= 256;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Runs `work` exactly once with `active` as OpenTelemetry's current context, so instrumentations
|
|
45
|
+
* that use the global API parent under the Hue span. Without a registered context manager
|
|
46
|
+
* `context.with` only calls `work`; a failing manager is counted and cannot skip or rerun `work`.
|
|
47
|
+
*/
|
|
48
|
+
function runInContext(active, work, failed) {
|
|
49
|
+
const slot = {};
|
|
50
|
+
const invoke = () => {
|
|
51
|
+
try {
|
|
52
|
+
return { value: work() };
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return { error };
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
context.with(active, () => {
|
|
60
|
+
slot.outcome = invoke();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
failed();
|
|
65
|
+
}
|
|
66
|
+
const settled = slot.outcome ?? invoke();
|
|
67
|
+
if ("error" in settled)
|
|
68
|
+
throw settled.error;
|
|
69
|
+
return settled.value;
|
|
70
|
+
}
|
|
71
|
+
/** Error class name for `error.type` and `exception.type`; never the message or stack. */
|
|
72
|
+
function errorType(error) {
|
|
73
|
+
return error instanceof Error && error.name ? String(error.name) : "Error";
|
|
74
|
+
}
|
|
29
75
|
/** Local async context preserves nesting without registering or replacing global OTel providers. */
|
|
30
76
|
class ContextualTracer {
|
|
31
77
|
source;
|
|
32
78
|
storage;
|
|
33
|
-
|
|
79
|
+
enabled;
|
|
80
|
+
failed;
|
|
81
|
+
constructor(source, storage, enabled, failed) {
|
|
34
82
|
this.source = source;
|
|
35
83
|
this.storage = storage;
|
|
84
|
+
this.enabled = enabled;
|
|
85
|
+
this.failed = failed;
|
|
36
86
|
}
|
|
37
87
|
startSpan(name, options = {}, parent) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
...
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
88
|
+
if (!this.enabled())
|
|
89
|
+
return noopSpan();
|
|
90
|
+
try {
|
|
91
|
+
const active = this.storage.getStore();
|
|
92
|
+
return safeSpan(this.source.startSpan(name, {
|
|
93
|
+
...options,
|
|
94
|
+
attributes: {
|
|
95
|
+
...options.attributes,
|
|
96
|
+
...(active?.sessionId ? { "gen_ai.conversation.id": active.sessionId } : {}),
|
|
97
|
+
...(active?.userId ? { "user.id": active.userId } : {}),
|
|
98
|
+
},
|
|
99
|
+
}, parent ?? active?.context ?? context.active()), this.failed);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
this.failed();
|
|
103
|
+
return noopSpan();
|
|
104
|
+
}
|
|
47
105
|
}
|
|
48
106
|
startActiveSpan(name, optionsOrFn, contextOrFn, fn) {
|
|
49
107
|
const options = typeof optionsOrFn === "function" ? {} : optionsOrFn;
|
|
@@ -54,17 +112,48 @@ class ContextualTracer {
|
|
|
54
112
|
: fn;
|
|
55
113
|
if (!callback)
|
|
56
114
|
throw new TypeError("A span callback is required");
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
115
|
+
let parent = ROOT_CONTEXT;
|
|
116
|
+
try {
|
|
117
|
+
parent =
|
|
118
|
+
typeof contextOrFn === "object"
|
|
119
|
+
? contextOrFn
|
|
120
|
+
: (this.storage.getStore()?.context ?? context.active());
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
this.failed();
|
|
124
|
+
}
|
|
60
125
|
const span = this.startSpan(name, options, parent);
|
|
61
|
-
|
|
126
|
+
// A disabled client creates no span and leaves the application's active context untouched.
|
|
127
|
+
const created = isSpanContextValid(span.spanContext());
|
|
128
|
+
let active = parent;
|
|
129
|
+
if (created)
|
|
130
|
+
try {
|
|
131
|
+
active = trace.setSpan(parent, span);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
this.failed();
|
|
135
|
+
active = trace.setSpan(ROOT_CONTEXT, span);
|
|
136
|
+
}
|
|
137
|
+
return this.storage.run({ ...this.storage.getStore(), context: active }, () => created
|
|
138
|
+
? runInContext(active, () => callback(span), this.failed)
|
|
139
|
+
: callback(span));
|
|
62
140
|
}
|
|
63
141
|
}
|
|
142
|
+
const propagator = new W3CTraceContextPropagator();
|
|
143
|
+
/**
|
|
144
|
+
* Hue tracing client. Helpers create spans through a private tracer and local async context, never
|
|
145
|
+
* through global OpenTelemetry registration, and they fail open: capture problems are counted in
|
|
146
|
+
* the export report while application code runs and returns unchanged.
|
|
147
|
+
*/
|
|
64
148
|
export class HueClient {
|
|
149
|
+
/** Export pipeline: counters, issue history and the processors that feed Hue. */
|
|
65
150
|
transport;
|
|
151
|
+
/** Hue's tracer for other instrumentations; spans parent under `withSpan` and inherit identifiers. */
|
|
66
152
|
tracer;
|
|
153
|
+
/** Whether helpers record content, as configured. */
|
|
67
154
|
captureContent;
|
|
155
|
+
/** False for `enabled: false` clients and for `createHueSafe` fallbacks; helpers then only run callbacks. */
|
|
156
|
+
enabled;
|
|
68
157
|
logger;
|
|
69
158
|
storage = new AsyncLocalStorage();
|
|
70
159
|
tracerProvider;
|
|
@@ -73,135 +162,348 @@ export class HueClient {
|
|
|
73
162
|
closed = false;
|
|
74
163
|
shutdownPromise;
|
|
75
164
|
flushPromise;
|
|
165
|
+
safeFlushPromise;
|
|
166
|
+
safeShutdownPromise;
|
|
76
167
|
constructor(options) {
|
|
77
168
|
if ("transport" in options) {
|
|
78
169
|
this.transport = options.transport;
|
|
79
170
|
this.tracerProvider = options.tracerProvider;
|
|
80
171
|
this.loggerProvider = options.loggerProvider;
|
|
172
|
+
if (this.transport.options.resourceAttributes !== undefined)
|
|
173
|
+
this.transport.issue("traces", "warning", 0, "resourceAttributes are ignored in attach mode; configure the resource on the application's providers");
|
|
81
174
|
}
|
|
82
175
|
else {
|
|
83
176
|
this.transport = createHueTransport(options);
|
|
84
|
-
|
|
85
|
-
|
|
177
|
+
// The OpenTelemetry default resource supplies the required telemetry.sdk.* attributes and
|
|
178
|
+
// Hue's service identity is merged on top of any caller-supplied resourceAttributes, so
|
|
179
|
+
// serviceName and serviceVersion take precedence over same-named keys. Nothing here reads
|
|
180
|
+
// environment variables.
|
|
181
|
+
const resource = defaultResource().merge(resourceFromAttributes({
|
|
182
|
+
...options.resourceAttributes,
|
|
183
|
+
"service.name": this.transport.options.serviceName,
|
|
86
184
|
...(options.serviceVersion ? { "service.version": options.serviceVersion } : {}),
|
|
87
|
-
});
|
|
185
|
+
}));
|
|
88
186
|
const tracer = new TracerProvider({
|
|
89
187
|
resource,
|
|
90
|
-
spanProcessors: [this.transport.spanProcessor],
|
|
188
|
+
spanProcessors: this.transport.options.enabled === false ? [] : [this.transport.spanProcessor],
|
|
91
189
|
});
|
|
92
190
|
const logger = new LoggerProvider({
|
|
93
191
|
resource,
|
|
94
|
-
processors: [this.transport.logRecordProcessor],
|
|
192
|
+
processors: this.transport.options.enabled === false ? [] : [this.transport.logRecordProcessor],
|
|
95
193
|
});
|
|
96
194
|
this.ownedProviders = { tracer, logger };
|
|
97
195
|
this.tracerProvider = tracer;
|
|
98
196
|
this.loggerProvider = logger;
|
|
99
197
|
}
|
|
100
198
|
this.captureContent = this.transport.options.captureContent;
|
|
101
|
-
this.
|
|
102
|
-
this.
|
|
199
|
+
this.enabled = this.transport.options.enabled !== false;
|
|
200
|
+
this.tracer = new ContextualTracer(this.tracerProvider.getTracer("@hue-run/sdk", sdkVersion), this.storage, () => this.enabled && !this.closed, () => this.transport.instrumentationFailure());
|
|
201
|
+
this.logger = this.loggerProvider.getLogger("@hue-run/sdk", sdkVersion);
|
|
103
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Verifies that Hue stored a trace by ID, optionally waiting for expected span IDs and normalized
|
|
205
|
+
* fields within the budget. Read-only; it does not flush or inspect content.
|
|
206
|
+
*
|
|
207
|
+
* @throws HueConnectionError when the client is disabled.
|
|
208
|
+
* @throws HueTraceVerificationError for authentication, unsupported endpoint, transport or invalid response failures.
|
|
209
|
+
* @throws TypeError for an invalid trace ID or options.
|
|
210
|
+
*/
|
|
104
211
|
verifyTrace(traceId, options = {}) {
|
|
212
|
+
if (!this.enabled)
|
|
213
|
+
return Promise.reject(new HueConnectionError("Hue telemetry is disabled"));
|
|
105
214
|
return verifyTrace(this.transport.options, traceId, options);
|
|
106
215
|
}
|
|
216
|
+
/** The helper's current context: the innermost active Hue span, else the OpenTelemetry active context. */
|
|
107
217
|
getContext() {
|
|
108
|
-
|
|
218
|
+
try {
|
|
219
|
+
return this.storage.getStore()?.context ?? context.active();
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
this.transport.instrumentationFailure();
|
|
223
|
+
return ROOT_CONTEXT;
|
|
224
|
+
}
|
|
109
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Runs `callback` inside a new span that nests under the active Hue span. Application errors are
|
|
228
|
+
* recorded on the span and rethrown unchanged; the span always ends when the callback settles.
|
|
229
|
+
*/
|
|
110
230
|
async withSpan(name, callback, options = {}) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
span,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
231
|
+
let span = noopSpan();
|
|
232
|
+
let active = { context: this.getContext() };
|
|
233
|
+
if (this.enabled && !this.closed) {
|
|
234
|
+
try {
|
|
235
|
+
const inherited = this.storage.getStore();
|
|
236
|
+
active = {
|
|
237
|
+
context: options.parentContext ?? inherited?.context ?? context.active(),
|
|
238
|
+
sessionId: identifier(options.sessionId ?? inherited?.sessionId),
|
|
239
|
+
userId: identifier(options.userId ?? inherited?.userId),
|
|
240
|
+
model: inherited?.model,
|
|
241
|
+
};
|
|
242
|
+
span = this.storage.run(active, () => this.tracer.startSpan(name, { kind: options.kind ?? SpanKind.INTERNAL, attributes: options.attributes }, active.context));
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
this.transport.instrumentationFailure();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// A disabled or failed client creates no span; the application's own active span then stays
|
|
249
|
+
// visible through getContext(), inject() and HueSpan.context instead of an invalid one.
|
|
250
|
+
const created = isSpanContextValid(span.spanContext());
|
|
251
|
+
let spanContext = active.context;
|
|
252
|
+
if (created)
|
|
253
|
+
try {
|
|
254
|
+
spanContext = trace.setSpan(active.context, span);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
this.transport.instrumentationFailure();
|
|
258
|
+
spanContext = trace.setSpan(ROOT_CONTEXT, span);
|
|
259
|
+
}
|
|
260
|
+
const handle = {
|
|
261
|
+
span,
|
|
262
|
+
context: spanContext,
|
|
263
|
+
traceId: span.spanContext().traceId,
|
|
264
|
+
spanId: span.spanContext().spanId,
|
|
265
|
+
setInput: (value) => this.setContent(span, "input.value", value),
|
|
266
|
+
setOutput: (value) => this.setContent(span, "output.value", value),
|
|
267
|
+
setUsage: (usage) => this.setUsage(span, usage),
|
|
268
|
+
};
|
|
269
|
+
const execute = async () => {
|
|
270
|
+
// Setup, capture and cleanup have separate failure boundaries from customer code.
|
|
271
|
+
try {
|
|
272
|
+
if (this.enabled && !this.closed && options.input !== undefined)
|
|
273
|
+
handle.setInput(options.input);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
this.transport.instrumentationFailure();
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
return await callback(handle);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
this.recordError(span, error);
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
span.end();
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
// The span is also OpenTelemetry's active span while the callback runs, so spans from other
|
|
290
|
+
// instrumentations (HTTP clients, provider SDKs) join this trace when the application has
|
|
291
|
+
// registered a context manager. Hue still registers none itself.
|
|
292
|
+
return this.storage.run({ ...active, context: spanContext }, () => created
|
|
293
|
+
? runInContext(spanContext, execute, () => this.transport.instrumentationFailure())
|
|
294
|
+
: execute());
|
|
144
295
|
}
|
|
145
|
-
|
|
146
|
-
|
|
296
|
+
/**
|
|
297
|
+
* Runs `execute` inside an `execute_tool` span named after the tool. `input` and a defined result
|
|
298
|
+
* are recorded as `gen_ai.tool.call.arguments` / `gen_ai.tool.call.result` when `captureContent`
|
|
299
|
+
* is true; values that are not JSON-encodable are omitted with an instrumentation failure.
|
|
300
|
+
* `options.callId` is recorded as `gen_ai.tool.call.id`, like the Python `call_id=` keyword.
|
|
301
|
+
*/
|
|
302
|
+
async tool(name, input, execute, options = {}) {
|
|
303
|
+
const attributes = {
|
|
304
|
+
"gen_ai.operation.name": "execute_tool",
|
|
305
|
+
"gen_ai.tool.name": name,
|
|
306
|
+
};
|
|
307
|
+
const callId = options.callId;
|
|
308
|
+
if (callId !== undefined) {
|
|
309
|
+
// A blank or non-string id is omitted and counted; the tool call itself still runs.
|
|
310
|
+
if (isLabel(callId))
|
|
311
|
+
attributes["gen_ai.tool.call.id"] = callId;
|
|
312
|
+
else if (this.enabled && !this.closed)
|
|
313
|
+
this.transport.instrumentationFailure();
|
|
314
|
+
}
|
|
315
|
+
return this.withSpan(`execute_tool ${name}`, async ({ span }) => {
|
|
147
316
|
this.setContent(span, "gen_ai.tool.call.arguments", input);
|
|
148
317
|
const result = await execute();
|
|
149
318
|
if (result !== undefined)
|
|
150
319
|
this.setContent(span, "gen_ai.tool.call.result", result);
|
|
151
320
|
return result;
|
|
152
|
-
}, {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const type = error instanceof Error ? error.name : "Error";
|
|
156
|
-
span.setStatus({
|
|
157
|
-
code: SpanStatusCode.ERROR,
|
|
158
|
-
...(this.captureContent
|
|
159
|
-
? { message: error instanceof Error ? error.message : "Operation failed" }
|
|
160
|
-
: {}),
|
|
321
|
+
}, {
|
|
322
|
+
attributes,
|
|
323
|
+
...(options.parentContext ? { parentContext: options.parentContext } : {}),
|
|
161
324
|
});
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Runs `callback` inside a GenAI client span for one direct provider call, named
|
|
328
|
+
* `{operation} {model}` unless `options.name` is given and carrying `gen_ai.operation.name`,
|
|
329
|
+
* `gen_ai.request.model` and `gen_ai.provider.name`. The argument order matches `withSpan`. The
|
|
330
|
+
* handle's `setInput`/`setOutput` record `gen_ai.input.messages` / `gen_ai.output.messages`,
|
|
331
|
+
* which should use the GenAI semantic-convention message shape; `recordMessages` inside the
|
|
332
|
+
* callback inherits the request metadata.
|
|
333
|
+
*/
|
|
334
|
+
async model(model, callback, options) {
|
|
335
|
+
// A disabled or closed client creates no span, so invalid metadata is not an instrumentation
|
|
336
|
+
// failure either; only an active client records it (matching the other helpers).
|
|
337
|
+
const active = this.enabled && !this.closed;
|
|
338
|
+
const label = (value, fallback) => {
|
|
339
|
+
if (isLabel(value))
|
|
340
|
+
return value;
|
|
341
|
+
if (active)
|
|
342
|
+
this.transport.instrumentationFailure();
|
|
343
|
+
return fallback;
|
|
344
|
+
};
|
|
345
|
+
const requestModel = label(model, "unknown");
|
|
346
|
+
const operation = label(options?.operation ?? "chat", "chat");
|
|
347
|
+
const provider = label(options?.provider, "unknown");
|
|
348
|
+
const name = options?.name === undefined
|
|
349
|
+
? `${operation} ${requestModel}`
|
|
350
|
+
: label(options.name, `${operation} ${requestModel}`);
|
|
351
|
+
const metadata = { operation, provider, requestModel };
|
|
352
|
+
const { sessionId, userId, parentContext, input } = options ?? {};
|
|
353
|
+
return this.withSpan(name, (span) => {
|
|
354
|
+
const handle = {
|
|
355
|
+
...span,
|
|
356
|
+
setInput: (value) => this.setContent(span.span, "gen_ai.input.messages", value),
|
|
357
|
+
setOutput: (value) => this.setContent(span.span, "gen_ai.output.messages", value),
|
|
358
|
+
};
|
|
359
|
+
if (input !== undefined)
|
|
360
|
+
handle.setInput(input);
|
|
361
|
+
// recordMessages inside the callback copies this request metadata onto its log record.
|
|
362
|
+
const store = this.storage.getStore() ?? { context: span.context };
|
|
363
|
+
return this.storage.run({ ...store, model: metadata }, () => callback(handle));
|
|
364
|
+
}, {
|
|
365
|
+
sessionId,
|
|
366
|
+
userId,
|
|
367
|
+
parentContext,
|
|
368
|
+
kind: SpanKind.CLIENT,
|
|
369
|
+
attributes: {
|
|
370
|
+
"gen_ai.operation.name": operation,
|
|
371
|
+
"gen_ai.request.model": requestModel,
|
|
372
|
+
"gen_ai.provider.name": provider,
|
|
373
|
+
},
|
|
170
374
|
});
|
|
171
375
|
}
|
|
376
|
+
setUsage(span, usage) {
|
|
377
|
+
if (!this.enabled || this.closed)
|
|
378
|
+
return;
|
|
379
|
+
for (const [key, value] of [
|
|
380
|
+
["gen_ai.usage.input_tokens", usage?.inputTokens],
|
|
381
|
+
["gen_ai.usage.output_tokens", usage?.outputTokens],
|
|
382
|
+
]) {
|
|
383
|
+
if (value === undefined)
|
|
384
|
+
continue;
|
|
385
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
386
|
+
this.transport.instrumentationFailure();
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
span.setAttribute(key, value);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
this.transport.instrumentationFailure();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Writes W3C `traceparent` for the active span into a carrier; never the API key or baggage.
|
|
399
|
+
* Propagation also runs for a disabled or closed client so downstream tracing stays connected.
|
|
400
|
+
*/
|
|
401
|
+
inject(carrier, activeContext = this.getContext()) {
|
|
402
|
+
try {
|
|
403
|
+
propagator.inject(activeContext, carrier, defaultTextMapSetter);
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
this.transport.instrumentationFailure();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/** Reads W3C trace context from a carrier for use as `parentContext`. */
|
|
410
|
+
extract(carrier) {
|
|
411
|
+
try {
|
|
412
|
+
return propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
this.transport.instrumentationFailure();
|
|
416
|
+
return ROOT_CONTEXT;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Marks a span failed the same way in every helper: `error.type`, an ERROR status without a
|
|
421
|
+
* description and an `exception` event carrying only the type. Exception messages and stack
|
|
422
|
+
* traces are never recorded, whatever `captureContent` is, matching the Python SDK.
|
|
423
|
+
*/
|
|
424
|
+
recordError(span, error) {
|
|
425
|
+
if (!this.enabled || this.closed)
|
|
426
|
+
return;
|
|
427
|
+
try {
|
|
428
|
+
const type = errorType(error);
|
|
429
|
+
span.setAttribute("error.type", type);
|
|
430
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
431
|
+
span.addEvent("exception", { "exception.type": type });
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
this.transport.instrumentationFailure();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Emits a `gen_ai.client.inference.operation.details` log record correlated with the active (or
|
|
439
|
+
* given) span, carrying the messages in its body. `gen_ai.operation.name`, `gen_ai.provider.name`
|
|
440
|
+
* and `gen_ai.request.model` are set as record attributes from the caller's values or the
|
|
441
|
+
* enclosing {@link model} span, and `gen_ai.conversation.id` from the active session. Nothing is
|
|
442
|
+
* emitted when `captureContent` is false; capture failures are counted, never thrown.
|
|
443
|
+
*/
|
|
172
444
|
recordMessages(messages, explicitContext) {
|
|
173
|
-
if (this.closed)
|
|
174
|
-
throw new Error("Hue client is shut down");
|
|
175
|
-
if (!this.captureContent)
|
|
445
|
+
if (!this.enabled || this.closed || !this.captureContent)
|
|
176
446
|
return;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
body
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
447
|
+
try {
|
|
448
|
+
const store = this.storage.getStore();
|
|
449
|
+
const active = explicitContext ?? store?.context ?? context.active();
|
|
450
|
+
if (!trace.getSpanContext(active))
|
|
451
|
+
throw new Error("Message records require an active span or explicit span context");
|
|
452
|
+
const body = {};
|
|
453
|
+
if (messages.input !== undefined)
|
|
454
|
+
body["gen_ai.input.messages"] = messages.input;
|
|
455
|
+
if (messages.output !== undefined)
|
|
456
|
+
body["gen_ai.output.messages"] = messages.output;
|
|
457
|
+
// Request metadata is inherited only when the record correlates with the enclosing helper
|
|
458
|
+
// scope; an unrelated explicit context carries caller-supplied values alone.
|
|
459
|
+
const enclosing = explicitContext === undefined || explicitContext === store?.context ? store : undefined;
|
|
460
|
+
const attributes = {};
|
|
461
|
+
const stamp = (key, explicit, inherited) => {
|
|
462
|
+
if (explicit === undefined) {
|
|
463
|
+
if (inherited !== undefined)
|
|
464
|
+
attributes[key] = inherited;
|
|
465
|
+
}
|
|
466
|
+
else if (typeof explicit === "string" && explicit.trim() && explicit.length <= 256)
|
|
467
|
+
attributes[key] = explicit;
|
|
468
|
+
else
|
|
469
|
+
this.transport.instrumentationFailure("logs");
|
|
470
|
+
};
|
|
471
|
+
stamp("gen_ai.operation.name", messages.operation, enclosing?.model?.operation);
|
|
472
|
+
stamp("gen_ai.provider.name", messages.provider, enclosing?.model?.provider);
|
|
473
|
+
stamp("gen_ai.request.model", messages.model, enclosing?.model?.requestModel);
|
|
474
|
+
stamp("gen_ai.conversation.id", undefined, enclosing?.sessionId);
|
|
475
|
+
this.logger.emit({
|
|
476
|
+
context: active,
|
|
477
|
+
severityNumber: SeverityNumber.INFO,
|
|
478
|
+
eventName: "gen_ai.client.inference.operation.details",
|
|
479
|
+
attributes,
|
|
480
|
+
body: JSON.parse(encodeContent(body)),
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
this.transport.instrumentationFailure("logs");
|
|
485
|
+
}
|
|
191
486
|
}
|
|
192
487
|
setContent(span, key, value) {
|
|
193
|
-
if (!this.captureContent)
|
|
488
|
+
if (!this.enabled || this.closed || !this.captureContent)
|
|
194
489
|
return;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
throw new RangeError("Captured content must be JSON and no more than 256 KiB");
|
|
202
|
-
span.setAttribute(key, encoded);
|
|
490
|
+
try {
|
|
491
|
+
span.setAttribute(key, encodeContent(value));
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
this.transport.instrumentationFailure();
|
|
495
|
+
}
|
|
203
496
|
}
|
|
497
|
+
/**
|
|
498
|
+
* Confirms the key and origin by reading the current project; a setup and CI diagnostic, not a
|
|
499
|
+
* readiness gate. Redirects are refused and the response is bounded.
|
|
500
|
+
*
|
|
501
|
+
* @throws HueConnectionError when the client is disabled, Hue is unreachable (the network or
|
|
502
|
+
* timeout error is the `cause`), the key is rejected (`status` is set) or the response is invalid.
|
|
503
|
+
*/
|
|
204
504
|
async checkConnection() {
|
|
505
|
+
if (!this.enabled)
|
|
506
|
+
throw new HueConnectionError("Hue telemetry is disabled");
|
|
205
507
|
const options = this.transport.options;
|
|
206
508
|
let response;
|
|
207
509
|
try {
|
|
@@ -211,8 +513,8 @@ export class HueClient {
|
|
|
211
513
|
signal: AbortSignal.timeout(options.timeoutMillis),
|
|
212
514
|
});
|
|
213
515
|
}
|
|
214
|
-
catch {
|
|
215
|
-
throw new HueConnectionError("Unable to connect to Hue; check the endpoint and network");
|
|
516
|
+
catch (error) {
|
|
517
|
+
throw new HueConnectionError("Unable to connect to Hue; check the endpoint and network", undefined, { cause: error });
|
|
216
518
|
}
|
|
217
519
|
if (!response.ok) {
|
|
218
520
|
await response.body?.cancel();
|
|
@@ -254,10 +556,75 @@ export class HueClient {
|
|
|
254
556
|
slug: fields.slug,
|
|
255
557
|
};
|
|
256
558
|
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
throw new HueConnectionError("Hue returned an invalid project response", undefined, {
|
|
561
|
+
cause: error,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/** Production lifecycle path: never rejects; timeouts do not cancel borrowed provider work. */
|
|
566
|
+
flushSafe(options = {}) {
|
|
567
|
+
if (this.safeFlushPromise)
|
|
568
|
+
return this.safeFlushPromise;
|
|
569
|
+
const work = this.flushPromise ?? this.flush();
|
|
570
|
+
const result = this.safeLifecycle(() => work, options);
|
|
571
|
+
this.safeFlushPromise = result;
|
|
572
|
+
const clear = () => {
|
|
573
|
+
this.safeFlushPromise = undefined;
|
|
574
|
+
};
|
|
575
|
+
// Retain the timed-out result until its underlying drain settles. Repeated
|
|
576
|
+
// timeouts must not accumulate promises against a hung borrowed provider.
|
|
577
|
+
void work.then(clear, clear);
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
580
|
+
/** Safe for finally blocks; preserves the application's result or original exception. */
|
|
581
|
+
shutdownSafe(options = {}) {
|
|
582
|
+
if (this.safeShutdownPromise)
|
|
583
|
+
return this.safeShutdownPromise;
|
|
584
|
+
const work = this.shutdown();
|
|
585
|
+
const result = this.safeLifecycle(() => work, options);
|
|
586
|
+
this.safeShutdownPromise = result;
|
|
587
|
+
const clear = () => {
|
|
588
|
+
this.safeShutdownPromise = undefined;
|
|
589
|
+
};
|
|
590
|
+
void work.then(clear, clear);
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
async safeLifecycle(work, options) {
|
|
594
|
+
let timer;
|
|
595
|
+
try {
|
|
596
|
+
const timeout = options.timeoutMillis ?? 1000;
|
|
597
|
+
if (!Number.isInteger(timeout) || timeout < 1 || timeout > 60000)
|
|
598
|
+
throw new TypeError("Invalid lifecycle budget");
|
|
599
|
+
const outcome = await Promise.race([
|
|
600
|
+
work().then(() => "complete"),
|
|
601
|
+
new Promise((resolve) => {
|
|
602
|
+
timer = setTimeout(() => resolve("timeout"), timeout);
|
|
603
|
+
}),
|
|
604
|
+
]);
|
|
605
|
+
const report = this.transport.getReport();
|
|
606
|
+
return {
|
|
607
|
+
ok: outcome === "complete" &&
|
|
608
|
+
this.transport.getFailureSequence() === 0 &&
|
|
609
|
+
report.pendingSpans + report.pendingLogs === 0,
|
|
610
|
+
timedOut: outcome === "timeout",
|
|
611
|
+
report,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
257
614
|
catch {
|
|
258
|
-
|
|
615
|
+
return { ok: false, timedOut: false, report: this.transport.getReport() };
|
|
616
|
+
}
|
|
617
|
+
finally {
|
|
618
|
+
clearTimeout(timer);
|
|
259
619
|
}
|
|
260
620
|
}
|
|
621
|
+
/**
|
|
622
|
+
* Drains the trace and log providers and waits for Hue's acknowledgements. Each caller gets a
|
|
623
|
+
* fresh serialized drain that includes records emitted before its call.
|
|
624
|
+
*
|
|
625
|
+
* @throws HueExportError when this drain observed a new rejection, delivery failure, drop or
|
|
626
|
+
* invalid record; its `issues` and `report` are sanitized counts, never server text.
|
|
627
|
+
*/
|
|
261
628
|
flush() {
|
|
262
629
|
// Each caller needs a drain after its own preceding span/log emissions.
|
|
263
630
|
// Joining an earlier drain can acknowledge records that were not in its batch.
|
|
@@ -281,14 +648,20 @@ export class HueClient {
|
|
|
281
648
|
}
|
|
282
649
|
async flushOnce() {
|
|
283
650
|
const results = await Promise.allSettled([
|
|
284
|
-
this.tracerProvider.forceFlush(),
|
|
285
|
-
this.loggerProvider.forceFlush(),
|
|
651
|
+
Promise.resolve().then(() => this.tracerProvider.forceFlush()),
|
|
652
|
+
Promise.resolve().then(() => this.loggerProvider.forceFlush()),
|
|
286
653
|
]);
|
|
287
654
|
for (const [index, result] of results.entries())
|
|
288
655
|
if (result.status === "rejected")
|
|
289
656
|
this.transport.issue(index === 0 ? "traces" : "logs", "failed", 0, "OpenTelemetry provider flush failed");
|
|
290
657
|
return this.transport.flush();
|
|
291
658
|
}
|
|
659
|
+
/**
|
|
660
|
+
* Flushes, then shuts down the providers this client owns; borrowed providers are left running.
|
|
661
|
+
* Idempotent: later calls return the same promise, and later helper calls only run their callbacks.
|
|
662
|
+
*
|
|
663
|
+
* @throws HueExportError when the final flush observed new failures; owned providers are still released.
|
|
664
|
+
*/
|
|
292
665
|
shutdown() {
|
|
293
666
|
this.shutdownPromise ??= (async () => {
|
|
294
667
|
this.closed = true;
|
|
@@ -308,6 +681,41 @@ export class HueClient {
|
|
|
308
681
|
return this.shutdownPromise;
|
|
309
682
|
}
|
|
310
683
|
}
|
|
684
|
+
/**
|
|
685
|
+
* Creates a client that owns its providers ({@link HueOptions}) or attaches to the application's
|
|
686
|
+
* ({@link ExistingHueProviders}). Strict: use it for setup and CI, `createHueSafe` in serving code.
|
|
687
|
+
*
|
|
688
|
+
* @throws TypeError for invalid options, including a missing `captureContent` choice, an invalid
|
|
689
|
+
* key or `serviceName`, a non-origin or insecure `baseUrl`, or out-of-range budgets.
|
|
690
|
+
*/
|
|
311
691
|
export function createHue(options) {
|
|
312
692
|
return new HueClient(options);
|
|
313
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Fail-open initialization for production: invalid options return a disabled client with one
|
|
696
|
+
* instrumentation failure recorded instead of throwing. Strict {@link createHue} remains for setup/CI.
|
|
697
|
+
*/
|
|
698
|
+
export function createHueSafe(options) {
|
|
699
|
+
try {
|
|
700
|
+
return new HueClient(options);
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
// The caller's diagnostics hook stays attached and hears why telemetry is off.
|
|
704
|
+
const disabled = new HueClient({ enabled: false, onExportIssue: issueCallback(options) });
|
|
705
|
+
const reason = error instanceof Error && error.message ? error.message : "unknown error";
|
|
706
|
+
disabled.transport.instrumentationFailure("traces", `Hue is disabled: ${reason.slice(0, 256)}`);
|
|
707
|
+
return disabled;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
function issueCallback(options) {
|
|
711
|
+
try {
|
|
712
|
+
const source = options && typeof options === "object" && "transport" in options
|
|
713
|
+
? options.transport?.options
|
|
714
|
+
: options;
|
|
715
|
+
const callback = source?.onExportIssue;
|
|
716
|
+
return typeof callback === "function" ? callback : undefined;
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
return undefined;
|
|
720
|
+
}
|
|
721
|
+
}
|