@hue-run/sdk 0.1.3 → 0.1.4

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.
@@ -0,0 +1,84 @@
1
+ # Run an existing agent from Hue
2
+
3
+ `@hue-run/sdk/managed` adapts an existing server-side function to Hue's managed
4
+ evaluation protocol. Hue dispatches a frozen case; your application keeps its
5
+ model, tools and OpenTelemetry provider. This helper does not create an agent,
6
+ choose a model, install instrumentation or replace global providers.
7
+
8
+ ```ts
9
+ import { createManagedTargetHandler } from "@hue-run/sdk/managed";
10
+
11
+ // Your application initializes its existing NodeSDK/context manager before requests.
12
+ export const POST = createManagedTargetHandler({
13
+ machineCredential: process.env.HUE_MANAGED_TARGET_SECRET!,
14
+ // baseUrl defaults to https://app.hue.run; configure it here, never from a request.
15
+ target: async ({ input, config, inputFiles, signal }) => {
16
+ const result = await existingAgent({ input, config, files: inputFiles, signal });
17
+ return {
18
+ output: { text: result.text },
19
+ files: result.files.map((file) => ({
20
+ filename: file.name,
21
+ contentType: file.contentType,
22
+ data: file.bytes, // Uint8Array; upload these exact bytes once generated.
23
+ primary: file.isPrimary,
24
+ })),
25
+ };
26
+ },
27
+ flushTelemetry: async () => {
28
+ await flushExistingTracesAndLogs(); // Throw if either pipeline did not flush.
29
+ },
30
+ });
31
+ ```
32
+
33
+ The example's `existingAgent` and `flushExistingTracesAndLogs` are application
34
+ functions. A flush callback must throw on delivery failure; an explicit `false`
35
+ result or a Hue report with pending records also leaves telemetry pending. Hue's
36
+ flush API throws for new export failures; its historical failure counters do not
37
+ invalidate later successful drains. An optional `tracer` uses an already configured provider. The default
38
+ uses the global provider, which must record the supplied sampled trace context.
39
+ Configure a host request limit of at least 120 seconds for the defaults: 90 seconds
40
+ for the target and 30 seconds reserved for uploads, checkpointing and telemetry.
41
+ Targets must honor `signal`; JavaScript cannot forcibly stop a callback that
42
+ ignores cancellation. A callback still running at the deadline returns `uncertain`.
43
+
44
+ ## What the helper guarantees
45
+
46
+ - Validates the dedicated machine credential before network access. The scoped
47
+ `X-Hue-Invocation-Token` is used only for Hue callbacks; neither credential is
48
+ passed to the target, output uploads or telemetry attributes.
49
+ - Claims the assigned execution before calling the target. A duplicate claim
50
+ returns HTTP 409 without another agent call. A lost claim response is uncertain.
51
+ - Downloads only declared files from the configured Hue origin and verifies their
52
+ length and SHA-256 before calling the target. No redirects are followed.
53
+ - Creates a real `ai.managed_target` span under the incoming W3C `traceparent`,
54
+ using the existing provider. It records no input/output content of its own.
55
+ - Uploads returned file buffers, including valid secondary files on an error
56
+ result. Each reservation has a stable idempotency key; an already ready file is
57
+ reused. Only granted content-type/private-access upload headers are accepted.
58
+ - Ends the span, checkpoints the outcome, flushes existing traces and logs, then
59
+ persists a telemetry acknowledgement before returning HTTP 200.
60
+
61
+ HTTP 200 returns `{protocolVersion:1,executionId,state:"checkpointed",telemetry}`.
62
+ `telemetry` is `"flushed"` or `"pending"`; a flush/acknowledgement failure preserves
63
+ the saved outcome. A checkpoint is not experiment completion or a receipt proving
64
+ all evidence has been stored. Hue performs that independent verification.
65
+
66
+ The callback returns `ManagedTargetResult`: optional `output` JSON, `state`
67
+ (`succeeded`, `error`, `cancelled`), a safe `{type,message?}` error, file buffers and
68
+ optional token usage. Omitted output means unavailable; `null` is present output.
69
+ At most one file can be primary. Files are limited to 16, 25 MiB each and 64 MiB
70
+ total; input/output JSON and HTTP envelopes also have bounded sizes. Error text
71
+ is caller-owned public content: never return raw provider exceptions or secrets.
72
+
73
+ Signed file PUTs make one attempt and never follow redirects. If their acknowledgement
74
+ is lost, Hue's completion callback verifies the stored bytes before accepting the file.
75
+ Only identical idempotent reservation/completion/checkpoint/telemetry callbacks retry,
76
+ at most once within the finalization deadline. The helper never retries the agent, automatically
77
+ resumes a lost callback, or claims exactly-once execution across crashes. After
78
+ transport loss or a callback deadline, inspect the saved execution in Hue. An
79
+ explicitly authorized new attempt is a separate execution decision.
80
+
81
+ Protect and rate-limit the endpoint at your host as appropriate. Keep the dedicated
82
+ machine credential server-side. This API uses invocation-scoped callbacks, not the
83
+ project-wide `EvaluationClient` credential. The existing telemetry pipeline retains
84
+ its configured content-capture policy and exporter credentials.
package/README.md CHANGED
@@ -227,3 +227,31 @@ published. The chatbot README describes running that external installation.
227
227
  # Local evaluation workflows
