@hue-run/sdk 0.14.0 → 0.15.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/CLI.md +21 -14
- package/EVALUATIONS.md +1 -1
- package/README.md +112 -19
- package/dist/blobs.d.ts +164 -0
- package/dist/blobs.js +703 -0
- package/dist/cli/eval.js +3 -3
- package/dist/client.d.ts +15 -5
- package/dist/client.js +63 -13
- package/dist/config.d.ts +20 -0
- package/dist/config.js +21 -1
- 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/local-worker.d.ts +1 -1
- package/dist/evals/runner.d.ts +1 -1
- package/dist/evals/runner.js +2 -2
- package/dist/evals/simulation.d.ts +1 -1
- 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 +8 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/cli/eval.js
CHANGED
|
@@ -85,7 +85,7 @@ Output and limits:
|
|
|
85
85
|
--content the case span still carries the output
|
|
86
86
|
--save-version Freeze an unsaved eval-set version before running
|
|
87
87
|
--checkpoint-dir <path> Private checkpoint directory (default: .hue/eval/<agent-key>)
|
|
88
|
-
--concurrency <n> Cases in flight, 1-
|
|
88
|
+
--concurrency <n> Cases in flight, 1-100 (default: 1)
|
|
89
89
|
--trace-not-accepted <policy> A case whose telemetry Hue did not accept in time: fail_case
|
|
90
90
|
(default) completes it as failed with the evidence omitted;
|
|
91
91
|
pending completes it in its true state with the trace still to
|
|
@@ -1093,7 +1093,7 @@ function parseBaseline(value) {
|
|
|
1093
1093
|
}
|
|
1094
1094
|
async function runOnce(values, connection, agents, agent, hue, output, signal) {
|
|
1095
1095
|
const client = new EvaluationClient(connection);
|
|
1096
|
-
const concurrency = integer("concurrency", values.concurrency, 1, 1,
|
|
1096
|
+
const concurrency = integer("concurrency", values.concurrency, 1, 1, 100);
|
|
1097
1097
|
const baselineId = parseBaseline(values.baseline);
|
|
1098
1098
|
const mode = parseMode(values.mode);
|
|
1099
1099
|
const pins = await resolveSelection(client, values);
|
|
@@ -1296,7 +1296,7 @@ async function runWorker(values, connection, agents, agent, hue, output, signal)
|
|
|
1296
1296
|
const client = new ObservedClient(connection);
|
|
1297
1297
|
const environmentClient = createEnvironmentClient(connection);
|
|
1298
1298
|
const wait = integer("wait", values.wait, 300, 0, 86_400);
|
|
1299
|
-
const concurrency = integer("concurrency", values.concurrency, 1, 1,
|
|
1299
|
+
const concurrency = integer("concurrency", values.concurrency, 1, 1, 100);
|
|
1300
1300
|
const maxRuns = values["max-runs"] === undefined
|
|
1301
1301
|
? undefined
|
|
1302
1302
|
: integer("max-runs", values["max-runs"], 1, 1, 1_000_000);
|
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. */
|
|
@@ -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
|
}));
|
|
@@ -100,7 +100,7 @@ export interface RunLocalAgentOptions {
|
|
|
100
100
|
agent: LocalAgentRegistration;
|
|
101
101
|
/** Local scorer bindings matching the published source digests. */
|
|
102
102
|
scorers?: LocalScorer[];
|
|
103
|
-
/** Cases in flight, between 1 and
|
|
103
|
+
/** Cases in flight, between 1 and 100; defaults to 1. */
|
|
104
104
|
concurrency?: number;
|
|
105
105
|
/** What a case does when its telemetry is not accepted; see
|
|
106
106
|
* `traceNotAccepted` of {@link runExperiment}. Defaults to `"stop"`. */
|
package/dist/evals/runner.d.ts
CHANGED
|
@@ -74,7 +74,7 @@ interface RunnerOptions {
|
|
|
74
74
|
* (for example a Hue-operated grading worker that owns the evaluator source) instead of
|
|
75
75
|
* refusing the run. Their IDs are reported in `deferredScorerVersionIds`. */
|
|
76
76
|
deferUnboundLocalScorers?: boolean;
|
|
77
|
-
/** Cases in flight at once, 1–
|
|
77
|
+
/** Cases in flight at once, 1–100. Default 1. */
|
|
78
78
|
concurrency?: number;
|
|
79
79
|
/** Deadline for JSON Schema scoring in its worker, 100–60000 ms. Default 2000. */
|
|
80
80
|
schemaTimeoutMillis?: number;
|
package/dist/evals/runner.js
CHANGED
|
@@ -183,8 +183,8 @@ function settings(options) {
|
|
|
183
183
|
if (typeof options.persistResultContent !== "boolean")
|
|
184
184
|
throw new TypeError("Choose persistResultContent explicitly: true or false");
|
|
185
185
|
const concurrency = options.concurrency ?? 1;
|
|
186
|
-
if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency >
|
|
187
|
-
throw new RangeError("concurrency must be 1–
|
|
186
|
+
if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 100)
|
|
187
|
+
throw new RangeError("concurrency must be 1–100");
|
|
188
188
|
const timeout = options.schemaTimeoutMillis ?? 2000;
|
|
189
189
|
if (!Number.isInteger(timeout) || timeout < 100 || timeout > 60_000)
|
|
190
190
|
throw new RangeError("schemaTimeoutMillis must be 100–60000");
|
|
@@ -166,7 +166,7 @@ export interface RunSimulationOptions {
|
|
|
166
166
|
onTelemetryNotAccepted?(entry: TelemetryNotAccepted): void | Promise<void>;
|
|
167
167
|
/** Local scorer callbacks bound by their declared source digests. */
|
|
168
168
|
localScorers?: LocalScorer[];
|
|
169
|
-
/** Cases in flight, 1–
|
|
169
|
+
/** Cases in flight, 1–100; defaults to 1. */
|
|
170
170
|
concurrency?: number;
|
|
171
171
|
/** JSON Schema worker deadline in milliseconds. */
|
|
172
172
|
schemaTimeoutMillis?: number;
|
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;
|