@hue-run/sdk 0.21.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLI.md +246 -52
- package/README.md +54 -13
- package/WORLDS.md +6 -3
- package/dist/ai-sdk.d.ts +23 -1
- package/dist/ai-sdk.js +60 -2
- package/dist/cli/import-formats/adapter.d.ts +80 -0
- package/dist/cli/import-formats/adapter.js +1 -0
- package/dist/cli/import-formats/agent-events.d.ts +5 -0
- package/dist/cli/import-formats/agent-events.js +16 -0
- package/dist/cli/import-formats/atif.d.ts +3 -0
- package/dist/cli/import-formats/atif.js +15 -0
- package/dist/cli/import-formats/braintrust.d.ts +2 -0
- package/dist/cli/import-formats/braintrust.js +222 -0
- package/dist/cli/import-formats/containers.d.ts +32 -0
- package/dist/cli/import-formats/containers.js +399 -0
- package/dist/cli/import-formats/datadog.d.ts +2 -0
- package/dist/cli/import-formats/datadog.js +264 -0
- package/dist/cli/import-formats/helicone.d.ts +2 -0
- package/dist/cli/import-formats/helicone.js +161 -0
- package/dist/cli/import-formats/identity.d.ts +112 -0
- package/dist/cli/import-formats/identity.js +594 -0
- package/dist/cli/import-formats/jaeger.d.ts +3 -0
- package/dist/cli/import-formats/jaeger.js +266 -0
- package/dist/cli/import-formats/langfuse.d.ts +7 -0
- package/dist/cli/import-formats/langfuse.js +510 -0
- package/dist/cli/import-formats/langsmith.d.ts +2 -0
- package/dist/cli/import-formats/langsmith.js +244 -0
- package/dist/cli/import-formats/messages.d.ts +28 -0
- package/dist/cli/import-formats/messages.js +301 -0
- package/dist/cli/import-formats/mlflow.d.ts +2 -0
- package/dist/cli/import-formats/mlflow.js +260 -0
- package/dist/cli/import-formats/openai-agents.d.ts +2 -0
- package/dist/cli/import-formats/openai-agents.js +221 -0
- package/dist/cli/import-formats/opik.d.ts +2 -0
- package/dist/cli/import-formats/opik.js +193 -0
- package/dist/cli/import-formats/otel-family.d.ts +5 -0
- package/dist/cli/import-formats/otel-family.js +17 -0
- package/dist/cli/import-formats/otel-spans.d.ts +94 -0
- package/dist/cli/import-formats/otel-spans.js +262 -0
- package/dist/cli/import-formats/otlp-protobuf.d.ts +35 -0
- package/dist/cli/import-formats/otlp-protobuf.js +486 -0
- package/dist/cli/import-formats/otlp.d.ts +4 -0
- package/dist/cli/import-formats/otlp.js +15 -0
- package/dist/cli/import-formats/phoenix.d.ts +3 -0
- package/dist/cli/import-formats/phoenix.js +256 -0
- package/dist/cli/import-formats/raindrop.d.ts +21 -0
- package/dist/cli/import-formats/raindrop.js +255 -0
- package/dist/cli/import-formats/reader.d.ts +56 -0
- package/dist/cli/import-formats/reader.js +364 -0
- package/dist/cli/import-formats/registry.d.ts +36 -0
- package/dist/cli/import-formats/registry.js +71 -0
- package/dist/cli/import-formats/row-platform-fields.d.ts +32 -0
- package/dist/cli/import-formats/row-platform-fields.js +76 -0
- package/dist/cli/import-formats/rows.d.ts +165 -0
- package/dist/cli/import-formats/rows.js +638 -0
- package/dist/cli/import-formats/transcripts.d.ts +25 -0
- package/dist/cli/import-formats/transcripts.js +498 -0
- package/dist/cli/import-formats/weave.d.ts +2 -0
- package/dist/cli/import-formats/weave.js +159 -0
- package/dist/cli/import-formats/zipkin.d.ts +2 -0
- package/dist/cli/import-formats/zipkin.js +219 -0
- package/dist/cli/import.d.ts +1 -1
- package/dist/cli/import.js +4 -2
- package/dist/cli/traces-import-convert.d.ts +65 -33
- package/dist/cli/traces-import-convert.js +211 -214
- package/dist/cli/traces-import-prepare.d.ts +26 -22
- package/dist/cli/traces-import-prepare.js +51 -122
- package/dist/cli/traces-import.d.ts +4 -4
- package/dist/cli/traces-import.js +63 -11
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { ConversionError, isObject, parseNestedJsonText, unixNano, } from "../traces-import-convert.js";
|
|
2
|
+
import { identityText } from "./identity.js";
|
|
3
|
+
import { nanosFrom, readRecords } from "./rows.js";
|
|
4
|
+
import { requestContent, requestInstructions, responseContent } from "./transcripts.js";
|
|
5
|
+
const object = (value) => isObject(value) ? value : undefined;
|
|
6
|
+
const text = (value) => typeof value === "string" && value ? value : undefined;
|
|
7
|
+
const count = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
8
|
+
const isRequest = (value) => isObject(value) &&
|
|
9
|
+
typeof value.request_id === "string" &&
|
|
10
|
+
"request_created_at" in value &&
|
|
11
|
+
("response_status" in value || "request_user_id" in value || "request_properties" in value);
|
|
12
|
+
/** The requests an API answer (`{"data": […]}`) holds, when it holds any. */
|
|
13
|
+
function envelopeRequests(value) {
|
|
14
|
+
return isObject(value) && Array.isArray(value.data) && value.data.some(isRequest)
|
|
15
|
+
? value.data
|
|
16
|
+
: undefined;
|
|
17
|
+
}
|
|
18
|
+
/** A body as recorded, JSON text parsed; Helicone's placeholder for an unfetched body is none. */
|
|
19
|
+
function body(value) {
|
|
20
|
+
let parsed = value;
|
|
21
|
+
if (typeof value === "string") {
|
|
22
|
+
if (!value)
|
|
23
|
+
return undefined;
|
|
24
|
+
try {
|
|
25
|
+
parsed = parseNestedJsonText(value).value;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const fields = object(parsed);
|
|
32
|
+
if (fields && Object.keys(fields).length === 1 && "helicone_message" in fields)
|
|
33
|
+
return undefined;
|
|
34
|
+
return parsed ?? undefined;
|
|
35
|
+
}
|
|
36
|
+
/** A duration in milliseconds as nanoseconds; zero is allowed. */
|
|
37
|
+
const milliseconds = (value) => (value === 0 ? 0n : nanosFrom(value, "ms"));
|
|
38
|
+
/** Properties that mark Helicone's own evaluation traffic, lower-cased as Helicone compares
|
|
39
|
+
* them. */
|
|
40
|
+
const EVALUATION_PROPERTIES = new Set(["helicone-experiment-id", "helicone-evaluator"]);
|
|
41
|
+
/** The request's path: `target_url`'s, else `request_path` (the older Postgres export's). */
|
|
42
|
+
function pathOf(record) {
|
|
43
|
+
const target = text(record.target_url);
|
|
44
|
+
if (target)
|
|
45
|
+
try {
|
|
46
|
+
return new URL(target).pathname;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Not a URL: the recorded path, if any, is read instead.
|
|
50
|
+
}
|
|
51
|
+
return text(record.request_path) ?? "";
|
|
52
|
+
}
|
|
53
|
+
/** Input and output as the GenAI conventions count them: Helicone's counts with the cached and
|
|
54
|
+
* reasoning tokens it took out added back. */
|
|
55
|
+
function usageOf(record) {
|
|
56
|
+
const prompt = count(record.prompt_tokens);
|
|
57
|
+
const completion = count(record.completion_tokens);
|
|
58
|
+
const cacheRead = count(record.prompt_cache_read_tokens);
|
|
59
|
+
const cacheWrite = count(record.prompt_cache_write_tokens);
|
|
60
|
+
const reasoning = count(record.reasoning_tokens);
|
|
61
|
+
return {
|
|
62
|
+
input: prompt === undefined ? undefined : prompt + (cacheRead ?? 0) + (cacheWrite ?? 0),
|
|
63
|
+
output: completion === undefined ? undefined : completion + (reasoning ?? 0),
|
|
64
|
+
cacheRead,
|
|
65
|
+
cacheWrite,
|
|
66
|
+
reasoning,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function readRequest(record, context) {
|
|
70
|
+
const requestId = text(record.request_id);
|
|
71
|
+
if (!requestId)
|
|
72
|
+
throw new ConversionError("a request without a request_id");
|
|
73
|
+
const properties = object(record.request_properties) ?? object(record.properties);
|
|
74
|
+
for (const key of ["request_properties", "properties"])
|
|
75
|
+
if (record[key] != null && !object(record[key]))
|
|
76
|
+
throw new ConversionError(`${key} is not an object`);
|
|
77
|
+
const session = identityText(properties?.["Helicone-Session-Id"]);
|
|
78
|
+
if (typeof record.request_created_at !== "string")
|
|
79
|
+
throw new ConversionError("a request without request_created_at");
|
|
80
|
+
const start = BigInt(unixNano(record.request_created_at));
|
|
81
|
+
let end;
|
|
82
|
+
let timing;
|
|
83
|
+
if (text(record.response_created_at))
|
|
84
|
+
end = BigInt(unixNano(record.response_created_at));
|
|
85
|
+
else if (typeof record.delay_ms === "number" && record.delay_ms >= 0) {
|
|
86
|
+
end = start + milliseconds(record.delay_ms);
|
|
87
|
+
timing = "end_from_latency";
|
|
88
|
+
}
|
|
89
|
+
const path = pathOf(record);
|
|
90
|
+
const embedding = /\/embeddings\/?$/.test(path);
|
|
91
|
+
const requestBody = body(record.request_body);
|
|
92
|
+
const responseBody = body(record.response_body);
|
|
93
|
+
const recorded = text(record.model);
|
|
94
|
+
const model = text(record.model_override) ??
|
|
95
|
+
text(record.request_model) ??
|
|
96
|
+
(recorded !== "unknown" ? recorded : undefined);
|
|
97
|
+
const status = record.response_status;
|
|
98
|
+
const system = embedding ? undefined : requestInstructions(requestBody);
|
|
99
|
+
const user = identityText(record.request_user_id);
|
|
100
|
+
const cost = typeof record.cost === "number" ? record.cost : record.costUSD;
|
|
101
|
+
const row = {
|
|
102
|
+
traceKey: session ?? requestId,
|
|
103
|
+
spanKey: requestId,
|
|
104
|
+
name: `${embedding ? "embeddings" : "chat"}${model ? ` ${model}` : ""}`,
|
|
105
|
+
kind: embedding ? "embedding" : "llm",
|
|
106
|
+
start,
|
|
107
|
+
...(end !== undefined ? { end } : {}),
|
|
108
|
+
...(timing ? { timing } : {}),
|
|
109
|
+
...(model ? { model } : {}),
|
|
110
|
+
...(text(record.response_model) ? { responseModel: text(record.response_model) } : {}),
|
|
111
|
+
...(text(record.provider) ? { provider: text(record.provider).toLowerCase() } : {}),
|
|
112
|
+
...(requestBody !== undefined
|
|
113
|
+
? { input: embedding ? requestBody : requestContent(requestBody) }
|
|
114
|
+
: {}),
|
|
115
|
+
...(responseBody !== undefined ? { output: responseContent(responseBody) } : {}),
|
|
116
|
+
...(system !== undefined ? { systemInstructions: system } : {}),
|
|
117
|
+
usage: usageOf(record),
|
|
118
|
+
...(user !== undefined ? { user } : {}),
|
|
119
|
+
...(session !== undefined ? { session } : {}),
|
|
120
|
+
attributes: {
|
|
121
|
+
...(properties && Object.keys(properties).length
|
|
122
|
+
? { "helicone.properties": JSON.stringify(properties) }
|
|
123
|
+
: {}),
|
|
124
|
+
...(text(properties?.["Helicone-Session-Path"])
|
|
125
|
+
? { "helicone.session.path": text(properties?.["Helicone-Session-Path"]) }
|
|
126
|
+
: {}),
|
|
127
|
+
...(text(properties?.["Helicone-Session-Name"])
|
|
128
|
+
? { "helicone.session.name": text(properties?.["Helicone-Session-Name"]) }
|
|
129
|
+
: {}),
|
|
130
|
+
...(path ? { "helicone.request.path": path } : {}),
|
|
131
|
+
...(typeof cost === "number" && Number.isFinite(cost) ? { "helicone.cost_usd": cost } : {}),
|
|
132
|
+
},
|
|
133
|
+
record,
|
|
134
|
+
...(text(record.updated_at) ? { revision: text(record.updated_at) } : {}),
|
|
135
|
+
};
|
|
136
|
+
if (Object.keys(properties ?? {}).some((key) => EVALUATION_PROPERTIES.has(key.toLowerCase())))
|
|
137
|
+
row.origin = "evaluation";
|
|
138
|
+
if (typeof status === "number" && status >= 400) {
|
|
139
|
+
const message = text(object(object(responseBody)?.error)?.message);
|
|
140
|
+
row.error = { message: message ?? `HTTP ${status}`, type: String(status) };
|
|
141
|
+
}
|
|
142
|
+
context.row(row);
|
|
143
|
+
}
|
|
144
|
+
export const helicone = {
|
|
145
|
+
id: "helicone",
|
|
146
|
+
manifestFormat: "otlp_json",
|
|
147
|
+
detect: (value) => isRequest(value) || envelopeRequests(value) !== undefined,
|
|
148
|
+
convert(value, context) {
|
|
149
|
+
const records = isRequest(value) ? [value] : (envelopeRequests(value) ?? []);
|
|
150
|
+
readRecords(context, records, (record) => {
|
|
151
|
+
if (!isRequest(record))
|
|
152
|
+
return undefined;
|
|
153
|
+
const properties = object(record.request_properties) ?? object(record.properties);
|
|
154
|
+
return identityText(properties?.["Helicone-Session-Id"]) ?? text(record.request_id);
|
|
155
|
+
}, (record) => {
|
|
156
|
+
if (!isRequest(record))
|
|
157
|
+
throw new ConversionError("a record that is not a Helicone request");
|
|
158
|
+
readRequest(record, context);
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { type ConvertedTrace, type ImportEntry, type JsonValue, type TraceIdentity } from "../traces-import-convert.js";
|
|
2
|
+
/**
|
|
3
|
+
* Who a trace ran for and which conversation it belongs to, read the way Hue reads a trace it
|
|
4
|
+
* receives, so the manifest's identity and the report's `without_identity` say what Hue will
|
|
5
|
+
* capture. Two parts, both copied from Hue's reader and kept in its order:
|
|
6
|
+
*
|
|
7
|
+
* - The identity registry: the attributes and metadata fields instrumentations name a user or a
|
|
8
|
+
* conversation by, first present wins on a record.
|
|
9
|
+
* - The owner rule: a trace's local roots (no parent, or a parent outside the trace) speak first;
|
|
10
|
+
* when they name nobody, the agent's other spans do. A tool span and everything beneath it never
|
|
11
|
+
* name the owner. Every carrier at the deciding step must agree, and an explicit
|
|
12
|
+
* `hue.world.entity.*` declaration wins over users.
|
|
13
|
+
*
|
|
14
|
+
* `--user-key` and `--session-key` name the identity of exports whose user sits under a key of
|
|
15
|
+
* their own; see {@link parseKeyPath} and {@link applyIdentityKeys}.
|
|
16
|
+
*/
|
|
17
|
+
/** Dedicated attributes, in precedence order. */
|
|
18
|
+
declare const DEDICATED: {
|
|
19
|
+
readonly user: readonly ["user.id", "enduser.id", "hue.user.id", "ai.telemetry.metadata.userId", "langfuse.user.id", "enduser.pseudo.id", "mlflow.trace.user", "mastra.metadata.resourceId"];
|
|
20
|
+
readonly session: readonly ["gen_ai.conversation.id", "session.id", "hue.session.id", "ai.telemetry.metadata.sessionId", "langfuse.session.id", "mlflow.trace.session"];
|
|
21
|
+
};
|
|
22
|
+
export type IdentityKind = keyof typeof DEDICATED;
|
|
23
|
+
/** Whether Hue reads an owner or a conversation from an attribute of this name. */
|
|
24
|
+
export declare const namesIdentity: (key: string) => boolean;
|
|
25
|
+
/** A recorded attribute without what Hue would read a `kind` identifier from: undefined for an
|
|
26
|
+
* attribute that names one itself, and a metadata map without the kind's fields. */
|
|
27
|
+
export declare function withoutIdentity(kind: IdentityKind, key: string, value: JsonValue): JsonValue | undefined;
|
|
28
|
+
/** An identifier as recorded: a non-blank string, or an integer as its decimal text. */
|
|
29
|
+
export declare function identityText(value: unknown): string | undefined;
|
|
30
|
+
/** The record's `kind` identifier: the first registry key it carries. `read` answers an
|
|
31
|
+
* attribute in source form (an int64 as its decimal text, a kvlist as an object). */
|
|
32
|
+
export declare function readIdentity(kind: IdentityKind, read: (key: string) => unknown): string | undefined;
|
|
33
|
+
/** Whether Hue reads the span as a tool call: its operation names one, or an instrumentation
|
|
34
|
+
* convention reads a tool from its attributes. */
|
|
35
|
+
export declare function isToolSpan(attributes: ReadonlyMap<string, JsonValue>): boolean;
|
|
36
|
+
/** One span of a trace as the owner rule sees it. */
|
|
37
|
+
export interface SpanView {
|
|
38
|
+
spanId: string;
|
|
39
|
+
parentSpanId?: string;
|
|
40
|
+
start: bigint;
|
|
41
|
+
attributes: Map<string, JsonValue>;
|
|
42
|
+
resource: Map<string, JsonValue>;
|
|
43
|
+
/** The span object as emitted, for a key path into it. */
|
|
44
|
+
span: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
export declare function spanViews(entries: readonly ImportEntry[]): SpanView[];
|
|
47
|
+
/** The ids of the spans `marked` picks and of every span beneath one, walked from them down
|
|
48
|
+
* without recursion; a parent chain that closes on itself reaches nothing marked on its own. */
|
|
49
|
+
export declare function subtreeIds<T extends {
|
|
50
|
+
spanId: string;
|
|
51
|
+
parentSpanId?: string;
|
|
52
|
+
}>(spans: readonly T[], marked: (span: T) => boolean): Set<string>;
|
|
53
|
+
/** The spans the agent itself recorded, split into local roots and the rest: a tool span and
|
|
54
|
+
* every span beneath one are left out. */
|
|
55
|
+
export declare function agentSpans<T extends {
|
|
56
|
+
spanId: string;
|
|
57
|
+
parentSpanId?: string;
|
|
58
|
+
}>(spans: readonly T[], tool: (span: T) => boolean): {
|
|
59
|
+
roots: T[];
|
|
60
|
+
others: T[];
|
|
61
|
+
};
|
|
62
|
+
/** The trace's identity as Hue will capture it from these spans: the owner (an explicit entity or
|
|
63
|
+
* a user) and the session. */
|
|
64
|
+
export declare function traceIdentity(entries: readonly ImportEntry[]): TraceIdentity | undefined;
|
|
65
|
+
/** Where `--user-key` or `--session-key` reads: a JSON path into a span's source record, or a
|
|
66
|
+
* dotted key looked up on the span and its record. */
|
|
67
|
+
export type KeyPath = {
|
|
68
|
+
kind: "pointer";
|
|
69
|
+
segments: string[];
|
|
70
|
+
text: string;
|
|
71
|
+
} | {
|
|
72
|
+
kind: "dotted";
|
|
73
|
+
key: string;
|
|
74
|
+
text: string;
|
|
75
|
+
};
|
|
76
|
+
export interface IdentityKeys {
|
|
77
|
+
user?: KeyPath;
|
|
78
|
+
session?: KeyPath;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A key path from its command-line text, or undefined when it is malformed:
|
|
82
|
+
* - `/a/b` is a JSON Pointer into the source record (`~1` for `/`, `~0` for `~`);
|
|
83
|
+
* - `$.a.b`, `$.a["b.c"]` or `$.a[0]` is a JSONPath of members and indexes into the record;
|
|
84
|
+
* - anything else is a dotted key: an attribute of the span or its resource, then a key of the
|
|
85
|
+
* record's metadata (exact, then as a dotted path), then of the record itself.
|
|
86
|
+
*/
|
|
87
|
+
export declare function parseKeyPath(text: string): KeyPath | undefined;
|
|
88
|
+
/** What a key path names on one span, as an identifier. */
|
|
89
|
+
export declare function resolveKey(path: KeyPath, view: {
|
|
90
|
+
attributes: ReadonlyMap<string, unknown>;
|
|
91
|
+
resource?: ReadonlyMap<string, unknown>;
|
|
92
|
+
}, record: unknown, metadata?: unknown): string | undefined;
|
|
93
|
+
/** The single value a key names across the agent's spans: the roots' when they name one, else
|
|
94
|
+
* the other spans'; spans that disagree name nothing. */
|
|
95
|
+
export declare function decideKey<T extends {
|
|
96
|
+
spanId: string;
|
|
97
|
+
parentSpanId?: string;
|
|
98
|
+
}>(spans: readonly T[], tool: (span: T) => boolean, value: (span: T) => string | undefined): {
|
|
99
|
+
found: boolean;
|
|
100
|
+
value?: string;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Applies `--user-key` and `--session-key` to a trace whose spans are kept as converted (OTLP,
|
|
104
|
+
* Langfuse, ATIF): a span's bytes never change, so a trace also sent live does not conflict.
|
|
105
|
+
* When a key names one user across the agent's spans and the trace names no owner, a child span
|
|
106
|
+
* `hue.import.user` with the single attribute `user.id` is added under the agent's earliest local
|
|
107
|
+
* root (never a tool span), which Hue reads as one of the agent's spans; `hue.import.session` with
|
|
108
|
+
* `gen_ai.conversation.id` likewise names a session the trace lacks. An owner the spans already
|
|
109
|
+
* name is kept: Hue reads it before any added span.
|
|
110
|
+
*/
|
|
111
|
+
export declare function applyIdentityKeys(trace: ConvertedTrace, keys: IdentityKeys): void;
|
|
112
|
+
export {};
|