@hue-run/sdk 0.1.5 → 0.2.1
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 +192 -18
- package/dist/ai-sdk.d.ts +9 -1
- package/dist/ai-sdk.js +34 -8
- package/dist/client.d.ts +121 -6
- package/dist/client.js +329 -56
- package/dist/config.d.ts +11 -2
- package/dist/config.js +36 -7
- 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 +3 -0
- package/dist/index.js +2 -0
- package/dist/managed.d.ts +51 -1
- package/dist/managed.js +11 -1
- package/dist/privacy.d.ts +2 -0
- package/dist/privacy.js +16 -1
- package/dist/receipt.d.ts +12 -1
- package/dist/receipt.js +10 -1
- package/dist/safety.d.ts +1 -2
- package/dist/snapshot.js +4 -0
- package/dist/transport.d.ts +41 -9
- package/dist/transport.js +80 -22
- package/dist/types.d.ts +144 -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, ROOT_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";
|
|
8
|
+
import { defaultResource, resourceFromAttributes } from "@opentelemetry/resources";
|
|
7
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,6 +36,42 @@ 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;
|
|
@@ -77,21 +123,36 @@ class ContextualTracer {
|
|
|
77
123
|
this.failed();
|
|
78
124
|
}
|
|
79
125
|
const span = this.startSpan(name, options, parent);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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));
|
|
89
140
|
}
|
|
90
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
|
+
*/
|
|
91
148
|
export class HueClient {
|
|
149
|
+
/** Export pipeline: counters, issue history and the processors that feed Hue. */
|
|
92
150
|
transport;
|
|
151
|
+
/** Hue's tracer for other instrumentations; spans parent under `withSpan` and inherit identifiers. */
|
|
93
152
|
tracer;
|
|
153
|
+
/** Whether helpers record content, as configured. */
|
|
94
154
|
captureContent;
|
|
155
|
+
/** False for `enabled: false` clients and for `createHueSafe` fallbacks; helpers then only run callbacks. */
|
|
95
156
|
enabled;
|
|
96
157
|
logger;
|
|
97
158
|
storage = new AsyncLocalStorage();
|
|
@@ -108,13 +169,20 @@ export class HueClient {
|
|
|
108
169
|
this.transport = options.transport;
|
|
109
170
|
this.tracerProvider = options.tracerProvider;
|
|
110
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");
|
|
111
174
|
}
|
|
112
175
|
else {
|
|
113
176
|
this.transport = createHueTransport(options);
|
|
114
|
-
|
|
115
|
-
|
|
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,
|
|
116
184
|
...(options.serviceVersion ? { "service.version": options.serviceVersion } : {}),
|
|
117
|
-
});
|
|
185
|
+
}));
|
|
118
186
|
const tracer = new TracerProvider({
|
|
119
187
|
resource,
|
|
120
188
|
spanProcessors: this.transport.options.enabled === false ? [] : [this.transport.spanProcessor],
|
|
@@ -129,14 +197,23 @@ export class HueClient {
|
|
|
129
197
|
}
|
|
130
198
|
this.captureContent = this.transport.options.captureContent;
|
|
131
199
|
this.enabled = this.transport.options.enabled !== false;
|
|
132
|
-
this.tracer = new ContextualTracer(this.tracerProvider.getTracer("@hue-run/sdk",
|
|
133
|
-
this.logger = this.loggerProvider.getLogger("@hue-run/sdk",
|
|
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);
|
|
134
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
|
+
*/
|
|
135
211
|
verifyTrace(traceId, options = {}) {
|
|
136
212
|
if (!this.enabled)
|
|
137
213
|
return Promise.reject(new HueConnectionError("Hue telemetry is disabled"));
|
|
138
214
|
return verifyTrace(this.transport.options, traceId, options);
|
|
139
215
|
}
|
|
216
|
+
/** The helper's current context: the innermost active Hue span, else the OpenTelemetry active context. */
|
|
140
217
|
getContext() {
|
|
141
218
|
try {
|
|
142
219
|
return this.storage.getStore()?.context ?? context.active();
|
|
@@ -146,6 +223,10 @@ export class HueClient {
|
|
|
146
223
|
return ROOT_CONTEXT;
|
|
147
224
|
}
|
|
148
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
|
+
*/
|
|
149
230
|
async withSpan(name, callback, options = {}) {
|
|
150
231
|
let span = noopSpan();
|
|
151
232
|
let active = { context: this.getContext() };
|
|
@@ -156,6 +237,7 @@ export class HueClient {
|
|
|
156
237
|
context: options.parentContext ?? inherited?.context ?? context.active(),
|
|
157
238
|
sessionId: identifier(options.sessionId ?? inherited?.sessionId),
|
|
158
239
|
userId: identifier(options.userId ?? inherited?.userId),
|
|
240
|
+
model: inherited?.model,
|
|
159
241
|
};
|
|
160
242
|
span = this.storage.run(active, () => this.tracer.startSpan(name, { kind: options.kind ?? SpanKind.INTERNAL, attributes: options.attributes }, active.context));
|
|
161
243
|
}
|
|
@@ -163,14 +245,18 @@ export class HueClient {
|
|
|
163
245
|
this.transport.instrumentationFailure();
|
|
164
246
|
}
|
|
165
247
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
+
}
|
|
174
260
|
const handle = {
|
|
175
261
|
span,
|
|
176
262
|
context: spanContext,
|
|
@@ -178,8 +264,9 @@ export class HueClient {
|
|
|
178
264
|
spanId: span.spanContext().spanId,
|
|
179
265
|
setInput: (value) => this.setContent(span, "input.value", value),
|
|
180
266
|
setOutput: (value) => this.setContent(span, "output.value", value),
|
|
267
|
+
setUsage: (usage) => this.setUsage(span, usage),
|
|
181
268
|
};
|
|
182
|
-
|
|
269
|
+
const execute = async () => {
|
|
183
270
|
// Setup, capture and cleanup have separate failure boundaries from customer code.
|
|
184
271
|
try {
|
|
185
272
|
if (this.enabled && !this.closed && options.input !== undefined)
|
|
@@ -198,47 +285,168 @@ export class HueClient {
|
|
|
198
285
|
finally {
|
|
199
286
|
span.end();
|
|
200
287
|
}
|
|
201
|
-
}
|
|
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());
|
|
202
295
|
}
|
|
203
|
-
|
|
204
|
-
|
|
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 }) => {
|
|
205
316
|
this.setContent(span, "gen_ai.tool.call.arguments", input);
|
|
206
317
|
const result = await execute();
|
|
207
318
|
if (result !== undefined)
|
|
208
319
|
this.setContent(span, "gen_ai.tool.call.result", result);
|
|
209
320
|
return result;
|
|
210
|
-
}, {
|
|
321
|
+
}, {
|
|
322
|
+
attributes,
|
|
323
|
+
...(options.parentContext ? { parentContext: options.parentContext } : {}),
|
|
324
|
+
});
|
|
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
|
+
},
|
|
374
|
+
});
|
|
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
|
+
}
|
|
211
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
|
+
*/
|
|
212
424
|
recordError(span, error) {
|
|
213
425
|
if (!this.enabled || this.closed)
|
|
214
426
|
return;
|
|
215
427
|
try {
|
|
216
|
-
const type = error
|
|
217
|
-
span.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
? { message: error instanceof Error ? error.message : "Operation failed" }
|
|
221
|
-
: {}),
|
|
222
|
-
});
|
|
223
|
-
span.addEvent("exception", {
|
|
224
|
-
"exception.type": type,
|
|
225
|
-
...(this.captureContent && error instanceof Error
|
|
226
|
-
? {
|
|
227
|
-
"exception.message": error.message,
|
|
228
|
-
...(error.stack ? { "exception.stacktrace": error.stack } : {}),
|
|
229
|
-
}
|
|
230
|
-
: {}),
|
|
231
|
-
});
|
|
428
|
+
const type = errorType(error);
|
|
429
|
+
span.setAttribute("error.type", type);
|
|
430
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
431
|
+
span.addEvent("exception", { "exception.type": type });
|
|
232
432
|
}
|
|
233
433
|
catch {
|
|
234
434
|
this.transport.instrumentationFailure();
|
|
235
435
|
}
|
|
236
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
|
+
*/
|
|
237
444
|
recordMessages(messages, explicitContext) {
|
|
238
445
|
if (!this.enabled || this.closed || !this.captureContent)
|
|
239
446
|
return;
|
|
240
447
|
try {
|
|
241
|
-
const
|
|
448
|
+
const store = this.storage.getStore();
|
|
449
|
+
const active = explicitContext ?? store?.context ?? context.active();
|
|
242
450
|
if (!trace.getSpanContext(active))
|
|
243
451
|
throw new Error("Message records require an active span or explicit span context");
|
|
244
452
|
const body = {};
|
|
@@ -246,10 +454,29 @@ export class HueClient {
|
|
|
246
454
|
body["gen_ai.input.messages"] = messages.input;
|
|
247
455
|
if (messages.output !== undefined)
|
|
248
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);
|
|
249
475
|
this.logger.emit({
|
|
250
476
|
context: active,
|
|
251
477
|
severityNumber: SeverityNumber.INFO,
|
|
252
478
|
eventName: "gen_ai.client.inference.operation.details",
|
|
479
|
+
attributes,
|
|
253
480
|
body: JSON.parse(encodeContent(body)),
|
|
254
481
|
});
|
|
255
482
|
}
|
|
@@ -267,6 +494,13 @@ export class HueClient {
|
|
|
267
494
|
this.transport.instrumentationFailure();
|
|
268
495
|
}
|
|
269
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
|
+
*/
|
|
270
504
|
async checkConnection() {
|
|
271
505
|
if (!this.enabled)
|
|
272
506
|
throw new HueConnectionError("Hue telemetry is disabled");
|
|
@@ -279,8 +513,8 @@ export class HueClient {
|
|
|
279
513
|
signal: AbortSignal.timeout(options.timeoutMillis),
|
|
280
514
|
});
|
|
281
515
|
}
|
|
282
|
-
catch {
|
|
283
|
-
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 });
|
|
284
518
|
}
|
|
285
519
|
if (!response.ok) {
|
|
286
520
|
await response.body?.cancel();
|
|
@@ -322,8 +556,10 @@ export class HueClient {
|
|
|
322
556
|
slug: fields.slug,
|
|
323
557
|
};
|
|
324
558
|
}
|
|
325
|
-
catch {
|
|
326
|
-
throw new HueConnectionError("Hue returned an invalid project response"
|
|
559
|
+
catch (error) {
|
|
560
|
+
throw new HueConnectionError("Hue returned an invalid project response", undefined, {
|
|
561
|
+
cause: error,
|
|
562
|
+
});
|
|
327
563
|
}
|
|
328
564
|
}
|
|
329
565
|
/** Production lifecycle path: never rejects; timeouts do not cancel borrowed provider work. */
|
|
@@ -382,6 +618,13 @@ export class HueClient {
|
|
|
382
618
|
clearTimeout(timer);
|
|
383
619
|
}
|
|
384
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
|
+
*/
|
|
385
628
|
flush() {
|
|
386
629
|
// Each caller needs a drain after its own preceding span/log emissions.
|
|
387
630
|
// Joining an earlier drain can acknowledge records that were not in its batch.
|
|
@@ -413,6 +656,12 @@ export class HueClient {
|
|
|
413
656
|
this.transport.issue(index === 0 ? "traces" : "logs", "failed", 0, "OpenTelemetry provider flush failed");
|
|
414
657
|
return this.transport.flush();
|
|
415
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
|
+
*/
|
|
416
665
|
shutdown() {
|
|
417
666
|
this.shutdownPromise ??= (async () => {
|
|
418
667
|
this.closed = true;
|
|
@@ -432,17 +681,41 @@ export class HueClient {
|
|
|
432
681
|
return this.shutdownPromise;
|
|
433
682
|
}
|
|
434
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
|
+
*/
|
|
435
691
|
export function createHue(options) {
|
|
436
692
|
return new HueClient(options);
|
|
437
693
|
}
|
|
438
|
-
/**
|
|
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
|
+
*/
|
|
439
698
|
export function createHueSafe(options) {
|
|
440
699
|
try {
|
|
441
700
|
return new HueClient(options);
|
|
442
701
|
}
|
|
443
|
-
catch {
|
|
444
|
-
|
|
445
|
-
disabled
|
|
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)}`);
|
|
446
707
|
return disabled;
|
|
447
708
|
}
|
|
448
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
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import type { HueOptions } from "./types.js";
|
|
1
|
+
import type { HueOptions, SharedHueOptions } from "./types.js";
|
|
2
2
|
export declare const MAX_BODY_BYTES: number;
|
|
3
3
|
export declare const MAX_CONTENT_BYTES: number;
|
|
4
|
-
|
|
4
|
+
/** Loopback hostnames that may use plain HTTP without opting in. */
|
|
5
|
+
export declare function isLoopbackHost(hostname: string): boolean;
|
|
6
|
+
/** True when a validated origin exports over plain HTTP to a host other than loopback. */
|
|
7
|
+
export declare function isInsecureOrigin(baseUrl: string): boolean;
|
|
8
|
+
export declare function validateOptions(options: HueOptions): HueOptions & Required<Pick<SharedHueOptions, "captureContent" | "baseUrl" | "timeoutMillis" | "maxQueueBytes">> & {
|
|
9
|
+
/** Project key after validation; empty for a disabled client. */
|
|
10
|
+
apiKey: string;
|
|
11
|
+
/** Service name after validation; `hue-disabled` for a disabled client. */
|
|
12
|
+
serviceName: string;
|
|
13
|
+
};
|
package/dist/config.js
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
export const MAX_BODY_BYTES = 1024 * 1024;
|
|
2
2
|
export const MAX_CONTENT_BYTES = 256 * 1024;
|
|
3
|
+
/** Loopback hostnames that may use plain HTTP without opting in. */
|
|
4
|
+
export function isLoopbackHost(hostname) {
|
|
5
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
6
|
+
}
|
|
7
|
+
/** True when a validated origin exports over plain HTTP to a host other than loopback. */
|
|
8
|
+
export function isInsecureOrigin(baseUrl) {
|
|
9
|
+
const url = new URL(baseUrl);
|
|
10
|
+
return url.protocol === "http:" && !isLoopbackHost(url.hostname);
|
|
11
|
+
}
|
|
12
|
+
// The return type stays anonymous: HueTransport.options exposes it through ReturnType, and the
|
|
13
|
+
// API reference must not reference a name that is not part of the public entry points.
|
|
3
14
|
export function validateOptions(options) {
|
|
4
|
-
if (typeof options.captureContent !== "boolean")
|
|
5
|
-
throw new TypeError("Choose captureContent explicitly: true or false");
|
|
6
15
|
if (options.enabled !== undefined && typeof options.enabled !== "boolean")
|
|
7
16
|
throw new TypeError("enabled must be a boolean");
|
|
8
|
-
if (options.enabled === false)
|
|
17
|
+
if (options.enabled === false) {
|
|
18
|
+
// The kill switch exports nothing, so the content decision defaults to metadata-only. The
|
|
19
|
+
// diagnostics hook stays attached so a disabled client can still report why it is off.
|
|
20
|
+
if (options.captureContent !== undefined && typeof options.captureContent !== "boolean")
|
|
21
|
+
throw new TypeError("captureContent must be a boolean");
|
|
9
22
|
return {
|
|
10
|
-
|
|
23
|
+
...(typeof options.onExportIssue === "function"
|
|
24
|
+
? { onExportIssue: options.onExportIssue }
|
|
25
|
+
: {}),
|
|
26
|
+
captureContent: options.captureContent ?? false,
|
|
11
27
|
enabled: false,
|
|
12
28
|
apiKey: "",
|
|
13
29
|
serviceName: "hue-disabled",
|
|
@@ -15,6 +31,9 @@ export function validateOptions(options) {
|
|
|
15
31
|
timeoutMillis: 10000,
|
|
16
32
|
maxQueueBytes: 8 * 1024 * 1024,
|
|
17
33
|
};
|
|
34
|
+
}
|
|
35
|
+
if (typeof options.captureContent !== "boolean")
|
|
36
|
+
throw new TypeError("Choose captureContent explicitly: true or false");
|
|
18
37
|
if (typeof options.apiKey !== "string" ||
|
|
19
38
|
!options.apiKey ||
|
|
20
39
|
options.apiKey.length > 4096 ||
|
|
@@ -25,6 +44,13 @@ export function validateOptions(options) {
|
|
|
25
44
|
!options.serviceName.trim() ||
|
|
26
45
|
options.serviceName.length > 256)
|
|
27
46
|
throw new TypeError("A serviceName of 1–256 characters is required");
|
|
47
|
+
if (options.allowInsecureHttp !== undefined && typeof options.allowInsecureHttp !== "boolean")
|
|
48
|
+
throw new TypeError("allowInsecureHttp must be a boolean");
|
|
49
|
+
if (options.resourceAttributes !== undefined &&
|
|
50
|
+
(options.resourceAttributes === null ||
|
|
51
|
+
typeof options.resourceAttributes !== "object" ||
|
|
52
|
+
Array.isArray(options.resourceAttributes)))
|
|
53
|
+
throw new TypeError("resourceAttributes must be an object of attribute values");
|
|
28
54
|
let url;
|
|
29
55
|
try {
|
|
30
56
|
url = new URL(options.baseUrl ?? "https://app.hue.run");
|
|
@@ -32,9 +58,12 @@ export function validateOptions(options) {
|
|
|
32
58
|
catch {
|
|
33
59
|
throw new TypeError("Invalid Hue baseUrl");
|
|
34
60
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
61
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
62
|
+
throw new TypeError("Hue baseUrl must use https, or http for a loopback development server");
|
|
63
|
+
if (url.protocol === "http:" &&
|
|
64
|
+
!isLoopbackHost(url.hostname) &&
|
|
65
|
+
options.allowInsecureHttp !== true)
|
|
66
|
+
throw new TypeError("Hue requires HTTPS except for a loopback development server; set allowInsecureHttp: true to export over plain HTTP to another host");
|
|
38
67
|
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash)
|
|
39
68
|
throw new TypeError("Hue baseUrl must be an origin without credentials, a path, query parameters or fragments");
|
|
40
69
|
const timeoutMillis = options.timeoutMillis ?? 10000;
|