@hue-run/sdk 0.18.0 → 0.18.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/CLI.md +130 -8
- package/EVALUATIONS.md +7 -4
- package/README.md +86 -22
- package/dist/attachment.d.ts +51 -7
- package/dist/attachment.js +141 -33
- package/dist/blobs.d.ts +22 -5
- package/dist/blobs.js +187 -72
- package/dist/cli/import.d.ts +35 -0
- package/dist/cli/import.js +692 -0
- package/dist/cli/langfuse.d.ts +309 -0
- package/dist/cli/langfuse.js +1075 -0
- package/dist/client.d.ts +9 -4
- package/dist/client.js +46 -15
- package/dist/evals/client.d.ts +7 -2
- package/dist/evals/client.js +20 -4
- package/dist/evals/files.js +100 -12
- package/dist/evals/media.d.ts +5 -1
- package/dist/evals/media.js +15 -3
- package/dist/evals/types.d.ts +4 -3
- package/dist/inline-files.d.ts +16 -6
- package/dist/inline-files.js +18 -14
- package/dist/json-edits.js +26 -7
- package/dist/large-media.d.ts +106 -0
- package/dist/large-media.js +414 -0
- package/dist/setup/cli.js +7 -3
- package/dist/snapshot.d.ts +2 -0
- package/dist/snapshot.js +19 -1
- package/dist/transport.d.ts +4 -0
- package/dist/transport.js +36 -16
- package/dist/types.d.ts +10 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CLI.md
CHANGED
|
@@ -42,7 +42,7 @@ project manifest are refused because managers can update ancestor locks; Python
|
|
|
42
42
|
The generated `hue.setup.mjs` or `hue_setup.py` always selects `captureContent: false` /
|
|
43
43
|
`capture_content=False`. For a supported application, setup installs the dependency and adds the
|
|
44
44
|
managed import and middleware registration to the existing entrypoint; an unreferenced helper is
|
|
45
|
-
not a completed integration. TypeScript uses `@hue-run/sdk@0.18.
|
|
45
|
+
not a completed integration. TypeScript uses `@hue-run/sdk@0.18.2`, `@opentelemetry/api@1.9.1` and
|
|
46
46
|
`@opentelemetry/context-async-hooks@2.11.0`; Python setup uses its separately tested package pin.
|
|
47
47
|
Content capture requires an ordinary account-managed key and a later explicit application decision.
|
|
48
48
|
|
|
@@ -114,6 +114,7 @@ hue setup --origin http://127.0.0.1:PORT # isolated loopback tests only
|
|
|
114
114
|
hue login # validate keys created in Hue and store them in ./.env.hue
|
|
115
115
|
hue mcp install --client claude-code # write the Hue MCP configuration for a coding agent
|
|
116
116
|
hue listen --subscription <id> --forward-to http://localhost:3000/slack/events # deliver events to a local bot
|
|
117
|
+
hue import langfuse --from 2026-01-01 --dry-run # count a Langfuse project's traces, then import them
|
|
117
118
|
```
|
|
118
119
|
|
|
119
120
|
Hosted setup accepts HTTPS origins only. HTTP is accepted solely for `localhost`, `127.0.0.1` and
|
|
@@ -365,6 +366,127 @@ codes: `0` stopped, `1` Hue refused the credential, the subscription (unknown, r
|
|
|
365
366
|
delivers to a request URL) or the pull itself (a redirect, another refusal or an answer without
|
|
366
367
|
deliveries), `2` usage error, `130` interrupted twice.
|
|
367
368
|
|
|
369
|
+
## Import traces from Langfuse
|
|
370
|
+
|
|
371
|
+
`hue import langfuse` copies a Langfuse project's traces into Hue, for a team moving from Langfuse.
|
|
372
|
+
It runs on your machine: it reads Langfuse's public API with the project's own keys and sends the
|
|
373
|
+
spans with your Hue project key through the SDK's exporter, so the Langfuse keys go only to Langfuse
|
|
374
|
+
and Hue never receives or stores them. The command is part of the `hue` executable and runs with
|
|
375
|
+
`npx` whatever language the application is written in; the Python SDK has no counterpart.
|
|
376
|
+
|
|
377
|
+
```sh
|
|
378
|
+
# .env.langfuse, kept out of version control:
|
|
379
|
+
# LANGFUSE_PUBLIC_KEY=pk-lf-...
|
|
380
|
+
# LANGFUSE_SECRET_KEY=sk-lf-...
|
|
381
|
+
# LANGFUSE_BASE_URL=https://cloud.langfuse.com
|
|
382
|
+
npx -y @hue-run/sdk@0.18.2 import langfuse --env-path .env.langfuse --from 2026-01-01 --dry-run
|
|
383
|
+
npx -y @hue-run/sdk@0.18.2 import langfuse --env-path .env.langfuse --from 2026-01-01 --media
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
`LANGFUSE_PUBLIC_KEY` and `LANGFUSE_SECRET_KEY` come from the environment or `--env-path`
|
|
387
|
+
(`--env-file` also works when the file exists), loaded first; a variable already set keeps its
|
|
388
|
+
value. `--langfuse-public-key` replaces the public key on the command line, but the secret key is
|
|
389
|
+
never taken from it, where other local processes and shell history can read it. The host is
|
|
390
|
+
`--langfuse-host`, then `LANGFUSE_BASE_URL`, then `LANGFUSE_HOST`, then `https://cloud.langfuse.com`:
|
|
391
|
+
use your region's host or your self-hosted URL, over HTTPS (plain HTTP only on this machine). A
|
|
392
|
+
request carrying the keys follows no redirect. `HUE_API_KEY` is the Hue project key the spans are
|
|
393
|
+
sent with, from the environment, the env file, or `./.env.hue` (where `hue login` stores it) when
|
|
394
|
+
neither sets it (a dry run, which needs no Hue key, reads no `./.env.hue` beside a named file); `--origin` (default `HUE_BASE_URL`, then `https://app.hue.run`) is checked with
|
|
395
|
+
`GET /api/v1/projects/current` before Langfuse is read. Keys are never printed.
|
|
396
|
+
|
|
397
|
+
**What is read.** The command first asks for `GET /api/public/v2/observations`, which Langfuse Cloud
|
|
398
|
+
and self-hosted Langfuse v4 answer. The traces are then the root observations
|
|
399
|
+
(`isRootObservation=true`) that started in the window, listed newest first in cursor pages, and each
|
|
400
|
+
trace's observations are read 100 a page with the field groups
|
|
401
|
+
`core,basic,time,io,metadata,model,usage,prompt,trace_context`; a page whose metadata values Langfuse
|
|
402
|
+
cut at 200 characters is read again with those keys expanded. A deployment that answers `404` there
|
|
403
|
+
(self-hosted Langfuse v3) is read through `GET /api/public/traces` (`fromTimestamp`, `toTimestamp`,
|
|
404
|
+
`orderBy=timestamp.desc`, `fields=core,io`) and `GET /api/public/observations?traceId=…`, in numbered
|
|
405
|
+
pages. An answer over 32 MiB, or a `413`, is asked for again in smaller pages.
|
|
406
|
+
|
|
407
|
+
**Selection.** `--from` and `--to` take an ISO 8601 date (midnight UTC) or a date and time with a time
|
|
408
|
+
zone. A trace is selected by when it started, its root observation's start time or a v1 trace's
|
|
409
|
+
timestamp, `--from` inclusive and `--to` exclusive. Without `--to` the window ends an hour before the
|
|
410
|
+
command starts, so a trace still running, or still arriving in Langfuse, is left for a later run
|
|
411
|
+
rather than sent half recorded. `--limit N` imports the N
|
|
412
|
+
newest traces; `--trace-id ID` (repeatable, up to 1,000) imports those traces only and is not combined
|
|
413
|
+
with the window or the limit.
|
|
414
|
+
|
|
415
|
+
**What Hue receives.** Each observation becomes a completed span as a Langfuse OpenTelemetry SDK
|
|
416
|
+
records it: its name, start, end (an observation without an end lasts no time) and parent, with
|
|
417
|
+
`langfuse.observation.type` (lower case), `langfuse.observation.input` and `.output` (a string as
|
|
418
|
+
recorded, any other value as its JSON), one `langfuse.observation.metadata.<key>` per metadata key,
|
|
419
|
+
`.level`, `.status_message`, `.model.name`, `.model.parameters`, `.usage_details`, `.cost_details`,
|
|
420
|
+
`.completion_start_time`, `.prompt.name` and `.prompt.version`, and the trace's
|
|
421
|
+
`langfuse.trace.name`, `langfuse.trace.tags`, `langfuse.user.id`, `langfuse.session.id`,
|
|
422
|
+
`langfuse.release`, `langfuse.version` and `langfuse.environment`. The model is also
|
|
423
|
+
`gen_ai.request.model`, and Langfuse's `input` and `output` usage counts are
|
|
424
|
+
`gen_ai.usage.input_tokens` and `gen_ai.usage.output_tokens`. An observation at level `ERROR` is an
|
|
425
|
+
error span with its status message. A v1 trace is also a span of its own, named as the trace and
|
|
426
|
+
carrying `langfuse.trace.input`, `.output`, `.metadata.<key>`, `.tags` and `.public`; its observations
|
|
427
|
+
without a parent are its children, and it lasts from the earliest of its timestamp and their starts
|
|
428
|
+
to their last end. Spans carry `service.name` `langfuse` (`--service-name` sets another) and the
|
|
429
|
+
instrumentation scope `@hue-run/sdk/import/langfuse`.
|
|
430
|
+
|
|
431
|
+
**Ids and resuming.** A Langfuse id that already is an OpenTelemetry id (32 lowercase hex digits for
|
|
432
|
+
a trace, 16 for an observation, as Langfuse's OpenTelemetry SDKs write them) is kept; any other id,
|
|
433
|
+
such as an older SDK's UUID, becomes one derived from it with SHA-256. The original ids are kept as
|
|
434
|
+
`langfuse.trace.id` and `langfuse.observation.id`. Running the same command again sends the same
|
|
435
|
+
spans, and Hue keeps one copy of an exact replay of a completed span, so an interrupted or partly
|
|
436
|
+
failed import is resumed by running it again. Hue refuses a different payload for a span it already
|
|
437
|
+
holds: keep `--media`, `--no-content` and `--service-name` the same when resuming, and expect a span
|
|
438
|
+
whose Langfuse data changed after it was imported to be refused. For the same reason the import
|
|
439
|
+
sends nothing that depends on the run: a trace's root never carries `hue.sdk.dropped_records`, and
|
|
440
|
+
files and large values are stored before their span is sent. Leave out of the window any traces that
|
|
441
|
+
already reached Hue through OpenTelemetry.
|
|
442
|
+
|
|
443
|
+
**Export and content.** Spans go through the SDK's own exporter, as `createHue` sends them: batched,
|
|
444
|
+
compressed, retried and within the limits Hue advertises. Before a span is sent, its content is
|
|
445
|
+
stored as that exporter stores it, with the exporter's own placement: a file inline in an attribute
|
|
446
|
+
Hue reads files from (a `data:` URL, or the bytes of a provider part, that Langfuse kept in place) is
|
|
447
|
+
uploaded and placed in its digest form, and a text value still over Hue's inline limit (1 MiB) is
|
|
448
|
+
uploaded whole and replaced by its first 16 KiB, each listed in `hue.blobs` (a whole value also in
|
|
449
|
+
`hue.truncated`). Doing this first means a file or value that cannot be stored right now holds its
|
|
450
|
+
span back for the next run instead of being kept inline or cut; one over a limit (Hue's size limit,
|
|
451
|
+
or a span's 64 entries) is placed the same way on every run and counted in the summary. Content is
|
|
452
|
+
captured, the SDK's default; `--no-content` sends metadata only, as `captureContent: false` does (no
|
|
453
|
+
inputs, outputs, status messages or model parameters; metadata, model names and usage stay, and
|
|
454
|
+
nothing is uploaded). The import waits for its queue to be sent every 1,024 spans or 32 MiB, so its
|
|
455
|
+
memory stays bounded whatever the project's size.
|
|
456
|
+
|
|
457
|
+
**Media.** Langfuse's SDKs replace a file in an input, output or metadata value with a reference,
|
|
458
|
+
`@@@langfuseMedia:type=<media type>|id=<id>|source=<source>@@@`. Without `--media`, references are
|
|
459
|
+
sent as recorded, and Hue shows them as files held in Langfuse. With `--media`, each reference where
|
|
460
|
+
Hue reads a file (a whole string, or the content of a provider part such as an OpenAI `image_url` or
|
|
461
|
+
`input_audio`, an Anthropic `source` or a GenAI `blob`, no deeper than ten levels) is looked up with
|
|
462
|
+
`GET /api/public/media/{id}`. The file is downloaded from the link Langfuse answers (following its
|
|
463
|
+
store's redirects, without the Langfuse keys), uploaded under the span's trace through Hue's upload route (reservation, presigned
|
|
464
|
+
`PUT`, completion), and placed in the digest form the SDK writes for media it records: a whole string
|
|
465
|
+
becomes `{"type":"blob","mime_type":…,"modality":…,"sha256":…,"size":…}` and a provider part loses its
|
|
466
|
+
bytes and gains `sha256` and `size`, every other byte of the value's text kept, each file listed in
|
|
467
|
+
`hue.blobs` under `<attribute>#<JSON pointer>`. Each file is downloaded once per trace. A reference
|
|
468
|
+
stays as recorded when Langfuse holds no bytes for it, its file is over 25 MiB, it sits inside longer
|
|
469
|
+
text, or its span already lists 64 files. A file that cannot be downloaded or stored right now holds
|
|
470
|
+
its span back: the trace is reported, and the next run sends it.
|
|
471
|
+
|
|
472
|
+
**Rate limits and failures.** A `429` from Langfuse is waited out for as long as its `Retry-After`
|
|
473
|
+
asks, up to 15 minutes and ten times in a row; connection failures, timeouts and `5xx` answers are
|
|
474
|
+
retried after 1, 2, 4, 8 and 16 seconds. Langfuse Cloud limits each organization's requests per
|
|
475
|
+
minute by plan, so a large project imports at that pace: a request per page of traces and per page
|
|
476
|
+
of each trace's observations, and one per media file. A refusal of the Langfuse keys (`401` or `403`)
|
|
477
|
+
or of `HUE_API_KEY` stops the import before anything is sent; a trace Langfuse cannot answer for is
|
|
478
|
+
reported and the import goes on.
|
|
479
|
+
|
|
480
|
+
**Output and exit codes.** `--dry-run` reads and counts traces, spans and media references, needs no
|
|
481
|
+
Hue key and sends nothing. Without `--yes` the command asks for confirmation in a terminal (declining
|
|
482
|
+
exits `1`) and refuses to start without one. Progress is printed every 100 traces or 10 seconds, then
|
|
483
|
+
a summary of the traces imported, the spans sent and accepted, and the media files uploaded and left
|
|
484
|
+
in Langfuse; each trace not fully imported is named with the reason. Ctrl+C or `SIGTERM` stops
|
|
485
|
+
reading, sends what was read and exits `130`; a second Ctrl+C a second or more later exits `130` at
|
|
486
|
+
once, abandoning exports still in flight for the next run to send. Exit codes: `0` every selected
|
|
487
|
+
trace was imported, `1` a trace, span or file was not (or a key was refused), `2` usage error, `130`
|
|
488
|
+
stopped.
|
|
489
|
+
|
|
368
490
|
## Local state and conflicts
|
|
369
491
|
|
|
370
492
|
Before its first network write, setup creates a lowercase UUIDv4 and a 32-byte random installation
|
|
@@ -469,7 +591,7 @@ node packages/sdk-typescript/scripts/verify-package.mjs --artifacts-dir .artifac
|
|
|
469
591
|
# Set project to an existing supported fixture; use the same directory on resume.
|
|
470
592
|
project=/absolute/path/to/supported-fixture
|
|
471
593
|
node packages/sdk-typescript/scripts/verify-setup-live.mjs \
|
|
472
|
-
--archive .artifacts/typescript/hue-run-sdk-0.18.
|
|
594
|
+
--archive .artifacts/typescript/hue-run-sdk-0.18.2.tgz \
|
|
473
595
|
--origin https://STAGING_ORIGIN \
|
|
474
596
|
--project "$project" --command setup \
|
|
475
597
|
--evidence .context/setup-staging-before-claim.json
|
|
@@ -480,7 +602,7 @@ the private local handoff and finish the real browser claim, then reconcile the
|
|
|
480
602
|
|
|
481
603
|
```sh
|
|
482
604
|
node packages/sdk-typescript/scripts/verify-setup-live.mjs \
|
|
483
|
-
--archive .artifacts/typescript/hue-run-sdk-0.18.
|
|
605
|
+
--archive .artifacts/typescript/hue-run-sdk-0.18.2.tgz \
|
|
484
606
|
--origin https://STAGING_ORIGIN \
|
|
485
607
|
--project "$project" --command claim \
|
|
486
608
|
--evidence .context/setup-staging-after-claim.json
|
|
@@ -522,10 +644,10 @@ otherwise. Never hand the agent Hue-native tools (`context.tools`, `context.mcp`
|
|
|
522
644
|
production agent lacks. Pin the SDK, so the agent's repository gains no dependency:
|
|
523
645
|
|
|
524
646
|
```sh
|
|
525
|
-
npx -y @hue-run/sdk@0.18.
|
|
526
|
-
npx -y @hue-run/sdk@0.18.
|
|
527
|
-
npx -y @hue-run/sdk@0.18.
|
|
528
|
-
npx -y @hue-run/sdk@0.18.
|
|
647
|
+
npx -y @hue-run/sdk@0.18.2 eval --case "Refund an eligible charge" --command "node agent.js"
|
|
648
|
+
npx -y @hue-run/sdk@0.18.2 eval --case https://app.hue.run/projects/demo/scenarios/<id> --command "python agent.py" --timeout 120 --baseline <run id>
|
|
649
|
+
npx -y @hue-run/sdk@0.18.2 eval --set "Billing regressions" --scorer-version <id> --command "node agent.js" --save-version
|
|
650
|
+
npx -y @hue-run/sdk@0.18.2 eval --worker --command "node agent.js" --agent-key support-agent --revision <new-agent-revision>
|
|
529
651
|
```
|
|
530
652
|
|
|
531
653
|
One invocation creates one run holding every selected case; it prints the run URL
|
|
@@ -563,7 +685,7 @@ export default function runMyAgent(inputs: JsonValue, context: SimulationTargetC
|
|
|
563
685
|
```
|
|
564
686
|
|
|
565
687
|
```sh
|
|
566
|
-
npx -y @hue-run/sdk@0.18.
|
|
688
|
+
npx -y @hue-run/sdk@0.18.2 eval --case "Refund an eligible charge" ./hue-agent.ts --content
|
|
567
689
|
```
|
|
568
690
|
|
|
569
691
|
`--scenario` remains an alias for `--case` for existing scripts. Pass one selection flag.
|
package/EVALUATIONS.md
CHANGED
|
@@ -317,10 +317,13 @@ An uploaded file needs one of `outputContentTypes`, a well-formed name of 1–25
|
|
|
317
317
|
(as JavaScript counts a string's `length`) without path separators or control characters, and 1
|
|
318
318
|
byte to 25 MiB. Every file, and the case itself as it will be sent (its revision, key, values and
|
|
319
319
|
size), is checked before the first upload; a file outside that policy, a `path` that cannot be
|
|
320
|
-
read, or a case Hue would refuse throws `TypeError` or `RangeError` without a request.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
320
|
+
read, or a case Hue would refuse throws `TypeError` or `RangeError` without a request. A `path`
|
|
321
|
+
is hashed a slice at a time when it is checked and read again when it is uploaded, one file at a
|
|
322
|
+
time, so the write holds at most one file in memory and sends only the bytes its check hashed. A
|
|
323
|
+
file that changed in between, or can no longer be read, sends nothing: the write throws `TypeError`
|
|
324
|
+
and the case is not written. The reservation key names the version, the case's
|
|
325
|
+
`externalKey`, the role, the bytes, the name and the content type, so sending the same case again
|
|
326
|
+
reuses its artifacts and a corrected name or type reserves a new one; a reference to the artifact a file uploads as pins it once. `source`, `attached_template`, `attached_reference` and `original` files reach
|
|
324
327
|
the target as `context.files`; `org_template` and `evaluator_reference` files reach only scorers.
|
|
325
328
|
|
|
326
329
|
Recorded messages copied into a case's inputs keep their large files as digest parts. Pin each file
|
package/README.md
CHANGED
|
@@ -274,7 +274,8 @@ Each uploaded file is listed in the record's `hue.blobs`, keyed `<attribute>#<JS
|
|
|
274
274
|
through the string, which no JSON pointer can address, so Hue matches it by its digest.
|
|
275
275
|
- Text media over 64 KiB of decoded bytes in a provider part keeps a UTF-8-safe 16 KiB prefix in
|
|
276
276
|
its content field instead, and its key names that field
|
|
277
|
-
(`gen_ai.input.messages#/0/parts/1/content`). Smaller text and empty fields stay inline.
|
|
277
|
+
(`gen_ai.input.messages#/0/parts/1/content`). Smaller text and empty fields stay inline. A text
|
|
278
|
+
file a GenAI `blob`, Vertex `media` or AI SDK part holds as bare base64 takes the digest form.
|
|
278
279
|
|
|
279
280
|
Only the file's own value, or the members of the part that held it, change: every other byte of the
|
|
280
281
|
recorded text, its key order, number spellings, escapes and spacing included, is kept, in an encoded
|
|
@@ -286,9 +287,11 @@ Entries use the recorded media type and, when valid, the filename, name or title
|
|
|
286
287
|
is nonblank after trimming, at most 256 UTF-16 code units and well-formed Unicode, with no C0
|
|
287
288
|
characters (U+0000–U+001F) or DEL (U+007F). Invalid names are omitted from the entry without
|
|
288
289
|
rewriting the original field. Data URLs and fields explicitly encoded as base64 by the provider
|
|
289
|
-
contract are decoded before hashing. Other
|
|
290
|
-
type
|
|
291
|
-
`
|
|
290
|
+
contract are decoded before hashing. Other content is read as Hue's reader reads it, whatever its
|
|
291
|
+
media type: content that reads as base64 (a length divisible by four, base64 characters only, as
|
|
292
|
+
the GenAI conventions and `client.media.resolve` write a `blob` or `media` part's content) is the
|
|
293
|
+
file's base64 and is decoded, so `YWJj` hashes as `abc`; anything else is the file's own UTF-8
|
|
294
|
+
text. The digest matches `hue.recordFile`'s digest for the same bytes.
|
|
292
295
|
|
|
293
296
|
A file that is not uploaded (the upload failed, the receiver has no upload route, or a budget ran
|
|
294
297
|
out) keeps its digest without an entry in the recorded messages (`gen_ai.input.messages`,
|
|
@@ -309,13 +312,23 @@ listed under `hue.truncated`. While uploads are available, `recordMessages` reco
|
|
|
309
312
|
the value limit when it fits the limit with its files as their digest.
|
|
310
313
|
|
|
311
314
|
Inspection is bounded to 8 Mi UTF-16 code units of text per attribute and 16 Mi per record, a log
|
|
312
|
-
body's file contents included
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
315
|
+
body's file contents included. Within those bounds, inspection parses the value and decodes each
|
|
316
|
+
file while the record is queued and again when it is exported. A span's own attribute past either
|
|
317
|
+
bound that is held whole for upload (it is over the value limit) has its media read in place when
|
|
318
|
+
it is exported, as a voice agent's audio usually needs: each string in it longer than 16 Ki code
|
|
319
|
+
units and written without escapes as strict base64 or a base64 `data:` URL is decoded, hashed and
|
|
320
|
+
uploaded a 1 MiB window at a time from the recorded text, never parsed or copied out of it, and a
|
|
321
|
+
file may be up to Hue's 1 GB upload limit. The scan that finds those strings also reads the value
|
|
322
|
+
a window at a time, between turns of the event loop, so no step of it stalls the application for
|
|
323
|
+
long; a percent-escaped `data:` URL is parsed with the JSON around it. The value is placed exactly as one within the bounds: the same
|
|
324
|
+
digest form, entries and fallbacks. The JSON around those strings, a file written with escapes or
|
|
325
|
+
inside a JSON-encoded string included, is parsed within the 8 Mi bound; a value past it uses the
|
|
326
|
+
ordinary value limit (it uploads whole). Each such value is read within that bound on its own, apart
|
|
327
|
+
from the record's 16 Mi allowance, while it stays held within the 128 MiB for values awaiting
|
|
328
|
+
upload. Log records keep the bounds. Resources, links and span
|
|
329
|
+
events never upload inline media: in their recorded messages, media up to 64 KiB stays verbatim
|
|
330
|
+
and larger media is replaced by its digest, as in 0.16, and log records that cannot upload (not
|
|
331
|
+
correlated with a span, or while uploads are unavailable) keep the same rule. With
|
|
319
332
|
`captureContent: false`, nothing is inspected or uploaded.
|
|
320
333
|
|
|
321
334
|
### Attach a file anywhere
|
|
@@ -342,10 +355,19 @@ await hue.withSpan(
|
|
|
342
355
|
```
|
|
343
356
|
|
|
344
357
|
Give exactly one of `data` (a `Buffer`, typed array, `ArrayBuffer`, or a base64 string or `data:`
|
|
345
|
-
URL) and `path` (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
358
|
+
URL) and `path` (a file on the file system), a `contentType` and optionally a `name`. `data` is
|
|
359
|
+
read and hashed when the attachment is created and is limited to 25 MiB, the `recordFile` limit:
|
|
360
|
+
it is held in memory from then until export, within the 128 MiB budget that queued spans share for
|
|
361
|
+
values awaiting upload. A `path` names a regular file: anything else, such as a named pipe, is
|
|
362
|
+
refused without waiting on it. A `path` up to 25 MiB is read the same way. A larger regular file, up to
|
|
363
|
+
Hue's 1 GB upload limit, is not held: it is hashed a 1 MiB slice at a time when the attachment is
|
|
364
|
+
created and read again from disk when it is uploaded, each slice hashed as it is sent and the last
|
|
365
|
+
sent only once the file ended at the size and SHA-256 it was recorded with. Keep such a file in
|
|
366
|
+
place and unchanged until the client flushes: one that changed, is gone or is no longer a regular
|
|
367
|
+
file is not uploaded (the
|
|
368
|
+
request ends before its body is complete, and Hue's upload URL signs the size and SHA-256, so the
|
|
369
|
+
store refuses other bytes), and it counts in `report.uploadFallbacks`, its part keeping the digest
|
|
370
|
+
it was recorded with.
|
|
349
371
|
|
|
350
372
|
At its position the attachment is recorded as a GenAI `blob` part that names the file by digest,
|
|
351
373
|
`{"type":"blob","mime_type":"application/pdf","modality":"document","sha256":"<64 hex>","size":48213,"name":"contract.pdf"}`,
|
|
@@ -362,7 +384,11 @@ With `captureContent: false` or a setup credential no bytes leave the process: a
|
|
|
362
384
|
attribute still records the part, without its name, and content values are not recorded at all. A
|
|
363
385
|
part the redactor removes or rewrites is not uploaded. A part that cannot be uploaded keeps its
|
|
364
386
|
digest and is counted in `report.uploadFallbacks`, as is one nested more than 10 levels deep in its
|
|
365
|
-
value, where Hue does not read parts, or one in a value cut to the value limit.
|
|
387
|
+
value, where Hue does not read parts, or one in a value cut to the value limit. An attachment in a
|
|
388
|
+
value that is still over the value limit with its files as digest parts is uploaded and listed as
|
|
389
|
+
usual, and `client.media.fetch({ traceId, sha256 })` and Hue's span read reach it by digest; the
|
|
390
|
+
value itself uploads whole and keeps only its first 16 KiB inline, so readers do not place the file
|
|
391
|
+
at its position in the value. Creating an
|
|
366
392
|
attachment never throws: one without exactly one of `data` and `path`, with unreadable or larger
|
|
367
393
|
data or an invalid content type is recorded as `null` and counted as an instrumentation failure;
|
|
368
394
|
an invalid name alone is omitted and counted.
|
|
@@ -385,9 +411,14 @@ await hue.withSpan("review contract", async () => {
|
|
|
385
411
|
```
|
|
386
412
|
|
|
387
413
|
`upload` defaults to `false`, which hashes and measures `data` locally without exporting bytes.
|
|
388
|
-
`data` is limited to 25 MiB
|
|
389
|
-
pass `sha256` and `byteSize` when you already have them.
|
|
390
|
-
|
|
414
|
+
`data` is limited to 25 MiB, as it is held in memory until it is uploaded. Larger data is omitted
|
|
415
|
+
and counted as an instrumentation failure; pass `sha256` and `byteSize` when you already have them.
|
|
416
|
+
Give `path` instead of `data` for a file on disk: up to 25 MiB it is read as `data` is, and a larger
|
|
417
|
+
regular file, up to Hue's 1 GB upload limit, is hashed a slice at a time now and, with `upload`,
|
|
418
|
+
read again from disk when it is uploaded, as an [`Attachment`](#attach-a-file-anywhere) given by
|
|
419
|
+
path is: it uploads only if it still has the size and SHA-256 it was recorded with. A file over
|
|
420
|
+
1 GB, a path that is no regular file, or `data` and `path` together, is omitted and counted. Upload requires supplied bytes or a
|
|
421
|
+
path, content capture and an ordinary project key. It runs during export and adds a `hue.blobs` entry to the
|
|
391
422
|
same span, keyed `event:hue.file:<sha256>`, while preserving the event. An export fits every
|
|
392
423
|
span's events and entries before it uploads any of these files, then uploads the files of all its
|
|
393
424
|
spans together, four at a time; a file whose event was redacted or shed is not uploaded.
|
|
@@ -480,7 +511,37 @@ the recorded model call again with the real image, document or audio. See
|
|
|
480
511
|
| `LangfuseMedia` | `Attachment` | [`Attachment`](#attach-a-file-anywhere), at the same position |
|
|
481
512
|
| `resolveMediaReferences({ obj, resolveWith: "base64DataUri" })` | | `client.media.resolve(obj, { traceId })` |
|
|
482
513
|
| `GET /api/public/media/{mediaId}` | `ReadonlyAttachment.data()` | `client.media.fetch({ traceId, sha256 })` |
|
|
483
|
-
| Media tokens (`@@@langfuseMedia:…@@@`) | `ExternalAttachment` | Left as recorded: Hue never fetches a remote URL or a file another service holds |
|
|
514
|
+
| Media tokens (`@@@langfuseMedia:…@@@`) | `ExternalAttachment` | Left as recorded: Hue never fetches a remote URL or a file another service holds. [`hue import langfuse --media`](#import-from-langfuse) copies Langfuse's files with your keys |
|
|
515
|
+
|
|
516
|
+
## Import from Langfuse
|
|
517
|
+
|
|
518
|
+
`hue import langfuse` copies a Langfuse project's history into Hue. It runs on your machine with
|
|
519
|
+
the Langfuse project's own keys (`LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, and
|
|
520
|
+
`LANGFUSE_BASE_URL` or `LANGFUSE_HOST`), which go only to Langfuse, and sends the spans with your
|
|
521
|
+
`HUE_API_KEY`:
|
|
522
|
+
|
|
523
|
+
```sh
|
|
524
|
+
npx -y @hue-run/sdk@0.18.2 import langfuse --env-path .env.langfuse --from 2026-01-01 --dry-run
|
|
525
|
+
npx -y @hue-run/sdk@0.18.2 import langfuse --env-path .env.langfuse --from 2026-01-01 --media
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
- It reads Langfuse Cloud and self-hosted v4 through the Observations API v2, and self-hosted v3
|
|
529
|
+
through the v1 traces and observations API, newest first; `--from`, `--to`, `--limit` and
|
|
530
|
+
`--trace-id` select traces, and `--dry-run` counts them without sending anything.
|
|
531
|
+
- Each observation becomes the span a Langfuse OpenTelemetry SDK records, with the
|
|
532
|
+
`langfuse.observation.*` and `langfuse.trace.*` attributes Hue reads, the user, session, release
|
|
533
|
+
and environment, and the model and token counts as GenAI attributes. Hex ids are kept and other
|
|
534
|
+
ids derived from them, so running the same command again resumes an import without duplicates.
|
|
535
|
+
- Spans are exported by the SDK's transport with its content-capture default; `--no-content`
|
|
536
|
+
sends metadata only.
|
|
537
|
+
- With `--media`, files that Langfuse media references point to are downloaded and uploaded under
|
|
538
|
+
the trace in the [digest form](#automatically-captured-media) the SDK writes, listed in
|
|
539
|
+
`hue.blobs`; a reference Langfuse holds no bytes for stays as recorded.
|
|
540
|
+
- Langfuse's rate limits are waited out for as long as `Retry-After` asks.
|
|
541
|
+
|
|
542
|
+
The command has no Python counterpart; it runs with `npx` for an application in any language. See
|
|
543
|
+
[Import traces from Langfuse](CLI.md#import-traces-from-langfuse) for every option, the attribute
|
|
544
|
+
mapping, media handling, limits and exit codes.
|
|
484
545
|
|
|
485
546
|
## Vercel AI SDK 6
|
|
486
547
|
|
|
@@ -787,7 +848,9 @@ apart from their record, up to Hue's upload limit of 1,000,000,000 bytes:
|
|
|
787
848
|
|
|
788
849
|
Hue stores the value under the trace, so the trace's retention, deletion and erasure cover it.
|
|
789
850
|
Uploads run in the export, never on the application's thread: at most four at a time, each request
|
|
790
|
-
within the export timeout, and all of one export's uploads within six export timeouts
|
|
851
|
+
within the export timeout, and all of one export's uploads within six export timeouts (60 seconds
|
|
852
|
+
at the default `timeoutMillis` of 10000), so a 1 GB file needs about 17 MB/s at the defaults;
|
|
853
|
+
raise `timeoutMillis` (up to 60000) for slower links. A
|
|
791
854
|
transient failure is retried once. A rate-limited reservation (429) waits out its `Retry-After`
|
|
792
855
|
within the export's 60-second hold. `flush()` and `shutdown()` wait for uploads like any export
|
|
793
856
|
work, and a `shutdownSafe()` whose budget runs out ends the uploads in flight.
|
|
@@ -796,7 +859,8 @@ A value that cannot be uploaded (Hue or the store refused it or failed, the netw
|
|
|
796
859
|
did, the receiver has no upload route, or the value is over 1 GB) is exported as before uploads
|
|
797
860
|
existed: whole attributes keep a prefix up to the limit and are listed under `hue.truncated`;
|
|
798
861
|
media parts keep their digest without a blob entry; failed opt-in `recordFile` uploads preserve
|
|
799
|
-
the file event.
|
|
862
|
+
the file event. Bytes given in memory to `recordFile` or an `Attachment` are limited to 25 MiB; a
|
|
863
|
+
file given by path, a file read in place from a value and a whole attribute to 1 GB. Each
|
|
800
864
|
export reports such values in a `warning` issue per cause, whose `count` is the values and
|
|
801
865
|
`traceIds` their traces, and counts them in `report.uploadFallbacks`; successful uploads are counted
|
|
802
866
|
in `report.uploadedValues`. Both counters include whole attributes, eligible media, attachments
|
package/dist/attachment.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import type { ByteSource } from "./large-media.js";
|
|
1
2
|
/** How deep in a value an attachment's part may sit and still be uploaded: Hue reads media parts
|
|
2
3
|
* within ten levels of the value's root. */
|
|
3
4
|
export declare const MAX_ATTACHMENT_DEPTH = 10;
|
|
4
5
|
/** Options for {@link Attachment}. Give exactly one of `data` and `path`. */
|
|
5
6
|
export interface AttachmentOptions {
|
|
6
7
|
/** The file's bytes (a `Buffer`, typed array or `ArrayBuffer`), or a string holding them as
|
|
7
|
-
* base64 or a `data:` URL. */
|
|
8
|
+
* base64 or a `data:` URL; up to 25 MiB. */
|
|
8
9
|
data?: ArrayBufferView | ArrayBuffer | string;
|
|
9
|
-
/** A file to read
|
|
10
|
+
/** A file to read (Node.js file system): up to 25 MiB it is read when the attachment is created;
|
|
11
|
+
* a larger regular file, up to Hue's 1 GB upload limit, is hashed then and read again from disk
|
|
12
|
+
* when it is uploaded. */
|
|
10
13
|
path?: string;
|
|
11
14
|
/** The file's media type, such as `image/png` or `application/pdf`. */
|
|
12
15
|
contentType: string;
|
|
@@ -15,15 +18,50 @@ export interface AttachmentOptions {
|
|
|
15
18
|
}
|
|
16
19
|
/** What an attachment holds, read by the client and transport only. */
|
|
17
20
|
export interface AttachmentState {
|
|
18
|
-
/** The file's bytes; undefined for
|
|
21
|
+
/** The file's bytes; undefined for a file read from disk when it is uploaded, and for an
|
|
22
|
+
* attachment that could not be read or is invalid. */
|
|
19
23
|
bytes?: Uint8Array;
|
|
24
|
+
/** A file given by path over 25 MiB, read again from disk when it is uploaded. */
|
|
25
|
+
file?: PathFile;
|
|
20
26
|
sha256?: string;
|
|
27
|
+
/** The file's size; undefined for an invalid attachment. */
|
|
28
|
+
size?: number;
|
|
21
29
|
contentType: string;
|
|
22
30
|
/** The name, when it is a valid media name. */
|
|
23
31
|
name?: string;
|
|
24
32
|
/** A name was given and is not a valid media name: it is omitted and counted. */
|
|
25
33
|
invalidName: boolean;
|
|
26
34
|
}
|
|
35
|
+
/** A regular file given by path over the in-memory limit, as it was hashed when it was recorded:
|
|
36
|
+
* its absolute path, size and SHA-256. Its bytes are read again from disk when it is uploaded. */
|
|
37
|
+
export interface PathFile {
|
|
38
|
+
path: string;
|
|
39
|
+
size: number;
|
|
40
|
+
sha256: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A regular file given by path: its bytes when it is no larger than 25 MiB, and otherwise, up to
|
|
44
|
+
* Hue's 1 GB upload limit, its size and SHA-256 from one reading pass a slice at a time, never
|
|
45
|
+
* holding it whole. Undefined for a file over its limit and for anything that is no regular file
|
|
46
|
+
* (a named pipe, a device), which is opened without waiting and never read; throws for one that
|
|
47
|
+
* cannot be opened.
|
|
48
|
+
*/
|
|
49
|
+
export declare function readPath(path: string): {
|
|
50
|
+
bytes: Uint8Array;
|
|
51
|
+
} | {
|
|
52
|
+
file: PathFile;
|
|
53
|
+
} | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* A file given by path as an upload body, read from disk a slice at a time. Each slice is hashed as
|
|
56
|
+
* it is read and sent once the next one is read; the last is sent only once the file ended at its
|
|
57
|
+
* recorded size with its recorded SHA-256. A file that changed since it was recorded, or can no
|
|
58
|
+
* longer be read, ends the read with an error before its body is complete and sets `changed`, so
|
|
59
|
+
* the upload is abandoned and the value is counted as not uploaded; so does a path that is no
|
|
60
|
+
* longer a regular file, which is opened without waiting (a named pipe in its place would wait for
|
|
61
|
+
* a writer) and never read. Hue's presigned PUT also signs the size and SHA-256, so the store
|
|
62
|
+
* refuses a body that differs.
|
|
63
|
+
*/
|
|
64
|
+
export declare function fileSource(file: PathFile): ByteSource;
|
|
27
65
|
/**
|
|
28
66
|
* A file placed in a recorded value, the equivalent of Langfuse's `LangfuseMedia` and
|
|
29
67
|
* Braintrust's `Attachment`. Put it anywhere in a `withSpan()` input or output, in `model()`
|
|
@@ -32,10 +70,16 @@ export interface AttachmentState {
|
|
|
32
70
|
* names the file by digest (`{ type: "blob", mime_type, modality, sha256, size, name }`) and
|
|
33
71
|
* uploads its bytes during export, listed under the record's `hue.blobs`.
|
|
34
72
|
*
|
|
35
|
-
* The bytes are read and hashed when the attachment is created; `data`
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
73
|
+
* The bytes are read and hashed when the attachment is created; `data` is limited to 25 MiB, as
|
|
74
|
+
* the bytes are held in memory until they are uploaded. A `path` names a regular file, opened
|
|
75
|
+
* without waiting: anything else, such as a named pipe, is invalid. One up to 25 MiB is read then
|
|
76
|
+
* too; a larger regular file, up to Hue's 1 GB upload limit, is hashed then a slice at a time and read
|
|
77
|
+
* again from disk when it is uploaded, so it must stay in place and unchanged until the client
|
|
78
|
+
* flushes: a file whose size or SHA-256 then differs is not uploaded and counts as an upload
|
|
79
|
+
* fallback, its part keeping the digest it was recorded with. Creating one never throws: an
|
|
80
|
+
* attachment without exactly one of `data` and `path`, with unreadable data, larger data or an
|
|
81
|
+
* invalid `contentType` records `null` where it is placed and counts an instrumentation failure;
|
|
82
|
+
* an invalid `name` alone is omitted and counted.
|
|
39
83
|
*/
|
|
40
84
|
export declare class Attachment {
|
|
41
85
|
/** The media type given. */
|