228
228
 
229
229
  The optional `@hue-run/sdk/evals` entry point supports dataset/scorer registration, frozen-version experiments, local built-in/custom scoring, upload resume, and historical rescoring. See the [evaluation guide](https://docs.hue.run/evaluations/first-evaluation) for the complete journey, content policy and checkpoint recovery contract.
230
+
231
+ ## Managed targets
232
+
233
+ Start a frozen dataset run in Hue while your agent stays in your application. Expose a
234
+ protected POST route around your existing function:
235
+
236
+ ```ts
237
+ import { createManagedTargetHandler } from "@hue-run/sdk/managed";
238
+
239
+ export const POST = createManagedTargetHandler({
240
+ machineCredential: process.env.HUE_MANAGED_TARGET_SECRET!,
241
+ // Application-owned functions: keep your current provider, tools and tracing.
242
+ target: async ({ input, config, inputFiles, signal }) =>
243
+ runAgentForEvaluation({ input, config, inputFiles, signal }),
244
+ flushTelemetry: () => hue.flush(), // Existing Hue client; flush traces and logs.
245
+ });
246
+ ```
247
+
248
+ `runAgentForEvaluation` adapts your application result to `{ output, files? }`.
249
+ Files contain `filename`, `contentType`, actual `Uint8Array` data and an optional
250
+ `primary` flag. The helper verifies input bytes, claims the invocation, saves the
251
+ outcome and correlates its span with Hue. It never retries the agent automatically.
252
+ Use a 120-second host request limit for the default 90-second execution and
253
+ 30-second finalization budget; your target must honor `signal`.
254
+
255
+ See the [managed-run guide](https://docs.hue.run/evaluations/managed-runs) and the
256
+ [full adapter contract](MANAGED_TARGETS.md) for registration, file handling,
257
+ existing-provider flush callbacks and recovery. Local/CI runners remain available.
package/dist/client.js CHANGED
@@ -98,8 +98,8 @@ export class HueClient {
98
98
  this.loggerProvider = logger;
99
99
  }
100
100
  this.captureContent = this.transport.options.captureContent;
101
- this.tracer = new ContextualTracer(this.tracerProvider.getTracer("@hue-run/sdk", "0.1.3"), this.storage);
102
- this.logger = this.loggerProvider.getLogger("@hue-run/sdk", "0.1.3");
101
+ this.tracer = new ContextualTracer(this.tracerProvider.getTracer("@hue-run/sdk", "0.1.4"), this.storage);
102
+ this.logger = this.loggerProvider.getLogger("@hue-run/sdk", "0.1.4");
103
103
  }
104
104
  verifyTrace(traceId, options = {}) {
105
105
  return verifyTrace(this.transport.options, traceId, options);
@@ -55,6 +55,7 @@ export function sourceDigest(source) {
55
55
  }
56
56
  export function uuid(value) {
57
57
  if (typeof value !== "string" ||
58
+ value.length !== 36 ||
58
59
  !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value))
59
60
  throw new TypeError("Expected a UUID");
60
61
  return value;
@@ -0,0 +1,65 @@
1
+ import { type Tracer } from "@opentelemetry/api";
2
+ import type { JsonValue } from "./types.js";
3
+ export interface ManagedInputFile {
4
+ artifactId: string;
5
+ filename: string;
6
+ contentType: string;
7
+ byteSize: number;
8
+ sha256: string;
9
+ role: string;
10
+ }
11
+ export interface ManagedInvocation {
12
+ protocolVersion: 1;
13
+ executionId: string;
14
+ attempt: number;
15
+ input: JsonValue;
16
+ config: JsonValue;
17
+ inputFiles: ManagedInputFile[];
18
+ deadline: string;
19
+ traceparent: string;
20
+ }
21
+ export interface ManagedTargetContext {
22
+ executionId: string;
23
+ attempt: number;
24
+ input: JsonValue;
25
+ config: JsonValue;
26
+ inputFiles: Array<ManagedInputFile & {
27
+ data: Uint8Array;
28
+ }>;
29
+ signal: AbortSignal;
30
+ traceId: string;
31
+ }
32
+ export interface ManagedOutputFile {
33
+ filename: string;
34
+ contentType: string;
35
+ data: Uint8Array;
36
+ primary?: boolean;
37
+ }
38
+ export interface ManagedTargetResult {
39
+ state?: "succeeded" | "error" | "cancelled";
40
+ output?: JsonValue;
41
+ /** Public, safe error summary. Never include provider errors, credentials or stacks. */
42
+ error?: {
43
+ type: string;
44
+ message?: string;
45
+ };
46
+ files?: ManagedOutputFile[];
47
+ usage?: {
48
+ inputTokens?: number;
49
+ outputTokens?: number;
50
+ };
51
+ }
52
+ export interface ManagedTargetOptions {
53
+ machineCredential: string;
54
+ baseUrl?: string;
55
+ target: (invocation: ManagedTargetContext) => Promise<ManagedTargetResult>;
56
+ /** Flush the application's existing trace AND log pipelines; do not shut them down. */
57
+ flushTelemetry: () => Promise<unknown>;
58
+ tracer?: Tracer;
59
+ /** Reserve finalization time inside the host's request limit. Default 90 seconds. */
60
+ maxExecutionMillis?: number;
61
+ /** Upload/checkpoint/flush grace after execution deadline, at most 30 seconds. */
62
+ finalizationMillis?: number;
63
+ }
64
+ /** A machine-authenticated, framework-neutral POST handler. Never retries the target. */
65
+ export declare function createManagedTargetHandler(options: ManagedTargetOptions): (request: Request) => Promise<Response>;
@@ -0,0 +1,537 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import { request as httpRequest } from "node:http";
3
+ import { request as httpsRequest } from "node:https";
4
+ import { context, ROOT_CONTEXT, SpanStatusCode, trace } from "@opentelemetry/api";
5
+ import { W3CTraceContextPropagator } from "@opentelemetry/core";
6
+ import { validateOptions } from "./config.js";
7
+ import { json, uuid } from "./evals/json.js";
8
+ const MIB = 1024 * 1024;
9
+ const MAX_FILE = 25 * MIB;
10
+ const MAX_TOTAL = 64 * MIB;
11
+ const STATES = new Set([
12
+ "queued",
13
+ "dispatched",
14
+ "running",
15
+ "uncertain",
16
+ "checkpointed",
17
+ "completed",
18
+ "unsupported",
19
+ "error",
20
+ "cancelled",
21
+ "superseded",
22
+ ]);
23
+ const propagator = new W3CTraceContextPropagator();
24
+ /** A machine-authenticated, framework-neutral POST handler. Never retries the target. */
25
+ export function createManagedTargetHandler(options) {
26
+ token(options.machineCredential);
27
+ const baseUrl = validateOptions({
28
+ apiKey: "managed-invocation",
29
+ serviceName: "managed-target",
30
+ captureContent: false,
31
+ baseUrl: options.baseUrl,
32
+ }).baseUrl;
33
+ const maxExecution = bounded(options.maxExecutionMillis ?? 90_000, 1, 90_000);
34
+ const grace = bounded(options.finalizationMillis ?? 30_000, 1, 30_000);
35
+ if (typeof options.target !== "function" || typeof options.flushTelemetry !== "function")
36
+ throw new TypeError("target and flushTelemetry are required");
37
+ const credential = createHash("sha256").update(`Bearer ${options.machineCredential}`).digest();
38
+ return async (request) => {
39
+ if (request.method !== "POST")
40
+ return reply(405, { error: "method_not_allowed" });
41
+ const supplied = request.headers.get("authorization") ?? "";
42
+ if (supplied.length > 8192 ||
43
+ !timingSafeEqual(credential, createHash("sha256").update(supplied).digest()))
44
+ return reply(401, { error: "authentication" });
45
+ const began = Date.now();
46
+ let invocation;
47
+ let invocationToken;
48
+ try {
49
+ invocationToken = token(request.headers.get("x-hue-invocation-token"));
50
+ const body = await readBytes(request, MIB, AbortSignal.timeout(5_000));
51
+ invocation = validateInvocation(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(body)));
52
+ if (Date.parse(invocation.deadline) <= Date.now())
53
+ return reply(408, { error: "deadline_exceeded" });
54
+ }
55
+ catch {
56
+ return reply(400, { error: "invalid_invocation" });
57
+ }
58
+ const executionEnd = Math.min(Date.parse(invocation.deadline), began + maxExecution);
59
+ const finalEnd = Math.min(executionEnd + grace, began + maxExecution + grace);
60
+ const signal = AbortSignal.timeout(Math.max(1, executionEnd - Date.now()));
61
+ const api = new InvocationApi(baseUrl, invocationToken, invocation.executionId);
62
+ const uncertain = () => reply(503, { protocolVersion: 1, executionId: invocation.executionId, state: "uncertain" });
63
+ let claim;
64
+ try {
65
+ claim = object(await api.json("POST", "/claim", {}, executionEnd));
66
+ if (typeof claim.claimed !== "boolean" ||
67
+ claim.executionId !== invocation.executionId ||
68
+ !STATES.has(String(claim.state)))
69
+ throw new Error();
70
+ }
71
+ catch {
72
+ return uncertain();
73
+ }
74
+ if (!claim.claimed)
75
+ return reply(409, {
76
+ protocolVersion: 1,
77
+ executionId: invocation.executionId,
78
+ state: claim.state,
79
+ });
80
+ // The application's existing provider owns this span and every child. No global setup.
81
+ const parent = propagator.extract(ROOT_CONTEXT, { traceparent: invocation.traceparent }, {
82
+ keys: (carrier) => Object.keys(carrier),
83
+ get: (carrier, key) => carrier[key],
84
+ });
85
+ const span = (options.tracer ?? trace.getTracer("@hue-run/sdk/managed")).startSpan("ai.managed_target", {
86
+ attributes: {
87
+ "hue.execution.id": invocation.executionId,
88
+ "hue.execution.attempt": invocation.attempt,
89
+ },
90
+ }, parent);
91
+ const spanContext = span.spanContext();
92
+ if (!span.isRecording() || spanContext.traceId !== invocation.traceparent.split("-")[1]) {
93
+ span.end();
94
+ return uncertain();
95
+ }
96
+ let result;
97
+ try {
98
+ const inputFiles = [];
99
+ for (const file of invocation.inputFiles) {
100
+ const data = await api.bytes(`/inputs/${file.artifactId}`, file.byteSize, executionEnd);
101
+ if (data.byteLength !== file.byteSize || sha256(data) !== file.sha256)
102
+ throw new Error("input_integrity");
103
+ inputFiles.push({ ...file, data });
104
+ }
105
+ // Synchronous input hashing or a busy event loop can exhaust the budget
106
+ // before the abort timer runs. Do not start a paid callback in that gap.
107
+ if (Date.now() >= executionEnd)
108
+ throw new Error("deadline");
109
+ signal.throwIfAborted();
110
+ result = await within(context.with(trace.setSpan(parent, span), () => options.target({
111
+ executionId: invocation.executionId,
112
+ attempt: invocation.attempt,
113
+ input: invocation.input,
114
+ config: invocation.config,
115
+ inputFiles,
116
+ signal,
117
+ traceId: spanContext.traceId,
118
+ })), signal);
119
+ validateResult(result);
120
+ }
121
+ catch {
122
+ // A timed-out callback may still be running. Do not claim a terminal outcome for it.
123
+ if (signal.aborted || Date.now() >= executionEnd) {
124
+ span.setStatus({ code: SpanStatusCode.ERROR });
125
+ span.end();
126
+ return uncertain();
127
+ }
128
+ result = {
129
+ state: "error",
130
+ error: { type: "target_error", message: "The target or input validation failed." },
131
+ };
132
+ }
133
+ const artifactIds = [];
134
+ let primaryArtifactId;
135
+ let uploadFailed = false;
136
+ for (const [index, file] of (result.files ?? []).entries()) {
137
+ try {
138
+ const hash = sha256(file.data);
139
+ const reserved = object(await api.json("POST", "/files", {
140
+ idempotencyKey: `file:${index}:${hash}`,
141
+ filename: file.filename,
142
+ contentType: file.contentType,
143
+ byteSize: file.data.byteLength,
144
+ sha256: hash,
145
+ }, finalEnd, true));
146
+ const artifactId = uuid(reserved.artifactId);
147
+ if (reserved.state !== "ready") {
148
+ const uploadUrl = safeUploadUrl(reserved.uploadUrl);
149
+ const headers = uploadHeaders(reserved.headers, file.contentType);
150
+ // Never replay a signed write. Completion independently verifies the
151
+ // stored hash, so it can settle a lost successful acknowledgement.
152
+ try {
153
+ await api.upload(uploadUrl, file.data, headers, finalEnd);
154
+ }
155
+ catch {
156
+ /* Verify below. */
157
+ }
158
+ await api.json("POST", `/files/${artifactId}/complete`, {}, finalEnd, true);
159
+ }
160
+ artifactIds.push(artifactId);
161
+ if (file.primary)
162
+ primaryArtifactId = artifactId;
163
+ }
164
+ catch {
165
+ uploadFailed = true;
166
+ }
167
+ }
168
+ // Preserve every successfully uploaded secondary even when another file failed.
169
+ if (uploadFailed)
170
+ result = {
171
+ ...result,
172
+ state: "error",
173
+ error: {
174
+ type: "artifact_upload_failed",
175
+ message: "One or more output files could not be stored.",
176
+ },
177
+ };
178
+ const state = result.state ?? "succeeded";
179
+ if (state !== "succeeded")
180
+ span.setStatus({ code: SpanStatusCode.ERROR });
181
+ span.end();
182
+ const outcome = {
183
+ protocolVersion: 1,
184
+ executionId: invocation.executionId,
185
+ state,
186
+ ...(Object.hasOwn(result, "output") ? { output: result.output } : {}),
187
+ ...(result.error ? { error: result.error } : {}),
188
+ artifactIds,
189
+ ...(primaryArtifactId ? { primaryArtifactId } : {}),
190
+ traceId: spanContext.traceId,
191
+ expectedSpanIds: [spanContext.spanId],
192
+ ...(result.usage ? { usage: result.usage } : {}),
193
+ };
194
+ try {
195
+ await api.json("POST", "/outcome", outcome, finalEnd, true);
196
+ }
197
+ catch {
198
+ return uncertain();
199
+ }
200
+ const acknowledgement = {
201
+ protocolVersion: 1,
202
+ executionId: invocation.executionId,
203
+ state: "checkpointed",
204
+ };
205
+ try {
206
+ const remaining = finalEnd - Date.now();
207
+ if (remaining <= 0)
208
+ throw new Error();
209
+ const flushed = await within(Promise.resolve().then(options.flushTelemetry), AbortSignal.timeout(remaining));
210
+ if (flushed === false)
211
+ throw new Error("flush_failed");
212
+ if (flushed &&
213
+ typeof flushed === "object" &&
214
+ ("pendingSpans" in flushed || "pendingLogs" in flushed)) {
215
+ // Hue's failure counters are cumulative; its flush throws for new failures.
216
+ // Pending counters describe the current drain and must both be zero.
217
+ const report = flushed;
218
+ if (report.pendingSpans !== 0 || report.pendingLogs !== 0)
219
+ throw new Error("flush_pending");
220
+ }
221
+ await api.json("POST", "/telemetry", { expectedSpanIds: [spanContext.spanId], flushed: true }, finalEnd, true);
222
+ }
223
+ catch {
224
+ return reply(200, { ...acknowledgement, telemetry: "pending" });
225
+ }
226
+ return reply(200, { ...acknowledgement, telemetry: "flushed" });
227
+ };
228
+ }
229
+ class InvocationApi {
230
+ baseUrl;
231
+ invocationToken;
232
+ executionId;
233
+ constructor(baseUrl, invocationToken, executionId) {
234
+ this.baseUrl = baseUrl;
235
+ this.invocationToken = invocationToken;
236
+ this.executionId = executionId;
237
+ }
238
+ url(path) {
239
+ return `${this.baseUrl}/api/v1/managed-executions/${this.executionId}${path}`;
240
+ }
241
+ async json(method, path, value, deadline, retry = false) {
242
+ const body = JSON.stringify(json(value, MIB));
243
+ const bytes = await this.request(this.url(path), {
244
+ method,
245
+ headers: {
246
+ authorization: `Bearer ${this.invocationToken}`,
247
+ "content-type": "application/json",
248
+ },
249
+ body,
250
+ }, MIB, deadline, retry);
251
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
252
+ }
253
+ bytes(path, maximum, deadline) {
254
+ return this.request(this.url(path), { headers: { authorization: `Bearer ${this.invocationToken}` } }, maximum, deadline, false);
255
+ }
256
+ async upload(url, data, headers, deadline) {
257
+ const remaining = deadline - Date.now();
258
+ if (remaining <= 0)
259
+ throw new Error("deadline");
260
+ const signal = AbortSignal.timeout(remaining);
261
+ await new Promise((resolve, reject) => {
262
+ let activeResponse;
263
+ const finish = (error) => {
264
+ signal.removeEventListener("abort", abort);
265
+ if (error)
266
+ reject(error);
267
+ else
268
+ resolve();
269
+ };
270
+ const abort = () => {
271
+ activeResponse?.destroy();
272
+ request.destroy();
273
+ finish(new Error("Hue managed upload failed"));
274
+ };
275
+ // Signed writes must not be replayed by fetch, redirect handling or a
276
+ // reused-socket retry. A lost response is settled by Hue's verified read.
277
+ const request = (new URL(url).protocol === "https:" ? httpsRequest : httpRequest)(url, {
278
+ method: "PUT",
279
+ agent: false,
280
+ signal,
281
+ maxHeaderSize: 16 * 1024,
282
+ headers: { ...headers, "content-length": String(data.byteLength) },
283
+ }, (response) => {
284
+ activeResponse = response;
285
+ if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) {
286
+ response.destroy();
287
+ finish(new Error("Hue managed upload failed"));
288
+ return;
289
+ }
290
+ void (async () => {
291
+ let length = 0;
292
+ for await (const chunk of response) {
293
+ signal.throwIfAborted();
294
+ length += Buffer.byteLength(chunk);
295
+ if (length > MIB)
296
+ throw new Error("Upload response too large");
297
+ }
298
+ signal.throwIfAborted();
299
+ finish();
300
+ })().catch(() => {
301
+ response.destroy();
302
+ finish(new Error("Hue managed upload failed"));
303
+ });
304
+ });
305
+ request.once("error", () => finish(new Error("Hue managed upload failed")));
306
+ signal.addEventListener("abort", abort, { once: true });
307
+ request.end(Buffer.from(data));
308
+ });
309
+ }
310
+ async request(url, init, maximum, deadline, retry) {
311
+ for (let attempt = 0;; attempt++) {
312
+ const remaining = deadline - Date.now();
313
+ if (remaining <= 0)
314
+ throw new Error("deadline");
315
+ try {
316
+ const signal = AbortSignal.timeout(remaining);
317
+ const response = await fetch(url, { ...init, redirect: "error", signal });
318
+ if (!response.ok) {
319
+ await response.body?.cancel();
320
+ if (![429, 500, 502, 503, 504].includes(response.status))
321
+ throw new PermanentError();
322
+ throw new Error();
323
+ }
324
+ return await readBytes(response, maximum, signal);
325
+ }
326
+ catch (error) {
327
+ if (!retry ||
328
+ attempt >= 1 ||
329
+ error instanceof PermanentError ||
330
+ Date.now() + 100 >= deadline)
331
+ throw new Error("Hue managed request failed");
332
+ await new Promise((resolve) => setTimeout(resolve, 100));
333
+ }
334
+ }
335
+ }
336
+ }
337
+ class PermanentError extends Error {
338
+ }
339
+ function reply(status, body) {
340
+ return Response.json(body, { status, headers: { "cache-control": "no-store" } });
341
+ }
342
+ function sha256(data) {
343
+ return createHash("sha256").update(data).digest("hex");
344
+ }
345
+ function token(value) {
346
+ if (typeof value !== "string" || !value || value.length > 8192 || /[\s\u0000]/u.test(value))
347
+ throw new TypeError("Invalid credential");
348
+ return value;
349
+ }
350
+ function bounded(value, minimum, maximum) {
351
+ if (typeof value !== "number" ||
352
+ !Number.isSafeInteger(value) ||
353
+ value < minimum ||
354
+ value > maximum)
355
+ throw new TypeError("Invalid integer");
356
+ return value;
357
+ }
358
+ function object(value) {
359
+ if (!value || typeof value !== "object" || Array.isArray(value))
360
+ throw new TypeError("Expected object");
361
+ return value;
362
+ }
363
+ function filename(value) {
364
+ if (typeof value !== "string" ||
365
+ !value.trim() ||
366
+ value.length > 255 ||
367
+ /[\x00-\x1f\x7f/\\]/u.test(value) ||
368
+ value === "." ||
369
+ value === "..")
370
+ throw new TypeError("Invalid filename");
371
+ return value;
372
+ }
373
+ function shortString(value, maximum = 255) {
374
+ if (typeof value !== "string" ||
375
+ !value ||
376
+ value.length > maximum ||
377
+ /[\x00-\x1f\x7f]/u.test(value))
378
+ throw new TypeError("Invalid string");
379
+ return value;
380
+ }
381
+ function validateInvocation(value) {
382
+ const v = object(json(value, MIB));
383
+ if (Object.keys(v).some((key) => ![
384
+ "protocolVersion",
385
+ "executionId",
386
+ "attempt",
387
+ "input",
388
+ "config",
389
+ "inputFiles",
390
+ "deadline",
391
+ "traceparent",
392
+ ].includes(key)))
393
+ throw new TypeError("Unknown invocation field");
394
+ if (v.protocolVersion !== 1 || !Object.hasOwn(v, "input") || !Object.hasOwn(v, "config"))
395
+ throw new TypeError("Invalid protocol");
396
+ uuid(v.executionId);
397
+ bounded(v.attempt, 1, Number.MAX_SAFE_INTEGER);
398
+ if (typeof v.deadline !== "string" ||
399
+ v.deadline.trim() !== v.deadline ||
400
+ !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,3})?Z$/.test(v.deadline) ||
401
+ !Number.isFinite(Date.parse(v.deadline)))
402
+ throw new TypeError("Invalid deadline");
403
+ if (typeof v.traceparent !== "string" ||
404
+ v.traceparent.length !== 55 ||
405
+ !/^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/.test(v.traceparent) ||
406
+ /^00-0{32}-/.test(v.traceparent) ||
407
+ /-0{16}-01$/.test(v.traceparent))
408
+ throw new TypeError("A sampled W3C traceparent is required");
409
+ if (!Array.isArray(v.inputFiles) || v.inputFiles.length > 16)
410
+ throw new TypeError("Invalid files");
411
+ let total = 0;
412
+ const ids = new Set();
413
+ for (const raw of v.inputFiles) {
414
+ const file = object(raw);
415
+ const id = uuid(file.artifactId);
416
+ if (Object.keys(file).some((key) => !["artifactId", "filename", "contentType", "byteSize", "sha256", "role"].includes(key)))
417
+ throw new TypeError("Unknown file field");
418
+ if (ids.has(id))
419
+ throw new TypeError("Duplicate input file");
420
+ ids.add(id);
421
+ filename(file.filename);
422
+ shortString(file.contentType);
423
+ shortString(file.role, 64);
424
+ total += bounded(file.byteSize, 0, MAX_FILE);
425
+ if (typeof file.sha256 !== "string" ||
426
+ file.sha256.length !== 64 ||
427
+ !/^[0-9a-f]{64}$/.test(file.sha256))
428
+ throw new TypeError("Invalid hash");
429
+ }
430
+ if (total > MAX_TOTAL)
431
+ throw new TypeError("Input files too large");
432
+ return v;
433
+ }
434
+ function validateResult(result) {
435
+ object(result);
436
+ if (result.state && !["succeeded", "error", "cancelled"].includes(result.state))
437
+ throw new TypeError("Invalid outcome");
438
+ if (Object.hasOwn(result, "output"))
439
+ json(result.output);
440
+ if (result.error) {
441
+ shortString(result.error.type, 64);
442
+ if (!/^[a-z][a-z0-9_]{0,63}$/.test(result.error.type))
443
+ throw new TypeError("Invalid error type");
444
+ if (result.error.message !== undefined)
445
+ shortString(result.error.message, 1000);
446
+ }
447
+ if (result.usage)
448
+ for (const count of Object.values(result.usage))
449
+ bounded(count, 0, Number.MAX_SAFE_INTEGER);
450
+ if (result.files !== undefined && (!Array.isArray(result.files) || result.files.length > 16))
451
+ throw new TypeError("Invalid files");
452
+ let total = 0;
453
+ let primary = 0;
454
+ for (const file of result.files ?? []) {
455
+ filename(file.filename);
456
+ shortString(file.contentType);
457
+ if (!(file.data instanceof Uint8Array))
458
+ throw new TypeError("File data must be bytes");
459
+ total += bounded(file.data.byteLength, 0, MAX_FILE);
460
+ if (file.primary !== undefined && typeof file.primary !== "boolean")
461
+ throw new TypeError("Invalid primary");
462
+ if (file.primary)
463
+ primary++;
464
+ }
465
+ if (total > MAX_TOTAL || primary > 1)
466
+ throw new TypeError("Invalid output files");
467
+ }
468
+ function safeUploadUrl(value) {
469
+ if (typeof value !== "string" || value.length > 8192 || /[\x00-\x20\x7f]/u.test(value))
470
+ throw new TypeError("Invalid upload URL");
471
+ const url = new URL(value);
472
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
473
+ if ((url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) ||
474
+ url.username ||
475
+ url.password ||
476
+ url.hash)
477
+ throw new TypeError("Invalid upload URL");
478
+ // Validate without rewriting the provider's signed capability.
479
+ return value;
480
+ }
481
+ function uploadHeaders(value, contentType) {
482
+ const headers = { "content-type": contentType };
483
+ if (value === undefined || value === null)
484
+ return headers;
485
+ for (const [name, raw] of Object.entries(object(value))) {
486
+ const lower = name.toLowerCase();
487
+ const value = shortString(raw);
488
+ if (lower === "content-type" && value === contentType)
489
+ headers[lower] = value;
490
+ else if (lower === "x-vercel-blob-access" && value === "private")
491
+ headers[lower] = value;
492
+ else
493
+ throw new TypeError("Unsupported upload header");
494
+ }
495
+ return headers;
496
+ }
497
+ async function within(promise, signal) {
498
+ signal.throwIfAborted();
499
+ let abort;
500
+ try {
501
+ return await Promise.race([
502
+ promise,
503
+ new Promise((_, reject) => {
504
+ abort = () => reject(new Error("Deadline exceeded"));
505
+ signal.addEventListener("abort", abort, { once: true });
506
+ }),
507
+ ]);
508
+ }
509
+ finally {
510
+ signal.removeEventListener("abort", abort);
511
+ }
512
+ }
513
+ async function readBytes(source, maximum, signal) {
514
+ const declared = source.headers.get("content-length");
515
+ if (declared !== null && (!/^\d+$/.test(declared) || Number(declared) > maximum))
516
+ throw new Error("Oversized body");
517
+ if (!source.body)
518
+ return new Uint8Array();
519
+ const reader = source.body.getReader();
520
+ const chunks = [];
521
+ let size = 0;
522
+ try {
523
+ while (true) {
524
+ const next = await within(reader.read(), signal);
525
+ if (next.done)
526
+ break;
527
+ size += next.value.byteLength;
528
+ if (size > maximum)
529
+ throw new Error("Oversized body");
530
+ chunks.push(next.value);
531
+ }
532
+ return Buffer.concat(chunks, size);
533
+ }
534
+ finally {
535
+ void reader.cancel().catch(() => { });
536
+ }
537
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hue-run/sdk",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -19,6 +19,7 @@
19
19
  "dist",
20
20
  "README.md",
21
21
  "EVALUATIONS.md",
22
+ "MANAGED_TARGETS.md",
22
23
  "LICENSE"
23
24
  ],
24
25
  "type": "module",
@@ -34,6 +35,10 @@
34
35
  "./evals": {
35
36
  "types": "./dist/evals.d.ts",
36
37
  "import": "./dist/evals.js"
38
+ },
39
+ "./managed": {
40
+ "types": "./dist/managed.d.ts",
41
+ "import": "./dist/managed.js"
37
42
  }
38
43
  },
39
44
  "scripts": {