@hue-run/sdk 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EVALUATIONS.md +141 -0
- package/LICENSE +18 -0
- package/README.md +196 -0
- package/dist/ai-sdk.d.ts +4 -0
- package/dist/ai-sdk.js +10 -0
- package/dist/client.d.ts +40 -0
- package/dist/client.js +309 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +32 -0
- package/dist/evals/checkpoint.d.ts +9 -0
- package/dist/evals/checkpoint.js +89 -0
- package/dist/evals/client.d.ts +96 -0
- package/dist/evals/client.js +195 -0
- package/dist/evals/json.d.ts +6 -0
- package/dist/evals/json.js +61 -0
- package/dist/evals/runner.d.ts +49 -0
- package/dist/evals/runner.js +369 -0
- package/dist/evals/schema-worker.d.ts +1 -0
- package/dist/evals/schema-worker.js +14 -0
- package/dist/evals/scorers.d.ts +21 -0
- package/dist/evals/scorers.js +212 -0
- package/dist/evals/types.d.ts +301 -0
- package/dist/evals/types.js +1 -0
- package/dist/evals.d.ts +7 -0
- package/dist/evals.js +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/privacy.d.ts +7 -0
- package/dist/privacy.js +138 -0
- package/dist/transport.d.ts +44 -0
- package/dist/transport.js +320 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.js +1 -0
- package/package.json +79 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { context, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
|
|
3
|
+
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
4
|
+
import { LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
5
|
+
import { TracerProvider } from "@opentelemetry/sdk-trace";
|
|
6
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
7
|
+
import { MAX_CONTENT_BYTES } from "./config.js";
|
|
8
|
+
import { createHueTransport, HueExportError } from "./transport.js";
|
|
9
|
+
export class HueConnectionError extends Error {
|
|
10
|
+
status;
|
|
11
|
+
constructor(message, status) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.name = "HueConnectionError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function identifier(value) {
|
|
18
|
+
if (value === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (typeof value !== "string" ||
|
|
21
|
+
!value ||
|
|
22
|
+
value.length > 4096 ||
|
|
23
|
+
value.includes("\u0000") ||
|
|
24
|
+
!value.isWellFormed())
|
|
25
|
+
throw new TypeError("Session/user identifiers must contain 1–4096 valid characters");
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
/** Local async context preserves nesting without registering or replacing global OTel providers. */
|
|
29
|
+
class ContextualTracer {
|
|
30
|
+
source;
|
|
31
|
+
storage;
|
|
32
|
+
constructor(source, storage) {
|
|
33
|
+
this.source = source;
|
|
34
|
+
this.storage = storage;
|
|
35
|
+
}
|
|
36
|
+
startSpan(name, options = {}, parent) {
|
|
37
|
+
const active = this.storage.getStore();
|
|
38
|
+
return this.source.startSpan(name, {
|
|
39
|
+
...options,
|
|
40
|
+
attributes: {
|
|
41
|
+
...options.attributes,
|
|
42
|
+
...(active?.sessionId ? { "gen_ai.conversation.id": active.sessionId } : {}),
|
|
43
|
+
...(active?.userId ? { "user.id": active.userId } : {}),
|
|
44
|
+
},
|
|
45
|
+
}, parent ?? active?.context ?? context.active());
|
|
46
|
+
}
|
|
47
|
+
startActiveSpan(name, optionsOrFn, contextOrFn, fn) {
|
|
48
|
+
const options = typeof optionsOrFn === "function" ? {} : optionsOrFn;
|
|
49
|
+
const callback = typeof optionsOrFn === "function"
|
|
50
|
+
? optionsOrFn
|
|
51
|
+
: typeof contextOrFn === "function"
|
|
52
|
+
? contextOrFn
|
|
53
|
+
: fn;
|
|
54
|
+
if (!callback)
|
|
55
|
+
throw new TypeError("A span callback is required");
|
|
56
|
+
const parent = typeof contextOrFn === "object"
|
|
57
|
+
? contextOrFn
|
|
58
|
+
: (this.storage.getStore()?.context ?? context.active());
|
|
59
|
+
const span = this.startSpan(name, options, parent);
|
|
60
|
+
return this.storage.run({ ...this.storage.getStore(), context: trace.setSpan(parent, span) }, () => callback(span));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export class HueClient {
|
|
64
|
+
transport;
|
|
65
|
+
tracer;
|
|
66
|
+
captureContent;
|
|
67
|
+
logger;
|
|
68
|
+
storage = new AsyncLocalStorage();
|
|
69
|
+
tracerProvider;
|
|
70
|
+
loggerProvider;
|
|
71
|
+
ownedProviders;
|
|
72
|
+
closed = false;
|
|
73
|
+
shutdownPromise;
|
|
74
|
+
flushPromise;
|
|
75
|
+
constructor(options) {
|
|
76
|
+
if ("transport" in options) {
|
|
77
|
+
this.transport = options.transport;
|
|
78
|
+
this.tracerProvider = options.tracerProvider;
|
|
79
|
+
this.loggerProvider = options.loggerProvider;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
this.transport = createHueTransport(options);
|
|
83
|
+
const resource = resourceFromAttributes({
|
|
84
|
+
"service.name": options.serviceName,
|
|
85
|
+
...(options.serviceVersion ? { "service.version": options.serviceVersion } : {}),
|
|
86
|
+
});
|
|
87
|
+
const tracer = new TracerProvider({
|
|
88
|
+
resource,
|
|
89
|
+
spanProcessors: [this.transport.spanProcessor],
|
|
90
|
+
});
|
|
91
|
+
const logger = new LoggerProvider({
|
|
92
|
+
resource,
|
|
93
|
+
processors: [this.transport.logRecordProcessor],
|
|
94
|
+
});
|
|
95
|
+
this.ownedProviders = { tracer, logger };
|
|
96
|
+
this.tracerProvider = tracer;
|
|
97
|
+
this.loggerProvider = logger;
|
|
98
|
+
}
|
|
99
|
+
this.captureContent = this.transport.options.captureContent;
|
|
100
|
+
this.tracer = new ContextualTracer(this.tracerProvider.getTracer("@hue-run/sdk", "0.1.2"), this.storage);
|
|
101
|
+
this.logger = this.loggerProvider.getLogger("@hue-run/sdk", "0.1.2");
|
|
102
|
+
}
|
|
103
|
+
getContext() {
|
|
104
|
+
return this.storage.getStore()?.context ?? context.active();
|
|
105
|
+
}
|
|
106
|
+
async withSpan(name, callback, options = {}) {
|
|
107
|
+
if (this.closed)
|
|
108
|
+
throw new Error("Hue client is shut down");
|
|
109
|
+
const inherited = this.storage.getStore();
|
|
110
|
+
const sessionId = identifier(options.sessionId ?? inherited?.sessionId);
|
|
111
|
+
const userId = identifier(options.userId ?? inherited?.userId);
|
|
112
|
+
const parent = options.parentContext ?? inherited?.context ?? context.active();
|
|
113
|
+
const active = { context: parent, sessionId, userId };
|
|
114
|
+
return this.storage.run(active, async () => {
|
|
115
|
+
const span = this.tracer.startSpan(name, { kind: options.kind ?? SpanKind.INTERNAL, attributes: options.attributes }, parent);
|
|
116
|
+
const spanContext = trace.setSpan(parent, span);
|
|
117
|
+
const handle = {
|
|
118
|
+
span,
|
|
119
|
+
context: spanContext,
|
|
120
|
+
traceId: span.spanContext().traceId,
|
|
121
|
+
spanId: span.spanContext().spanId,
|
|
122
|
+
setInput: (value) => this.setContent(span, "input.value", value),
|
|
123
|
+
setOutput: (value) => this.setContent(span, "output.value", value),
|
|
124
|
+
};
|
|
125
|
+
return this.storage.run({ ...active, context: spanContext }, async () => {
|
|
126
|
+
try {
|
|
127
|
+
if (options.input !== undefined)
|
|
128
|
+
handle.setInput(options.input);
|
|
129
|
+
return await callback(handle);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
this.recordError(span, error);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
span.end();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async tool(name, input, execute) {
|
|
142
|
+
return this.withSpan(name, async ({ span }) => {
|
|
143
|
+
this.setContent(span, "gen_ai.tool.call.arguments", input);
|
|
144
|
+
const result = await execute();
|
|
145
|
+
if (result !== undefined)
|
|
146
|
+
this.setContent(span, "gen_ai.tool.call.result", result);
|
|
147
|
+
return result;
|
|
148
|
+
}, { attributes: { "gen_ai.operation.name": "execute_tool", "gen_ai.tool.name": name } });
|
|
149
|
+
}
|
|
150
|
+
recordError(span, error) {
|
|
151
|
+
const type = error instanceof Error ? error.name : "Error";
|
|
152
|
+
span.setStatus({
|
|
153
|
+
code: SpanStatusCode.ERROR,
|
|
154
|
+
...(this.captureContent
|
|
155
|
+
? { message: error instanceof Error ? error.message : "Operation failed" }
|
|
156
|
+
: {}),
|
|
157
|
+
});
|
|
158
|
+
span.addEvent("exception", {
|
|
159
|
+
"exception.type": type,
|
|
160
|
+
...(this.captureContent && error instanceof Error
|
|
161
|
+
? {
|
|
162
|
+
"exception.message": error.message,
|
|
163
|
+
...(error.stack ? { "exception.stacktrace": error.stack } : {}),
|
|
164
|
+
}
|
|
165
|
+
: {}),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
recordMessages(messages, explicitContext) {
|
|
169
|
+
if (this.closed)
|
|
170
|
+
throw new Error("Hue client is shut down");
|
|
171
|
+
if (!this.captureContent)
|
|
172
|
+
return;
|
|
173
|
+
const active = explicitContext ?? this.getContext();
|
|
174
|
+
if (!trace.getSpanContext(active))
|
|
175
|
+
throw new Error("Message records require an active span or explicit span context");
|
|
176
|
+
const body = {};
|
|
177
|
+
if (messages.input !== undefined)
|
|
178
|
+
body["gen_ai.input.messages"] = messages.input;
|
|
179
|
+
if (messages.output !== undefined)
|
|
180
|
+
body["gen_ai.output.messages"] = messages.output;
|
|
181
|
+
this.logger.emit({
|
|
182
|
+
context: active,
|
|
183
|
+
severityNumber: SeverityNumber.INFO,
|
|
184
|
+
eventName: "gen_ai.client.inference.operation.details",
|
|
185
|
+
body,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
setContent(span, key, value) {
|
|
189
|
+
if (!this.captureContent)
|
|
190
|
+
return;
|
|
191
|
+
const encoded = JSON.stringify(value, (_key, item) => {
|
|
192
|
+
if (typeof item === "number" && !Number.isFinite(item))
|
|
193
|
+
throw new TypeError("Captured JSON numbers must be finite");
|
|
194
|
+
return item;
|
|
195
|
+
});
|
|
196
|
+
if (encoded === undefined || Buffer.byteLength(encoded) > MAX_CONTENT_BYTES)
|
|
197
|
+
throw new RangeError("Captured content must be JSON and no more than 256 KiB");
|
|
198
|
+
span.setAttribute(key, encoded);
|
|
199
|
+
}
|
|
200
|
+
async checkConnection() {
|
|
201
|
+
const options = this.transport.options;
|
|
202
|
+
let response;
|
|
203
|
+
try {
|
|
204
|
+
response = await fetch(`${options.baseUrl}/api/v1/projects/current`, {
|
|
205
|
+
headers: { Authorization: `Bearer ${options.apiKey}` },
|
|
206
|
+
redirect: "error",
|
|
207
|
+
signal: AbortSignal.timeout(options.timeoutMillis),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
throw new HueConnectionError("Unable to connect to Hue; check the endpoint and network");
|
|
212
|
+
}
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
await response.body?.cancel();
|
|
215
|
+
throw new HueConnectionError("Hue rejected the project connection", response.status);
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
if (Number(response.headers.get("content-length") ?? 0) > 65536)
|
|
219
|
+
throw new Error("Oversized project response");
|
|
220
|
+
const reader = response.body?.getReader();
|
|
221
|
+
if (!reader)
|
|
222
|
+
throw new Error("Missing project response");
|
|
223
|
+
let size = 0;
|
|
224
|
+
const chunks = [];
|
|
225
|
+
try {
|
|
226
|
+
for (;;) {
|
|
227
|
+
const { done, value } = await reader.read();
|
|
228
|
+
if (done)
|
|
229
|
+
break;
|
|
230
|
+
size += value.byteLength;
|
|
231
|
+
if (size > 65536)
|
|
232
|
+
throw new Error("Oversized project response");
|
|
233
|
+
chunks.push(value);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
await reader.cancel();
|
|
238
|
+
}
|
|
239
|
+
const project = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
240
|
+
if (!project || typeof project !== "object")
|
|
241
|
+
throw new Error("Invalid project response");
|
|
242
|
+
const fields = project;
|
|
243
|
+
for (const key of ["id", "name", "organizationId", "slug"])
|
|
244
|
+
if (typeof fields[key] !== "string")
|
|
245
|
+
throw new Error("Invalid project response");
|
|
246
|
+
return {
|
|
247
|
+
id: fields.id,
|
|
248
|
+
name: fields.name,
|
|
249
|
+
organizationId: fields.organizationId,
|
|
250
|
+
slug: fields.slug,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
throw new HueConnectionError("Hue returned an invalid project response");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
flush() {
|
|
258
|
+
// Each caller needs a drain after its own preceding span/log emissions.
|
|
259
|
+
// Joining an earlier drain can acknowledge records that were not in its batch.
|
|
260
|
+
const from = this.transport.getFailureSequence();
|
|
261
|
+
const drain = async () => {
|
|
262
|
+
const report = await this.flushOnce();
|
|
263
|
+
if (this.transport.getFailureSequence() !== from)
|
|
264
|
+
throw new HueExportError(this.transport
|
|
265
|
+
.getIssues()
|
|
266
|
+
.filter((issue) => issue.sequence > from && issue.kind !== "warning"), report);
|
|
267
|
+
return report;
|
|
268
|
+
};
|
|
269
|
+
const next = (this.flushPromise ?? Promise.resolve()).then(drain, drain);
|
|
270
|
+
this.flushPromise = next;
|
|
271
|
+
const clear = () => {
|
|
272
|
+
if (this.flushPromise === next)
|
|
273
|
+
this.flushPromise = undefined;
|
|
274
|
+
};
|
|
275
|
+
void next.then(clear, clear);
|
|
276
|
+
return next;
|
|
277
|
+
}
|
|
278
|
+
async flushOnce() {
|
|
279
|
+
const results = await Promise.allSettled([
|
|
280
|
+
this.tracerProvider.forceFlush(),
|
|
281
|
+
this.loggerProvider.forceFlush(),
|
|
282
|
+
]);
|
|
283
|
+
for (const [index, result] of results.entries())
|
|
284
|
+
if (result.status === "rejected")
|
|
285
|
+
this.transport.issue(index === 0 ? "traces" : "logs", "failed", 0, "OpenTelemetry provider flush failed");
|
|
286
|
+
return this.transport.flush();
|
|
287
|
+
}
|
|
288
|
+
shutdown() {
|
|
289
|
+
this.shutdownPromise ??= (async () => {
|
|
290
|
+
this.closed = true;
|
|
291
|
+
try {
|
|
292
|
+
return await this.flush();
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
if (this.ownedProviders) {
|
|
296
|
+
await Promise.allSettled([
|
|
297
|
+
this.ownedProviders.tracer.shutdown(),
|
|
298
|
+
this.ownedProviders.logger.shutdown(),
|
|
299
|
+
]);
|
|
300
|
+
await this.transport.shutdown();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
})();
|
|
304
|
+
return this.shutdownPromise;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
export function createHue(options) {
|
|
308
|
+
return new HueClient(options);
|
|
309
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { HueOptions } from "./types.js";
|
|
2
|
+
export declare const MAX_BODY_BYTES: number;
|
|
3
|
+
export declare const MAX_CONTENT_BYTES: number;
|
|
4
|
+
export declare function validateOptions(options: HueOptions): Required<Pick<HueOptions, "apiKey" | "serviceName" | "baseUrl" | "captureContent" | "timeoutMillis">> & HueOptions;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const MAX_BODY_BYTES = 1024 * 1024;
|
|
2
|
+
export const MAX_CONTENT_BYTES = 256 * 1024;
|
|
3
|
+
export function validateOptions(options) {
|
|
4
|
+
if (typeof options.captureContent !== "boolean")
|
|
5
|
+
throw new TypeError("Choose captureContent explicitly: true or false");
|
|
6
|
+
if (typeof options.apiKey !== "string" ||
|
|
7
|
+
!options.apiKey ||
|
|
8
|
+
options.apiKey.length > 4096 ||
|
|
9
|
+
/\s/.test(options.apiKey) ||
|
|
10
|
+
options.apiKey.includes("\u0000"))
|
|
11
|
+
throw new TypeError("A valid Hue project API key is required");
|
|
12
|
+
if (typeof options.serviceName !== "string" ||
|
|
13
|
+
!options.serviceName.trim() ||
|
|
14
|
+
options.serviceName.length > 256)
|
|
15
|
+
throw new TypeError("A serviceName of 1–256 characters is required");
|
|
16
|
+
let url;
|
|
17
|
+
try {
|
|
18
|
+
url = new URL(options.baseUrl ?? "https://app.hue.run");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new TypeError("Invalid Hue baseUrl");
|
|
22
|
+
}
|
|
23
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
24
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback))
|
|
25
|
+
throw new TypeError("Hue requires HTTPS except for a loopback development server");
|
|
26
|
+
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash)
|
|
27
|
+
throw new TypeError("Hue baseUrl must be an origin without credentials, a path, query parameters or fragments");
|
|
28
|
+
const timeoutMillis = options.timeoutMillis ?? 10000;
|
|
29
|
+
if (!Number.isInteger(timeoutMillis) || timeoutMillis < 100 || timeoutMillis > 60000)
|
|
30
|
+
throw new TypeError("timeoutMillis must be 100–60000");
|
|
31
|
+
return { ...options, baseUrl: url.origin, timeoutMillis };
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** One owner per directory. A crash leaves .lock for explicit operator recovery. */
|
|
2
|
+
export declare class CheckpointStore {
|
|
3
|
+
readonly directory: string;
|
|
4
|
+
private constructor();
|
|
5
|
+
static acquire(directory: string, identity: unknown): Promise<CheckpointStore>;
|
|
6
|
+
read<T>(key: string): Promise<T | undefined>;
|
|
7
|
+
write(key: string, value: unknown): Promise<void>;
|
|
8
|
+
release(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, mkdir, open, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { digest } from "./json.js";
|
|
6
|
+
/** One owner per directory. A crash leaves .lock for explicit operator recovery. */
|
|
7
|
+
export class CheckpointStore {
|
|
8
|
+
directory;
|
|
9
|
+
constructor(directory) {
|
|
10
|
+
this.directory = directory;
|
|
11
|
+
}
|
|
12
|
+
static async acquire(directory, identity) {
|
|
13
|
+
const root = resolve(directory);
|
|
14
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
15
|
+
const info = await lstat(root);
|
|
16
|
+
if (!info.isDirectory() || info.isSymbolicLink() || (info.mode & 0o077) !== 0)
|
|
17
|
+
throw new Error("Use a private checkpoint directory (mode 0700, no symlink)");
|
|
18
|
+
const store = new CheckpointStore(root);
|
|
19
|
+
try {
|
|
20
|
+
await mkdir(join(root, ".lock"), { mode: 0o700 });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new Error("Checkpoint directory is locked; confirm its owner stopped before explicitly removing .lock");
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
await store.write(".lock/owner", { pid: process.pid });
|
|
27
|
+
const expected = { format: 1, identity, digest: digest(identity) };
|
|
28
|
+
const prior = await store.read("manifest");
|
|
29
|
+
if (prior && (prior.format !== 1 || prior.digest !== expected.digest))
|
|
30
|
+
throw new Error("Checkpoint identity differs from this project, run, pins or content policy");
|
|
31
|
+
if (!prior)
|
|
32
|
+
await store.write("manifest", expected);
|
|
33
|
+
return store;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
await store.release();
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async read(key) {
|
|
41
|
+
let file;
|
|
42
|
+
try {
|
|
43
|
+
file = await open(join(this.directory, `${key}.json`), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error.code === "ENOENT")
|
|
47
|
+
return undefined;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const info = await file.stat();
|
|
52
|
+
if (!info.isFile() || info.size > 8 * 1024 * 1024 || (info.mode & 0o077) !== 0)
|
|
53
|
+
throw new Error("Unsafe or oversized checkpoint");
|
|
54
|
+
const envelope = JSON.parse(await file.readFile("utf8"));
|
|
55
|
+
if (digest(envelope.value) !== envelope.digest)
|
|
56
|
+
throw new Error("Checkpoint integrity check failed");
|
|
57
|
+
return envelope.value;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await file.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async write(key, value) {
|
|
64
|
+
const encoded = JSON.stringify({ value, digest: digest(value) });
|
|
65
|
+
if (Buffer.byteLength(encoded) > 8 * 1024 * 1024)
|
|
66
|
+
throw new RangeError("Checkpoint exceeds 8 MiB");
|
|
67
|
+
const destination = join(this.directory, `${key}.json`);
|
|
68
|
+
const temporary = `${destination}.${randomUUID()}.tmp`;
|
|
69
|
+
const file = await open(temporary, "wx", 0o600);
|
|
70
|
+
try {
|
|
71
|
+
await file.writeFile(encoded);
|
|
72
|
+
await file.sync();
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await file.close();
|
|
76
|
+
}
|
|
77
|
+
await rename(temporary, destination);
|
|
78
|
+
const directory = await open(this.directory, "r");
|
|
79
|
+
try {
|
|
80
|
+
await directory.sync();
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await directory.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async release() {
|
|
87
|
+
await rm(join(this.directory, ".lock"), { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ProjectConnection } from "../types.js";
|
|
2
|
+
import type { CaseWrite, CompleteExecution, Completion, Dataset, DatasetCase, DatasetVersion, EvaluationItem, EvaluationRun, Execution, Experiment, ExperimentCase, ExperimentItem, Identity, JsonValue, JudgeBudget, JudgeJob, Page, PageOptions, Result, ResultSummary, Scorer, ScorerDefinition, ScorerVersion, StartExecution, Subject, StoredResult } from "./types.js";
|
|
3
|
+
export interface EvaluationClientOptions {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
timeoutMillis?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare class HueApiError extends Error {
|
|
9
|
+
readonly status?: number | undefined;
|
|
10
|
+
constructor(status?: number | undefined);
|
|
11
|
+
}
|
|
12
|
+
/** No implicit mutation retry: callers retain stable idempotency keys for experiments/results. */
|
|
13
|
+
export declare class EvaluationClient {
|
|
14
|
+
readonly baseUrl: string;
|
|
15
|
+
private readonly apiKey;
|
|
16
|
+
private readonly timeoutMillis;
|
|
17
|
+
constructor(options: EvaluationClientOptions);
|
|
18
|
+
private request;
|
|
19
|
+
private page;
|
|
20
|
+
checkConnection(): Promise<ProjectConnection>;
|
|
21
|
+
createDataset(input: Identity): Promise<Dataset>;
|
|
22
|
+
getDataset(id: string): Promise<Dataset>;
|
|
23
|
+
listDatasets(page?: PageOptions): Promise<Page<Omit<Dataset, "versions">>>;
|
|
24
|
+
createDatasetVersion(id: string, input?: {
|
|
25
|
+
fromVersionId?: string;
|
|
26
|
+
}): Promise<DatasetVersion>;
|
|
27
|
+
getDatasetVersion(id: string): Promise<DatasetVersion>;
|
|
28
|
+
listCases(id: string, page?: PageOptions): Promise<Page<DatasetCase>>;
|
|
29
|
+
addCase(id: string, input: CaseWrite): Promise<{
|
|
30
|
+
item: DatasetCase;
|
|
31
|
+
version: DatasetVersion;
|
|
32
|
+
}>;
|
|
33
|
+
freezeDatasetVersion(id: string, expectedRevision: number): Promise<DatasetVersion>;
|
|
34
|
+
createScorer(input: Identity): Promise<Scorer>;
|
|
35
|
+
getScorer(id: string): Promise<Scorer>;
|
|
36
|
+
listScorers(page?: PageOptions): Promise<Page<Scorer>>;
|
|
37
|
+
publishScorerVersion(id: string, definition: ScorerDefinition): Promise<ScorerVersion>;
|
|
38
|
+
getScorerVersion(id: string): Promise<ScorerVersion>;
|
|
39
|
+
createExperiment(input: {
|
|
40
|
+
idempotencyKey: string;
|
|
41
|
+
name: string;
|
|
42
|
+
datasetVersionId: string;
|
|
43
|
+
scorerVersionIds: string[];
|
|
44
|
+
config: JsonValue;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
id: string;
|
|
47
|
+
evaluationRunId: string;
|
|
48
|
+
}>;
|
|
49
|
+
getExperiment(id: string): Promise<Experiment>;
|
|
50
|
+
listExperimentItems(id: string, page?: PageOptions): Promise<Page<ExperimentItem>>;
|
|
51
|
+
getExperimentCase(id: string, caseId: string): Promise<ExperimentCase>;
|
|
52
|
+
startExecution(id: string, caseId: string, input: StartExecution): Promise<Execution>;
|
|
53
|
+
getExecution(id: string): Promise<Execution>;
|
|
54
|
+
completeExecution(id: string, input: CompleteExecution): Promise<Completion>;
|
|
55
|
+
finishExperiment(id: string, idempotencyKey: string): Promise<{
|
|
56
|
+
id: string;
|
|
57
|
+
finishedAt: string;
|
|
58
|
+
}>;
|
|
59
|
+
createEvaluationRun(input: {
|
|
60
|
+
idempotencyKey: string;
|
|
61
|
+
name: string;
|
|
62
|
+
subjectIds: string[];
|
|
63
|
+
scorerVersionIds: string[];
|
|
64
|
+
}): Promise<{
|
|
65
|
+
id: string;
|
|
66
|
+
}>;
|
|
67
|
+
getEvaluationRun(id: string): Promise<EvaluationRun>;
|
|
68
|
+
listEvaluationItems(id: string, page?: PageOptions): Promise<Page<EvaluationItem>>;
|
|
69
|
+
getSubject(id: string): Promise<Subject>;
|
|
70
|
+
submitResults(id: string, input: {
|
|
71
|
+
idempotencyKey: string;
|
|
72
|
+
results: Result[];
|
|
73
|
+
}): Promise<{
|
|
74
|
+
ids: string[];
|
|
75
|
+
}>;
|
|
76
|
+
listResults(id: string, page?: PageOptions): Promise<Page<ResultSummary>>;
|
|
77
|
+
getResult(id: string): Promise<StoredResult>;
|
|
78
|
+
createJudgeJobs(id: string, input: {
|
|
79
|
+
idempotencyKey: string;
|
|
80
|
+
jobs: {
|
|
81
|
+
evaluationItemId: string;
|
|
82
|
+
scorerVersionId: string;
|
|
83
|
+
}[];
|
|
84
|
+
}): Promise<{
|
|
85
|
+
ids: string[];
|
|
86
|
+
}>;
|
|
87
|
+
listJudgeJobs(id: string, page?: PageOptions): Promise<Page<JudgeJob>>;
|
|
88
|
+
getJudgeJob(id: string): Promise<JudgeJob>;
|
|
89
|
+
cancelJudgeJob(id: string, reason: string): Promise<{
|
|
90
|
+
id: string;
|
|
91
|
+
state: JudgeJob["state"];
|
|
92
|
+
cancellationRequested?: boolean;
|
|
93
|
+
}>;
|
|
94
|
+
getJudgeBudget(): Promise<JudgeBudget>;
|
|
95
|
+
}
|
|
96
|
+
export declare function createEvaluationClient(options: EvaluationClientOptions): EvaluationClient;
|