@hue-run/sdk 0.13.2 → 0.15.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/CLI.md +26 -19
- package/EVALUATIONS.md +1 -1
- package/README.md +117 -24
- package/dist/blobs.d.ts +164 -0
- package/dist/blobs.js +703 -0
- package/dist/cli/eval.js +9 -5
- package/dist/client.d.ts +15 -5
- package/dist/client.js +63 -13
- package/dist/config.d.ts +20 -0
- package/dist/config.js +31 -4
- package/dist/environment/client.d.ts +1 -1
- package/dist/environment/client.js +3 -3
- package/dist/evals/client.d.ts +1 -1
- package/dist/evals/client.js +4 -4
- package/dist/evals/verdicts.d.ts +10 -7
- package/dist/evals/verdicts.js +52 -16
- package/dist/inline-files.d.ts +44 -1
- package/dist/inline-files.js +65 -6
- package/dist/limits.d.ts +32 -0
- package/dist/limits.js +54 -0
- package/dist/privacy.d.ts +27 -5
- package/dist/privacy.js +140 -39
- package/dist/provider-tools.js +1 -1
- package/dist/safety.d.ts +2 -0
- package/dist/safety.js +10 -4
- package/dist/snapshot.d.ts +30 -8
- package/dist/snapshot.js +280 -49
- package/dist/tool-definitions.d.ts +4 -0
- package/dist/tool-definitions.js +11 -0
- package/dist/transport.d.ts +26 -0
- package/dist/transport.js +579 -177
- package/dist/types.d.ts +14 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/cli/eval.js
CHANGED
|
@@ -78,7 +78,8 @@ Output and limits:
|
|
|
78
78
|
--name <run name> Run name (default: <agent key> @ <revision>, hashes shortened)
|
|
79
79
|
--baseline <experiment id|url> Compare verdicts with a previous experiment
|
|
80
80
|
--json Print one JSON document on stdout; progress goes to stderr
|
|
81
|
-
--content Capture telemetry content (
|
|
81
|
+
--content Capture telemetry content (the default)
|
|
82
|
+
--no-content Omit telemetry inputs, outputs and messages
|
|
82
83
|
--no-output One-shot: do not store case outputs, error messages and
|
|
83
84
|
explanations (stored by default; --worker always stores); with
|
|
84
85
|
--content the case span still carries the output
|
|
@@ -141,7 +142,8 @@ function parse(argv) {
|
|
|
141
142
|
name: { type: "string" },
|
|
142
143
|
baseline: { type: "string" },
|
|
143
144
|
json: { type: "boolean", default: false },
|
|
144
|
-
content: { type: "boolean"
|
|
145
|
+
content: { type: "boolean" },
|
|
146
|
+
"no-content": { type: "boolean", default: false },
|
|
145
147
|
"no-output": { type: "boolean", default: false },
|
|
146
148
|
"trace-not-accepted": { type: "string" },
|
|
147
149
|
"save-version": { type: "boolean", default: false },
|
|
@@ -817,9 +819,9 @@ export function explain(error) {
|
|
|
817
819
|
if (error instanceof CheckpointIdentityError && error.startedWith) {
|
|
818
820
|
const flags = [
|
|
819
821
|
error.startedWith.persistResultContent === false ? "--no-output" : "",
|
|
820
|
-
error.startedWith.captureContent ===
|
|
822
|
+
error.startedWith.captureContent === false ? "--no-content" : "",
|
|
821
823
|
].filter(Boolean);
|
|
822
|
-
return `This unfinished run was started ${flags.length ? `with ${flags.join(" and ")}` : "without --no-output or --content"}; rerun with the same flags to resume it, or remove its checkpoint directory to start over`;
|
|
824
|
+
return `This unfinished run was started ${flags.length ? `with ${flags.join(" and ")}` : "without --no-output or --no-content"}; rerun with the same flags to resume it, or remove its checkpoint directory to start over`;
|
|
823
825
|
}
|
|
824
826
|
// The error's own message points at an in-memory report the user cannot reach.
|
|
825
827
|
if (error instanceof HueExportError)
|
|
@@ -1446,6 +1448,8 @@ export async function runEvalCommand(argv) {
|
|
|
1446
1448
|
let json = false;
|
|
1447
1449
|
try {
|
|
1448
1450
|
const { values, positionals } = parse(argv);
|
|
1451
|
+
if (values.content && values["no-content"])
|
|
1452
|
+
throw new UsageError("Choose either --content or --no-content");
|
|
1449
1453
|
// A usage error before anything is prepared or created: no run is left to recover from it.
|
|
1450
1454
|
traceNotAcceptedPolicy(values["trace-not-accepted"]);
|
|
1451
1455
|
json = values.json;
|
|
@@ -1522,7 +1526,7 @@ export async function runEvalCommand(argv) {
|
|
|
1522
1526
|
apiKey,
|
|
1523
1527
|
baseUrl,
|
|
1524
1528
|
serviceName: key,
|
|
1525
|
-
captureContent: values.content,
|
|
1529
|
+
captureContent: values.content ?? !values["no-content"],
|
|
1526
1530
|
timeoutMillis: CASE_TRACE_EXPORT_MILLIS,
|
|
1527
1531
|
});
|
|
1528
1532
|
return values.worker
|
package/dist/client.d.ts
CHANGED
|
@@ -156,11 +156,16 @@ export declare class HueClient {
|
|
|
156
156
|
recordFile(file: FileRecord, explicitContext?: Context): void;
|
|
157
157
|
/** The keys of each span's content values that were cut to Hue's value cap, for `hue.truncated`. */
|
|
158
158
|
private truncatedKeys;
|
|
159
|
+
/** A content value's bounds: Hue's value cap as the receiver last advertised it. */
|
|
160
|
+
private contentLimits;
|
|
159
161
|
/**
|
|
160
|
-
* A content value as a JSON attribute within Hue's
|
|
161
|
-
* a UTF-8 prefix and its key listed under
|
|
162
|
-
* values it cuts itself: the span carries the
|
|
163
|
-
*
|
|
162
|
+
* A content value as a JSON attribute within Hue's value cap (1 MiB unless the receiver
|
|
163
|
+
* advertises another). A larger value is cut to a UTF-8 prefix and its key listed under
|
|
164
|
+
* `hue.truncated`, where Hue's receiver lists the values it cuts itself: the span carries the
|
|
165
|
+
* call and the recorded part of its value instead of losing the value whole. While values over
|
|
166
|
+
* the cap may be uploaded, the value is encoded whole, up to Hue's 1 GB upload limit and as many
|
|
167
|
+
* values as a queued record may hold, and export uploads it or cuts it. Only a value that cannot
|
|
168
|
+
* be encoded is omitted and counted.
|
|
164
169
|
*/
|
|
165
170
|
private setContent;
|
|
166
171
|
/**
|
|
@@ -173,7 +178,12 @@ export declare class HueClient {
|
|
|
173
178
|
checkConnection(): Promise<ProjectConnection>;
|
|
174
179
|
/** Production lifecycle path: never rejects; timeouts do not cancel borrowed provider work. */
|
|
175
180
|
flushSafe(options?: SafeLifecycleOptions): Promise<SafeLifecycleResult>;
|
|
176
|
-
/**
|
|
181
|
+
/**
|
|
182
|
+
* Safe for finally blocks; preserves the application's result or original exception. When its
|
|
183
|
+
* budget runs out while the client's own transport holds records for a receiver that limited
|
|
184
|
+
* the rate, the hold ends: those records are reported lost to the rate limit, and the hold no
|
|
185
|
+
* longer keeps the process running.
|
|
186
|
+
*/
|
|
177
187
|
shutdownSafe(options?: SafeLifecycleOptions): Promise<SafeLifecycleResult>;
|
|
178
188
|
private safeLifecycle;
|
|
179
189
|
/**
|
package/dist/client.js
CHANGED
|
@@ -10,12 +10,13 @@ import { defaultResource, resourceFromAttributes } from "@opentelemetry/resource
|
|
|
10
10
|
import { HUE_SCOPE } from "./config.js";
|
|
11
11
|
import { hostedServerAddresses, hostedToolActivity, hostedToolProvider, providerErrorDescription, } from "./provider-tools.js";
|
|
12
12
|
import { toolCatalogSummary } from "./tool-definitions.js";
|
|
13
|
-
import { encodeBoundedContent, encodeContent, noopSpan, safeSpan, } from "./safety.js";
|
|
13
|
+
import { contentLimitsOf, encodeBoundedContent, encodeContent, noopSpan, safeSpan, } from "./safety.js";
|
|
14
14
|
import { TRUNCATED_KEY, withTruncatedKeys } from "./privacy.js";
|
|
15
|
-
import {
|
|
15
|
+
import { MAX_BLOB_BYTES } from "./blobs.js";
|
|
16
|
+
import { createHueTransport, exportBudgetMillis, HueExportError, } from "./transport.js";
|
|
16
17
|
import { verifyTrace } from "./receipt.js";
|
|
17
18
|
import { sdkVersion } from "./version.js";
|
|
18
|
-
import { MAX_BODY_BYTES, MAX_FILE_DATA_BYTES } from "./config.js";
|
|
19
|
+
import { MAX_BODY_BYTES, MAX_FILE_DATA_BYTES, MAX_RECORD_NODES } from "./config.js";
|
|
19
20
|
/**
|
|
20
21
|
* Thrown by {@link HueClient.checkConnection} and {@link HueClient.verifyTrace} when Hue cannot be
|
|
21
22
|
* reached, rejects the project key or answers unexpectedly. The message is fixed and safe to log;
|
|
@@ -151,6 +152,20 @@ class ContextualTracer {
|
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
const propagator = new W3CTraceContextPropagator();
|
|
155
|
+
/**
|
|
156
|
+
* Attributes, events and links one span or log record of a client's own providers keeps, and
|
|
157
|
+
* attributes per event and link. OpenTelemetry's default of 128 silently dropped the rest of a
|
|
158
|
+
* long conversation's flattened messages; Hue's receiver counts none of them. Providers an
|
|
159
|
+
* application attaches Hue to keep their own limits.
|
|
160
|
+
*/
|
|
161
|
+
const OWNED_RECORD_LIMIT = 2000;
|
|
162
|
+
const OWNED_SPAN_LIMITS = {
|
|
163
|
+
attributeCountLimit: OWNED_RECORD_LIMIT,
|
|
164
|
+
eventCountLimit: OWNED_RECORD_LIMIT,
|
|
165
|
+
linkCountLimit: OWNED_RECORD_LIMIT,
|
|
166
|
+
attributePerEventCountLimit: OWNED_RECORD_LIMIT,
|
|
167
|
+
attributePerLinkCountLimit: OWNED_RECORD_LIMIT,
|
|
168
|
+
};
|
|
154
169
|
/** A provider tool listing's bounds before its catalog summary: one export request, 64 levels and
|
|
155
170
|
* 65,536 values, as the Python SDK's content snapshot. */
|
|
156
171
|
const catalogLimits = { bytes: MAX_BODY_BYTES, nodes: 65_536, depth: 64 };
|
|
@@ -214,10 +229,12 @@ export class HueClient {
|
|
|
214
229
|
const tracer = new TracerProvider({
|
|
215
230
|
resource,
|
|
216
231
|
spanProcessors: this.transport.options.enabled === false ? [] : [this.transport.spanProcessor],
|
|
232
|
+
spanLimits: OWNED_SPAN_LIMITS,
|
|
217
233
|
});
|
|
218
234
|
const logger = new LoggerProvider({
|
|
219
235
|
resource,
|
|
220
236
|
processors: this.transport.options.enabled === false ? [] : [this.transport.logRecordProcessor],
|
|
237
|
+
logRecordLimits: { attributeCountLimit: OWNED_RECORD_LIMIT },
|
|
221
238
|
});
|
|
222
239
|
this.ownedProviders = { tracer, logger };
|
|
223
240
|
this.tracerProvider = tracer;
|
|
@@ -522,7 +539,7 @@ export class HueClient {
|
|
|
522
539
|
severityNumber: SeverityNumber.INFO,
|
|
523
540
|
eventName: "gen_ai.client.inference.operation.details",
|
|
524
541
|
attributes,
|
|
525
|
-
body: JSON.parse(encodeContent(body)),
|
|
542
|
+
body: JSON.parse(encodeContent(body, this.contentLimits())),
|
|
526
543
|
});
|
|
527
544
|
}
|
|
528
545
|
catch {
|
|
@@ -715,17 +732,36 @@ export class HueClient {
|
|
|
715
732
|
}
|
|
716
733
|
/** The keys of each span's content values that were cut to Hue's value cap, for `hue.truncated`. */
|
|
717
734
|
truncatedKeys = new WeakMap();
|
|
735
|
+
/** A content value's bounds: Hue's value cap as the receiver last advertised it. */
|
|
736
|
+
contentLimits() {
|
|
737
|
+
return contentLimitsOf(this.transport.receiverLimits().valueBytes);
|
|
738
|
+
}
|
|
718
739
|
/**
|
|
719
|
-
* A content value as a JSON attribute within Hue's
|
|
720
|
-
* a UTF-8 prefix and its key listed under
|
|
721
|
-
* values it cuts itself: the span carries the
|
|
722
|
-
*
|
|
740
|
+
* A content value as a JSON attribute within Hue's value cap (1 MiB unless the receiver
|
|
741
|
+
* advertises another). A larger value is cut to a UTF-8 prefix and its key listed under
|
|
742
|
+
* `hue.truncated`, where Hue's receiver lists the values it cuts itself: the span carries the
|
|
743
|
+
* call and the recorded part of its value instead of losing the value whole. While values over
|
|
744
|
+
* the cap may be uploaded, the value is encoded whole, up to Hue's 1 GB upload limit and as many
|
|
745
|
+
* values as a queued record may hold, and export uploads it or cuts it. Only a value that cannot
|
|
746
|
+
* be encoded is omitted and counted.
|
|
723
747
|
*/
|
|
724
748
|
setContent(span, key, value) {
|
|
725
749
|
if (!this.enabled || this.closed || !this.captureContent)
|
|
726
750
|
return;
|
|
727
751
|
try {
|
|
728
|
-
|
|
752
|
+
let encoded;
|
|
753
|
+
if (this.transport.holdsValues())
|
|
754
|
+
try {
|
|
755
|
+
encoded = encodeBoundedContent(value, {
|
|
756
|
+
...this.contentLimits(),
|
|
757
|
+
bytes: MAX_BLOB_BYTES,
|
|
758
|
+
nodes: MAX_RECORD_NODES,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
// Past the runtime's longest string, or its memory: cut to the cap instead.
|
|
763
|
+
}
|
|
764
|
+
const { text, truncated } = encoded ?? encodeBoundedContent(value, this.contentLimits());
|
|
729
765
|
span.setAttribute(key, text);
|
|
730
766
|
// The span's list as it stands: the application's own entries (read from the SDK span,
|
|
731
767
|
// which carries its attributes; the API alone shows none) and this helper's earlier ones.
|
|
@@ -826,12 +862,22 @@ export class HueClient {
|
|
|
826
862
|
void work.then(clear, clear);
|
|
827
863
|
return result;
|
|
828
864
|
}
|
|
829
|
-
/**
|
|
865
|
+
/**
|
|
866
|
+
* Safe for finally blocks; preserves the application's result or original exception. When its
|
|
867
|
+
* budget runs out while the client's own transport holds records for a receiver that limited
|
|
868
|
+
* the rate, the hold ends: those records are reported lost to the rate limit, and the hold no
|
|
869
|
+
* longer keeps the process running.
|
|
870
|
+
*/
|
|
830
871
|
shutdownSafe(options = {}) {
|
|
831
872
|
if (this.safeShutdownPromise)
|
|
832
873
|
return this.safeShutdownPromise;
|
|
833
874
|
const work = this.shutdown();
|
|
834
|
-
const result = this.safeLifecycle(() => work, options)
|
|
875
|
+
const result = this.safeLifecycle(() => work, options).then((outcome) => {
|
|
876
|
+
// A transport the application attached keeps running past this client's shutdown.
|
|
877
|
+
if (outcome.timedOut && this.ownedProviders)
|
|
878
|
+
this.transport.releaseRateLimitHolds();
|
|
879
|
+
return outcome;
|
|
880
|
+
});
|
|
835
881
|
this.safeShutdownPromise = result;
|
|
836
882
|
const clear = () => {
|
|
837
883
|
this.safeShutdownPromise = undefined;
|
|
@@ -896,9 +942,13 @@ export class HueClient {
|
|
|
896
942
|
return next;
|
|
897
943
|
}
|
|
898
944
|
async flushOnce() {
|
|
945
|
+
// Providers this client owns wait as long as an export may take, a rate-limit hold included,
|
|
946
|
+
// instead of OpenTelemetry's 30 s; borrowed providers keep their own budget.
|
|
947
|
+
const owned = this.ownedProviders;
|
|
948
|
+
const budget = { timeoutMillis: exportBudgetMillis(this.transport.options.timeoutMillis) };
|
|
899
949
|
const results = await Promise.allSettled([
|
|
900
|
-
Promise.resolve().then(() => this.tracerProvider.forceFlush()),
|
|
901
|
-
Promise.resolve().then(() => this.loggerProvider.forceFlush()),
|
|
950
|
+
Promise.resolve().then(() => owned ? owned.tracer.forceFlush(budget) : this.tracerProvider.forceFlush()),
|
|
951
|
+
Promise.resolve().then(() => owned ? owned.logger.forceFlush(budget) : this.loggerProvider.forceFlush()),
|
|
902
952
|
]);
|
|
903
953
|
for (const [index, result] of results.entries())
|
|
904
954
|
if (result.status === "rejected")
|
package/dist/config.d.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import type { HueOptions, SharedHueOptions } from "./types.js";
|
|
2
|
+
/** One request on the wire (after gzip), as Hue's receiver accepts before it advertises its own
|
|
3
|
+
* limit with `Hue-Max-Request-Bytes`. */
|
|
2
4
|
export declare const MAX_BODY_BYTES: number;
|
|
5
|
+
/** One request after decompression, before the receiver advertises `Hue-Max-Decoded-Bytes`. */
|
|
6
|
+
export declare const MAX_DECODED_BYTES: number;
|
|
7
|
+
/** One attribute value or log body kept whole, before the receiver advertises
|
|
8
|
+
* `Hue-Max-Value-Bytes`; a longer value is cut to it and listed under `hue.truncated`. */
|
|
3
9
|
export declare const MAX_CONTENT_BYTES: number;
|
|
10
|
+
/**
|
|
11
|
+
* Text past the value cap that a record keeps when it is queued, in UTF-16 code units. A string
|
|
12
|
+
* longer than the cap and this context is cut when it is queued, so a huge value cannot exhaust
|
|
13
|
+
* the queue's byte budget and drop its record; the context lets `redact` still see a secret that
|
|
14
|
+
* crosses the cap, and what the redactor saw without its continuation is never exported.
|
|
15
|
+
*/
|
|
16
|
+
export declare const REDACTION_CONTEXT_UNITS: number;
|
|
17
|
+
/**
|
|
18
|
+
* The values one record may hold when it is queued, redacted and measured for export. A client's
|
|
19
|
+
* own providers keep 2,000 events and links of 2,000 attributes each, so a record's byte budget
|
|
20
|
+
* (every value is charged at least 16 bytes against `maxQueueBytes`) bounds it before this does;
|
|
21
|
+
* this bounds the work on the application's thread when the queue budget is raised.
|
|
22
|
+
*/
|
|
23
|
+
export declare const MAX_RECORD_NODES = 1048576;
|
|
4
24
|
/** Instrumentation scope of the client's own tracer and logger. */
|
|
5
25
|
export declare const HUE_SCOPE = "@hue-run/sdk";
|
|
6
26
|
/** Maximum bytes accepted when recordFile hashes caller-provided data locally. */
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
|
+
/** One request on the wire (after gzip), as Hue's receiver accepts before it advertises its own
|
|
2
|
+
* limit with `Hue-Max-Request-Bytes`. */
|
|
1
3
|
export const MAX_BODY_BYTES = 1024 * 1024;
|
|
2
|
-
|
|
4
|
+
/** One request after decompression, before the receiver advertises `Hue-Max-Decoded-Bytes`. */
|
|
5
|
+
export const MAX_DECODED_BYTES = 4 * 1024 * 1024;
|
|
6
|
+
/** One attribute value or log body kept whole, before the receiver advertises
|
|
7
|
+
* `Hue-Max-Value-Bytes`; a longer value is cut to it and listed under `hue.truncated`. */
|
|
8
|
+
export const MAX_CONTENT_BYTES = 1024 * 1024;
|
|
9
|
+
/**
|
|
10
|
+
* Text past the value cap that a record keeps when it is queued, in UTF-16 code units. A string
|
|
11
|
+
* longer than the cap and this context is cut when it is queued, so a huge value cannot exhaust
|
|
12
|
+
* the queue's byte budget and drop its record; the context lets `redact` still see a secret that
|
|
13
|
+
* crosses the cap, and what the redactor saw without its continuation is never exported.
|
|
14
|
+
*/
|
|
15
|
+
export const REDACTION_CONTEXT_UNITS = 64 * 1024;
|
|
16
|
+
/**
|
|
17
|
+
* The values one record may hold when it is queued, redacted and measured for export. A client's
|
|
18
|
+
* own providers keep 2,000 events and links of 2,000 attributes each, so a record's byte budget
|
|
19
|
+
* (every value is charged at least 16 bytes against `maxQueueBytes`) bounds it before this does;
|
|
20
|
+
* this bounds the work on the application's thread when the queue budget is raised.
|
|
21
|
+
*/
|
|
22
|
+
export const MAX_RECORD_NODES = 1_048_576;
|
|
3
23
|
/** Instrumentation scope of the client's own tracer and logger. */
|
|
4
24
|
export const HUE_SCOPE = "@hue-run/sdk";
|
|
5
25
|
/** Maximum bytes accepted when recordFile hashes caller-provided data locally. */
|
|
@@ -37,8 +57,8 @@ export function validateOptions(options) {
|
|
|
37
57
|
liveSpans: false,
|
|
38
58
|
};
|
|
39
59
|
}
|
|
40
|
-
if (typeof options.captureContent !== "boolean")
|
|
41
|
-
throw new TypeError("
|
|
60
|
+
if (options.captureContent !== undefined && typeof options.captureContent !== "boolean")
|
|
61
|
+
throw new TypeError("captureContent must be a boolean");
|
|
42
62
|
if (typeof options.apiKey !== "string" ||
|
|
43
63
|
!options.apiKey ||
|
|
44
64
|
options.apiKey.length > 4096 ||
|
|
@@ -83,5 +103,12 @@ export function validateOptions(options) {
|
|
|
83
103
|
throw new TypeError("liveSpans must be a boolean");
|
|
84
104
|
// Setup credentials send installer telemetry only, never in-progress placeholders.
|
|
85
105
|
const liveSpans = options.liveSpans !== false && !options.apiKey.startsWith("hue_setup_");
|
|
86
|
-
return {
|
|
106
|
+
return {
|
|
107
|
+
...options,
|
|
108
|
+
captureContent: options.captureContent ?? true,
|
|
109
|
+
baseUrl: url.origin,
|
|
110
|
+
timeoutMillis,
|
|
111
|
+
maxQueueBytes,
|
|
112
|
+
liveSpans,
|
|
113
|
+
};
|
|
87
114
|
}
|
|
@@ -30,9 +30,9 @@ export declare class HueEnvironmentError extends Error {
|
|
|
30
30
|
export declare function isTransientEnvironmentError(error: unknown): boolean;
|
|
31
31
|
/** Typed client for authored environments, isolated runs and immutable journals. */
|
|
32
32
|
export declare class EnvironmentClient {
|
|
33
|
+
#private;
|
|
33
34
|
/** Validated Hue origin. */
|
|
34
35
|
readonly baseUrl: string;
|
|
35
|
-
private readonly apiKey;
|
|
36
36
|
private readonly timeoutMillis;
|
|
37
37
|
private readonly maxAttempts;
|
|
38
38
|
constructor(options: EnvironmentClientOptions);
|
|
@@ -62,7 +62,7 @@ const ENVIRONMENT_PUBLICATION_BOUNDS = aggregateBounds(240_000 + 32);
|
|
|
62
62
|
export class EnvironmentClient {
|
|
63
63
|
/** Validated Hue origin. */
|
|
64
64
|
baseUrl;
|
|
65
|
-
apiKey;
|
|
65
|
+
#apiKey;
|
|
66
66
|
timeoutMillis;
|
|
67
67
|
maxAttempts;
|
|
68
68
|
constructor(options) {
|
|
@@ -72,7 +72,7 @@ export class EnvironmentClient {
|
|
|
72
72
|
captureContent: false,
|
|
73
73
|
});
|
|
74
74
|
this.baseUrl = validated.baseUrl;
|
|
75
|
-
this
|
|
75
|
+
this.#apiKey = validated.apiKey;
|
|
76
76
|
this.timeoutMillis = validated.timeoutMillis;
|
|
77
77
|
const attempts = options.maxAttempts ?? 4;
|
|
78
78
|
if (!Number.isInteger(attempts) || attempts < 1 || attempts > 10)
|
|
@@ -86,7 +86,7 @@ export class EnvironmentClient {
|
|
|
86
86
|
response = await fetch(`${this.baseUrl}/api/v1${path}`, {
|
|
87
87
|
method,
|
|
88
88
|
headers: {
|
|
89
|
-
Authorization: `Bearer ${this
|
|
89
|
+
Authorization: `Bearer ${this.#apiKey}`,
|
|
90
90
|
...(payload ? { "Content-Type": "application/json" } : {}),
|
|
91
91
|
},
|
|
92
92
|
body: payload,
|
package/dist/evals/client.d.ts
CHANGED
|
@@ -56,9 +56,9 @@ export declare class ArtifactSizeError extends Error {
|
|
|
56
56
|
* bounded to 4 MiB.
|
|
57
57
|
*/
|
|
58
58
|
export declare class EvaluationClient {
|
|
59
|
+
#private;
|
|
59
60
|
/** Validated Hue origin. */
|
|
60
61
|
readonly baseUrl: string;
|
|
61
|
-
private readonly apiKey;
|
|
62
62
|
private readonly timeoutMillis;
|
|
63
63
|
private readonly maxAttempts;
|
|
64
64
|
constructor(options: EvaluationClientOptions);
|
package/dist/evals/client.js
CHANGED
|
@@ -183,7 +183,7 @@ function refusalRetryAfter(response) {
|
|
|
183
183
|
export class EvaluationClient {
|
|
184
184
|
/** Validated Hue origin. */
|
|
185
185
|
baseUrl;
|
|
186
|
-
apiKey;
|
|
186
|
+
#apiKey;
|
|
187
187
|
timeoutMillis;
|
|
188
188
|
maxAttempts;
|
|
189
189
|
constructor(options) {
|
|
@@ -193,7 +193,7 @@ export class EvaluationClient {
|
|
|
193
193
|
captureContent: false,
|
|
194
194
|
});
|
|
195
195
|
this.baseUrl = validated.baseUrl;
|
|
196
|
-
this
|
|
196
|
+
this.#apiKey = validated.apiKey;
|
|
197
197
|
this.timeoutMillis = validated.timeoutMillis;
|
|
198
198
|
const attempts = options.maxAttempts ?? 4;
|
|
199
199
|
if (!Number.isInteger(attempts) || attempts < 1 || attempts > 10)
|
|
@@ -276,7 +276,7 @@ export class EvaluationClient {
|
|
|
276
276
|
const response = await this.send(`${this.baseUrl}/api/v1${path}`, () => ({
|
|
277
277
|
method,
|
|
278
278
|
headers: {
|
|
279
|
-
Authorization: `Bearer ${this
|
|
279
|
+
Authorization: `Bearer ${this.#apiKey}`,
|
|
280
280
|
...(payload ? { "Content-Type": "application/json" } : {}),
|
|
281
281
|
},
|
|
282
282
|
body: payload,
|
|
@@ -321,7 +321,7 @@ export class EvaluationClient {
|
|
|
321
321
|
throw new RangeError("maxBytes must be a non-negative integer");
|
|
322
322
|
const response = await this.send(`${this.baseUrl}/api/v1/artifacts/${uuid(id)}/download`, () => ({
|
|
323
323
|
method: "GET",
|
|
324
|
-
headers: { Authorization: `Bearer ${this
|
|
324
|
+
headers: { Authorization: `Bearer ${this.#apiKey}` },
|
|
325
325
|
redirect: "error",
|
|
326
326
|
signal: AbortSignal.timeout(Math.max(this.timeoutMillis, 120_000)),
|
|
327
327
|
}));
|
package/dist/evals/verdicts.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EvaluationClient } from "./client.js";
|
|
2
|
-
import type { ExperimentItem, Metric, Subject, TypedError } from "./types.js";
|
|
2
|
+
import type { ExperimentItem, Metric, ScorerVersion, Subject, TypedError } from "./types.js";
|
|
3
3
|
/** Subset of {@link EvaluationClient} used to wait for results. */
|
|
4
4
|
export type VerdictClient = Pick<EvaluationClient, "listEvaluationItems" | "listResults" | "getResult">;
|
|
5
5
|
/** Options for {@link waitForResults}. */
|
|
@@ -44,7 +44,7 @@ export interface VerdictResult {
|
|
|
44
44
|
notApplicable?: boolean;
|
|
45
45
|
/** What a not-applicable evaluator needs that the case lacks, such as `outcome_criteria`. */
|
|
46
46
|
requires?: string;
|
|
47
|
-
/** Whether Hue recorded the result as advisory
|
|
47
|
+
/** Whether Hue recorded the result as advisory: a result of a
|
|
48
48
|
* pinned Hue judge whose evidence says `advisory: true`, not an error, and whose metrics carry
|
|
49
49
|
* no `passed`. It is shown with its verdict but never decides a case. Absent means false. */
|
|
50
50
|
advisory?: boolean;
|
|
@@ -81,8 +81,8 @@ export interface CaseVerdict {
|
|
|
81
81
|
/** `passed` and `failed` are scored verdicts; `error`, `skipped` and `pending` carry no verdict.
|
|
82
82
|
* Only the evaluators that apply to the case decide it; one where none applies is an error. */
|
|
83
83
|
state: "passed" | "failed" | "error" | "skipped" | "pending";
|
|
84
|
-
/** True only for `state: "passed"`: every
|
|
85
|
-
* applicable pin errored or is missing. */
|
|
84
|
+
/** True only for `state: "passed"`: every deciding metric passed, required evaluators decided,
|
|
85
|
+
* and no applicable pin errored or is missing. */
|
|
86
86
|
passed: boolean;
|
|
87
87
|
/** Pinned scorer versions Hue recorded as not applicable to the case; always set by
|
|
88
88
|
* {@link summarizeVerdicts}. */
|
|
@@ -114,7 +114,7 @@ export interface VerdictSummary {
|
|
|
114
114
|
failed: number;
|
|
115
115
|
/** Cases with a scorer error. */
|
|
116
116
|
error: number;
|
|
117
|
-
/** Cases whose pins were
|
|
117
|
+
/** Cases whose deciding pins were skipped or whose required evaluator has no decision. */
|
|
118
118
|
skipped: number;
|
|
119
119
|
/** Cases still missing a result. */
|
|
120
120
|
pending: number;
|
|
@@ -127,14 +127,17 @@ export interface VerdictSummary {
|
|
|
127
127
|
export declare function metricPassed(metric: Metric): boolean;
|
|
128
128
|
/**
|
|
129
129
|
* Groups results by experiment case. Cases link to results through their execution's subject;
|
|
130
|
-
* pass `subjects` when the items do not carry `execution.subjectId`.
|
|
131
|
-
*
|
|
130
|
+
* pass `subjects` when the items do not carry `execution.subjectId`. Applicable required pins
|
|
131
|
+
* need a decision before a case can pass; genuine not-applicable and advisory pins are exempt.
|
|
132
132
|
*/
|
|
133
133
|
export declare function summarizeVerdicts(results: VerdictResults, options: {
|
|
134
134
|
/** Experiment items, in the order rows should appear. */
|
|
135
135
|
experimentItems: Pick<ExperimentItem, "id" | "externalKey" | "execution">[];
|
|
136
136
|
/** Scorer versions every case needs a result for; defaults to the versions seen in `results`. */
|
|
137
137
|
scorerVersionIds?: string[];
|
|
138
|
+
/** Immutable pinned definitions, as returned on the experiment. Outcome entries and required
|
|
139
|
+
* judge versions need a decision; result evidence cannot change their required policy. */
|
|
140
|
+
scorerVersions?: Pick<ScorerVersion, "id" | "definition">[];
|
|
138
141
|
/** Subject to case links for servers that omit `execution.subjectId`. */
|
|
139
142
|
subjects?: Pick<Subject, "id" | "caseId">[];
|
|
140
143
|
}): VerdictSummary;
|
package/dist/evals/verdicts.js
CHANGED
|
@@ -104,13 +104,25 @@ export function metricPassed(metric) {
|
|
|
104
104
|
return metric.passed;
|
|
105
105
|
return typeof metric.value === "boolean" ? metric.value : true;
|
|
106
106
|
}
|
|
107
|
+
/** Outcome entries decide the case; a separate judge's immutable version opts into that policy.
|
|
108
|
+
* A named answer reference remains inside its outcome entry, not an advisory judge promotion. */
|
|
109
|
+
function requiresDecision(definition) {
|
|
110
|
+
if (!definition || typeof definition !== "object" || Array.isArray(definition))
|
|
111
|
+
return false;
|
|
112
|
+
const pinned = definition;
|
|
113
|
+
return (pinned.kind === "world_outcome" ||
|
|
114
|
+
(pinned.kind === "world_judge" && pinned.config?.required === true));
|
|
115
|
+
}
|
|
107
116
|
/**
|
|
108
117
|
* Groups results by experiment case. Cases link to results through their execution's subject;
|
|
109
|
-
* pass `subjects` when the items do not carry `execution.subjectId`.
|
|
110
|
-
*
|
|
118
|
+
* pass `subjects` when the items do not carry `execution.subjectId`. Applicable required pins
|
|
119
|
+
* need a decision before a case can pass; genuine not-applicable and advisory pins are exempt.
|
|
111
120
|
*/
|
|
112
121
|
export function summarizeVerdicts(results, options) {
|
|
113
122
|
const pins = new Set(options.scorerVersionIds ?? results.results.map((result) => result.scorerVersionId));
|
|
123
|
+
const required = new Set((options.scorerVersions ?? [])
|
|
124
|
+
.filter((version) => requiresDecision(version.definition))
|
|
125
|
+
.map((version) => version.id));
|
|
114
126
|
const subjectByCase = new Map();
|
|
115
127
|
for (const subject of options.subjects ?? [])
|
|
116
128
|
subjectByCase.set(subject.caseId, subject.id);
|
|
@@ -123,7 +135,8 @@ export function summarizeVerdicts(results, options) {
|
|
|
123
135
|
const metrics = own.flatMap((result) => result.metrics.map((metric) => ({ ...metric, scorerVersionId: result.scorerVersionId })));
|
|
124
136
|
// An evaluator that does not apply to the case neither passes nor fails it.
|
|
125
137
|
const skippedAsInapplicable = (result) => result.state === "skipped" && result.notApplicable === true;
|
|
126
|
-
const isAdvisory = (result) => result.
|
|
138
|
+
const isAdvisory = (result) => !required.has(result.scorerVersionId) &&
|
|
139
|
+
result.advisory === true &&
|
|
127
140
|
result.state !== "error" &&
|
|
128
141
|
result.metrics.every((metric) => metric.passed === undefined);
|
|
129
142
|
const inapplicable = own.filter(skippedAsInapplicable);
|
|
@@ -134,18 +147,29 @@ export function summarizeVerdicts(results, options) {
|
|
|
134
147
|
const errors = deciding.filter((result) => result.state === "error");
|
|
135
148
|
const scored = deciding.filter((result) => result.state === "scored");
|
|
136
149
|
const failing = scored.filter((result) => !result.metrics.every(metricPassed));
|
|
150
|
+
const undecided = deciding.filter((result) => required.has(result.scorerVersionId) &&
|
|
151
|
+
result.state !== "error" &&
|
|
152
|
+
(result.state !== "scored" ||
|
|
153
|
+
!result.metrics.some((metric) => typeof metric.passed === "boolean" || typeof metric.value === "boolean")));
|
|
137
154
|
const missing = [...pins].some((pin) => !own.some((result) => result.scorerVersionId === pin));
|
|
138
155
|
const noneApplies = !missing && own.length > 0 && !applicable.length;
|
|
139
156
|
const noneDecides = !missing && applicable.length > 0 && !deciding.length;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
157
|
+
// A real failure still decides the case if only required evaluators are unavailable. Other
|
|
158
|
+
// scorer errors keep their existing precedence over quality metrics.
|
|
159
|
+
const failureWins = failing.length > 0 && errors.every((result) => required.has(result.scorerVersionId));
|
|
160
|
+
const state = failureWins
|
|
161
|
+
? "failed"
|
|
162
|
+
: errors.length || noneApplies || noneDecides
|
|
163
|
+
? "error"
|
|
164
|
+
: failing.length
|
|
165
|
+
? "failed"
|
|
166
|
+
: missing || !own.length
|
|
167
|
+
? "pending"
|
|
168
|
+
: undecided.length
|
|
169
|
+
? "skipped"
|
|
170
|
+
: scored.length
|
|
171
|
+
? "passed"
|
|
172
|
+
: "skipped";
|
|
149
173
|
const requires = [
|
|
150
174
|
...new Set(inapplicable.flatMap((result) => (result.requires ? [result.requires] : []))),
|
|
151
175
|
];
|
|
@@ -159,6 +183,11 @@ export function summarizeVerdicts(results, options) {
|
|
|
159
183
|
advisory: advisory.map((result) => result.scorerVersionId),
|
|
160
184
|
metrics,
|
|
161
185
|
explanations: [
|
|
186
|
+
...(state === "skipped" &&
|
|
187
|
+
undecided.length &&
|
|
188
|
+
!undecided.some((result) => /^inconclusive:/i.test(result.explanation ?? ""))
|
|
189
|
+
? ["Inconclusive: a required evaluator did not decide this case."]
|
|
190
|
+
: []),
|
|
162
191
|
...(noneApplies
|
|
163
192
|
? [
|
|
164
193
|
`No pinned evaluator applies to this case${requires.length ? ` (they need ${requires.join(" or ")})` : ""}; pin one that grades it`,
|
|
@@ -173,7 +202,7 @@ export function summarizeVerdicts(results, options) {
|
|
|
173
202
|
]
|
|
174
203
|
: []),
|
|
175
204
|
...deciding
|
|
176
|
-
.filter((result) => result.state !== "scored" || failing.includes(result))
|
|
205
|
+
.filter((result) => result.state !== "scored" || failing.includes(result) || undecided.includes(result))
|
|
177
206
|
.map((result) => result.explanation)
|
|
178
207
|
.filter((explanation) => !!explanation),
|
|
179
208
|
],
|
|
@@ -227,9 +256,11 @@ export async function collectExperimentVerdicts(client, options) {
|
|
|
227
256
|
const experiment = await client.getExperiment(options.experimentId);
|
|
228
257
|
const scorerVersionIds = experiment.evaluation.scorerVersions.map((version) => version.id);
|
|
229
258
|
const { experimentId: _experimentId, ...wait } = options;
|
|
230
|
-
// Hue
|
|
259
|
+
// Only non-required Hue judges can be advisory. Their immutable policy is already in this
|
|
260
|
+
// response; a required version's result evidence cannot override it.
|
|
231
261
|
const judgeScorerVersionIds = experiment.evaluation.scorerVersions
|
|
232
|
-
.filter((version) => version.definition?.kind === "world_judge"
|
|
262
|
+
.filter((version) => version.definition?.kind === "world_judge" &&
|
|
263
|
+
!requiresDecision(version.definition))
|
|
233
264
|
.map((version) => version.id);
|
|
234
265
|
const results = await waitForResults(client, {
|
|
235
266
|
...wait,
|
|
@@ -250,6 +281,11 @@ export async function collectExperimentVerdicts(client, options) {
|
|
|
250
281
|
runId: experiment.evaluation.id,
|
|
251
282
|
scorerVersionIds,
|
|
252
283
|
results,
|
|
253
|
-
summary: summarizeVerdicts(results, {
|
|
284
|
+
summary: summarizeVerdicts(results, {
|
|
285
|
+
experimentItems,
|
|
286
|
+
scorerVersionIds,
|
|
287
|
+
scorerVersions: experiment.evaluation.scorerVersions,
|
|
288
|
+
subjects,
|
|
289
|
+
}),
|
|
254
290
|
};
|
|
255
291
|
}
|
package/dist/inline-files.d.ts
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
/** Inline file content larger than this many UTF-8 bytes is exported as its digest instead. */
|
|
2
2
|
export declare const INLINE_FILE_LIMIT: number;
|
|
3
|
+
/** The longest message text, in UTF-8 bytes, inspected for inline files. */
|
|
4
|
+
export declare const MAX_INLINE_FILE_TEXT: number;
|
|
3
5
|
/** Message text one record inspects for inline files in all, in UTF-16 code units. */
|
|
4
6
|
export declare const INLINE_FILE_TEXT_PER_RECORD: number;
|
|
5
7
|
/** Whether an attribute is one of the recorded message attributes that can inline files. */
|
|
6
8
|
export declare function isMessageKey(key: string): boolean;
|
|
9
|
+
/** An inline file's bytes, whether they were decoded from the part's text (base64 or
|
|
10
|
+
* percent-escapes) rather than taken as its UTF-8, and the media type its `data:` URL names. */
|
|
11
|
+
export interface InlineFileBytes {
|
|
12
|
+
bytes: Buffer;
|
|
13
|
+
decoded: boolean;
|
|
14
|
+
urlMediaType?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The bytes an inline file part carries, the file's own bytes whatever its media type: a `data:`
|
|
18
|
+
* URL decoded by its own encoding (base64 with `;base64`, percent-escapes otherwise), content in
|
|
19
|
+
* the base64 alphabet decoded, and anything else, such as a text file's own text, as UTF-8. A
|
|
20
|
+
* `data:` URL whose data does not decode is taken as UTF-8 too.
|
|
21
|
+
*/
|
|
22
|
+
export declare function inlineFileBytes(content: string): InlineFileBytes;
|
|
23
|
+
/** The key holding a part's inline content: GenAI `blob` parts and AI SDK 6 `file` parts. */
|
|
24
|
+
export declare function contentKey(part: Record<string, unknown>): "content" | "data" | undefined;
|
|
7
25
|
/**
|
|
8
26
|
* Replaces inline files longer than {@link INLINE_FILE_LIMIT} in a recorded message attribute
|
|
9
27
|
* with their SHA-256 and byte size, so a span that inlines a large file exports the file's
|
|
@@ -11,4 +29,29 @@ export declare function isMessageKey(key: string): boolean;
|
|
|
11
29
|
* `mime_type`, `modality`, `mediaType`, …). Other attributes, shorter messages and values that
|
|
12
30
|
* are not JSON are returned unchanged.
|
|
13
31
|
*/
|
|
14
|
-
export declare function hashInlineFiles(key: string, value: unknown
|
|
32
|
+
export declare function hashInlineFiles(key: string, value: unknown,
|
|
33
|
+
/** Counts the files replaced by their digest, when given. */
|
|
34
|
+
count?: {
|
|
35
|
+
files: number;
|
|
36
|
+
}): unknown;
|
|
37
|
+
/** One inline file part found in a parsed message: the part object, the key holding its inline
|
|
38
|
+
* content, that content, and the part's content as a JSON Pointer (RFC 6901) into the message. */
|
|
39
|
+
export interface InlineFilePart {
|
|
40
|
+
part: Record<string, unknown>;
|
|
41
|
+
key: "content" | "data";
|
|
42
|
+
content: string;
|
|
43
|
+
pointer: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The inline file parts of a parsed message whose content text is longer than
|
|
47
|
+
* {@link INLINE_FILE_LIMIT} UTF-8 bytes, in document order: the caller decodes each and keeps the
|
|
48
|
+
* ones whose file bytes are over the limit. The parts are the parsed value's own objects, so the
|
|
49
|
+
* caller may replace their content in place.
|
|
50
|
+
*/
|
|
51
|
+
export declare function largeInlineFileParts(value: unknown): InlineFilePart[];
|
|
52
|
+
/** The digest an inline file part is exported as when its bytes are not uploaded: the part's
|
|
53
|
+
* other fields with the file's SHA-256 and byte size, as {@link hashInlineFiles} writes it. */
|
|
54
|
+
export declare function digestPart(part: Record<string, unknown>, key: "content" | "data", digest: {
|
|
55
|
+
sha256: string;
|
|
56
|
+
size: number;
|
|
57
|
+
}): void;
|