@datagol/lens-js 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DataGOL
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,131 @@
1
- # Temporary Holding Version
1
+ # @datagol/lens-js
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Node/TypeScript SDK for [Datagol Lens](../README.md) — ships LLM-call and
4
+ agent tool-call traces to the Datagol Lens API. This is the Node counterpart
5
+ to the Python SDK at [`../sdk`](../sdk); the two talk the same
6
+ `POST /api/v1/projects/{project_id}/traces` wire format (see
7
+ `backend/app/schemas.py`) but are otherwise independent — this package
8
+ authenticates with a **project API key** only, not a user login/JWT (see
9
+ "Auth" below).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @datagol/lens-js
15
+ ```
16
+
17
+ Node 18 or newer. To work on the package itself: `npm install && npm test` in
18
+ this directory.
19
+
20
+ ## Auth
21
+
22
+ Trace-writing endpoints accept a project-scoped API key
23
+ (`Authorization: Bearer dgl_...`), separate from the human-login JWT the
24
+ dashboard uses. Mint one as a logged-in user:
25
+
26
+ ```bash
27
+ curl -X POST https://<lens-host>/api/v1/projects/<project_id>/api-keys \
28
+ -H "Authorization: Bearer <your-jwt>" \
29
+ -H "content-type: application/json" \
30
+ -d '{"name": "my-service"}'
31
+ # → { "token": "dgl_...", ... } — shown once, store it as a secret
32
+ ```
33
+
34
+ Then configure the SDK with that token — either via environment variables
35
+ (`DATAGOL_LENS_API_URL`, `DATAGOL_LENS_API_KEY`, `DATAGOL_LENS_PROJECT_ID`)
36
+ or constructor options.
37
+
38
+ ## Usage
39
+
40
+ ### Low-level: `DatagolLensClient`
41
+
42
+ ```ts
43
+ import { DatagolLensClient } from "@datagol/lens-js";
44
+
45
+ const client = new DatagolLensClient({
46
+ apiUrl: "https://lens.example.com",
47
+ apiKey: process.env.DATAGOL_LENS_API_KEY,
48
+ projectId: process.env.DATAGOL_LENS_PROJECT_ID,
49
+ });
50
+
51
+ await client.postTrace({
52
+ name: "qa-agent",
53
+ input: { question: "..." },
54
+ output: { answer: "..." },
55
+ model: "claude-sonnet-4-6",
56
+ prompt_tokens: 120,
57
+ completion_tokens: 40,
58
+ total_tokens: 160,
59
+ total_cost: 0.0009,
60
+ status: "success",
61
+ });
62
+ ```
63
+
64
+ ### Ergonomic: `TraceBuilder`
65
+
66
+ Accumulates spans (LLM calls, tool calls, retriever calls, ...) and ships one
67
+ `Trace` with all of them nested, mirroring the Python SDK's `TraceContext`:
68
+
69
+ ```ts
70
+ import { DatagolLensClient, TraceBuilder } from "@datagol/lens-js";
71
+
72
+ const client = new DatagolLensClient();
73
+ const trace = new TraceBuilder(client, {
74
+ name: "qa-agent",
75
+ input: { question: "..." },
76
+ sessionId: "chat-abc123",
77
+ externalUserId: "user-42",
78
+ });
79
+
80
+ try {
81
+ const start = Date.now();
82
+ const answer = await callLlm(/* ... */);
83
+ trace.addSpan({
84
+ name: "llm-call",
85
+ spanType: "llm",
86
+ model: "claude-sonnet-4-6",
87
+ input: { messages: [/* ... */] },
88
+ output: { text: answer },
89
+ promptTokens: 120,
90
+ completionTokens: 40,
91
+ cost: 0.0009,
92
+ latencyMs: Date.now() - start,
93
+ });
94
+ trace.setOutput({ answer });
95
+ } catch (err) {
96
+ trace.setError(String(err));
97
+ throw err;
98
+ } finally {
99
+ await trace.flush();
100
+ }
101
+ ```
102
+
103
+ ### Budgets and attribution
104
+
105
+ Pass `companyId`, `agentKey` and `agentName` (plus `externalUserId`) to
106
+ `TraceBuilder` and Lens counts the trace's cost against that company, agent and
107
+ user. Lens prices spans that don't report a `cost` from their model and tokens.
108
+ Before a run, check the budgets:
109
+
110
+ ```ts
111
+ import { BudgetExceeded, LensRequestError } from "@datagol/lens-js";
112
+
113
+ const { company_ids } = await client.getEnforcedCompanies(); // cache this
114
+ if (company_ids.includes(companyId)) {
115
+ const result = await client.checkBudget({ companyId, userId, agentKey });
116
+ if (!result.allowed) throw new BudgetExceeded("over budget", result);
117
+ }
118
+ ```
119
+
120
+ Both calls throw `LensRequestError` on a timeout (2 s by default), a network
121
+ error, a non-2xx status or a body that isn't JSON, so you choose whether to
122
+ fail open or closed.
123
+
124
+ ## Scope
125
+
126
+ This package covers trace/span ingestion only — no redaction or guardrails
127
+ port yet (the Python SDK's `redaction.py`/`guardrails.py` have no JS
128
+ equivalent here). If a Node consumer needs PII redaction before data leaves
129
+ the process, that's a natural follow-up; for now, configure redaction rules
130
+ server-side (`GET /api/v1/projects/{project_id}/redaction-rules`) or scrub
131
+ `input`/`output` yourself before calling `postTrace`/`addSpan`.
@@ -0,0 +1,54 @@
1
+ import type { BudgetCheckResponse, BudgetStatus, EnforcedCompanies, TraceCreate, TraceOut } from "./types.js";
2
+ /** A Lens API call failed: network error, timeout, non-2xx status or a bad body.
3
+ * `status` is set when the server answered with a non-2xx status. */
4
+ export declare class LensRequestError extends Error {
5
+ readonly status?: number | undefined;
6
+ constructor(message: string, status?: number | undefined);
7
+ }
8
+ /** Thrown by callers when a budget check blocks a request; `budget` is the
9
+ * check's `blocked_by`. */
10
+ export declare class BudgetExceeded extends Error {
11
+ readonly result?: BudgetCheckResponse | undefined;
12
+ readonly budget: BudgetStatus | null;
13
+ constructor(message: string, result?: BudgetCheckResponse | undefined);
14
+ }
15
+ export interface DatagolLensClientOptions {
16
+ /** Base URL of the Datagol Lens API. Defaults to DATAGOL_LENS_API_URL, then http://localhost:8000. */
17
+ apiUrl?: string;
18
+ /** A project-scoped API key ("dgl_..."), minted via
19
+ * POST /api/v1/projects/{project_id}/api-keys (requires a logged-in user), or
20
+ * a platform key ("dglp_...") from Lens Admin. Defaults to DATAGOL_LENS_API_KEY. */
21
+ apiKey?: string;
22
+ /** Default project id used when a call site doesn't pass one explicitly.
23
+ * Defaults to DATAGOL_LENS_PROJECT_ID. */
24
+ projectId?: string;
25
+ /** Override fetch (e.g. for tests). Defaults to the global fetch. */
26
+ fetchImpl?: typeof fetch;
27
+ }
28
+ /** Minimal HTTP client for the Datagol Lens tracing API. Authenticates with a
29
+ * project-scoped API key only — this SDK is meant for server-side/agent
30
+ * tracing (see app/auth.py's get_trace_actor), not interactive human use, so
31
+ * it deliberately doesn't support the user-JWT login flow the Python SDK's
32
+ * DatagolLensClient predates this one with. */
33
+ export declare class DatagolLensClient {
34
+ readonly apiUrl: string;
35
+ readonly apiKey: string;
36
+ readonly projectId: string | undefined;
37
+ private readonly fetchImpl;
38
+ constructor(options?: DatagolLensClientOptions);
39
+ /** POST one trace (with its spans nested inline). Throws on a non-2xx response. */
40
+ postTrace(trace: TraceCreate, projectId?: string | undefined): Promise<TraceOut>;
41
+ /** Check a company (and optionally a user and agent) against its Lens budgets
42
+ * before starting a run. Throws LensRequestError on any failure, so the
43
+ * caller decides whether to fail open or closed. */
44
+ checkBudget(request: {
45
+ companyId: string;
46
+ userId?: string;
47
+ agentKey?: string;
48
+ scopes?: Record<string, string>;
49
+ }, projectId?: string | undefined, timeoutMs?: number): Promise<BudgetCheckResponse>;
50
+ /** The company ids with budget enforcement on, so callers can skip the check
51
+ * for everyone else. Throws LensRequestError on any failure. */
52
+ getEnforcedCompanies(projectId?: string | undefined, timeoutMs?: number): Promise<EnforcedCompanies>;
53
+ private requestJson;
54
+ }
package/dist/client.js ADDED
@@ -0,0 +1,104 @@
1
+ /** A Lens API call failed: network error, timeout, non-2xx status or a bad body.
2
+ * `status` is set when the server answered with a non-2xx status. */
3
+ export class LensRequestError extends Error {
4
+ status;
5
+ constructor(message, status) {
6
+ super(message);
7
+ this.status = status;
8
+ this.name = "LensRequestError";
9
+ }
10
+ }
11
+ /** Thrown by callers when a budget check blocks a request; `budget` is the
12
+ * check's `blocked_by`. */
13
+ export class BudgetExceeded extends Error {
14
+ result;
15
+ budget;
16
+ constructor(message, result) {
17
+ super(message);
18
+ this.result = result;
19
+ this.name = "BudgetExceeded";
20
+ this.budget = result?.blocked_by ?? null;
21
+ }
22
+ }
23
+ /** Minimal HTTP client for the Datagol Lens tracing API. Authenticates with a
24
+ * project-scoped API key only — this SDK is meant for server-side/agent
25
+ * tracing (see app/auth.py's get_trace_actor), not interactive human use, so
26
+ * it deliberately doesn't support the user-JWT login flow the Python SDK's
27
+ * DatagolLensClient predates this one with. */
28
+ export class DatagolLensClient {
29
+ apiUrl;
30
+ apiKey;
31
+ projectId;
32
+ fetchImpl;
33
+ constructor(options = {}) {
34
+ this.apiUrl = (options.apiUrl ?? process.env["DATAGOL_LENS_API_URL"] ?? "http://localhost:8000").replace(/\/+$/, "");
35
+ this.apiKey = options.apiKey ?? process.env["DATAGOL_LENS_API_KEY"] ?? "";
36
+ this.projectId = options.projectId ?? process.env["DATAGOL_LENS_PROJECT_ID"];
37
+ this.fetchImpl = options.fetchImpl ?? fetch;
38
+ if (!this.apiKey) {
39
+ throw new Error("DatagolLensClient requires an API key (a project-scoped `dgl_...` key — mint one via " +
40
+ "POST /api/v1/projects/{project_id}/api-keys). Pass `apiKey` or set DATAGOL_LENS_API_KEY.");
41
+ }
42
+ }
43
+ /** POST one trace (with its spans nested inline). Throws on a non-2xx response. */
44
+ async postTrace(trace, projectId = this.projectId) {
45
+ if (!projectId) {
46
+ throw new Error("postTrace requires a projectId (constructor `projectId` option or a per-call argument).");
47
+ }
48
+ const res = await this.fetchImpl(`${this.apiUrl}/api/v1/projects/${projectId}/traces`, {
49
+ method: "POST",
50
+ headers: {
51
+ "content-type": "application/json",
52
+ authorization: `Bearer ${this.apiKey}`,
53
+ },
54
+ body: JSON.stringify(trace),
55
+ });
56
+ if (!res.ok) {
57
+ const body = await res.text().catch(() => "");
58
+ throw new Error(`DatagolLensClient.postTrace failed: ${res.status} ${res.statusText} ${body}`);
59
+ }
60
+ return (await res.json());
61
+ }
62
+ /** Check a company (and optionally a user and agent) against its Lens budgets
63
+ * before starting a run. Throws LensRequestError on any failure, so the
64
+ * caller decides whether to fail open or closed. */
65
+ async checkBudget(request, projectId = this.projectId, timeoutMs = 2000) {
66
+ const body = {
67
+ company_id: request.companyId,
68
+ user_id: request.userId ?? null,
69
+ agent_key: request.agentKey ?? null,
70
+ ...(request.scopes ? { scopes: request.scopes } : {}),
71
+ };
72
+ return this.requestJson("POST", projectId, "budget/check", timeoutMs, body);
73
+ }
74
+ /** The company ids with budget enforcement on, so callers can skip the check
75
+ * for everyone else. Throws LensRequestError on any failure. */
76
+ async getEnforcedCompanies(projectId = this.projectId, timeoutMs = 2000) {
77
+ return this.requestJson("GET", projectId, "budget/enforced-companies", timeoutMs);
78
+ }
79
+ async requestJson(method, projectId, path, timeoutMs, body) {
80
+ if (!projectId)
81
+ throw new LensRequestError(`${path} requires a projectId`);
82
+ const url = `${this.apiUrl}/api/v1/projects/${projectId}/${path}`;
83
+ let res;
84
+ try {
85
+ res = await this.fetchImpl(url, {
86
+ method,
87
+ headers: { "content-type": "application/json", authorization: `Bearer ${this.apiKey}` },
88
+ body: body === undefined ? undefined : JSON.stringify(body),
89
+ signal: AbortSignal.timeout(timeoutMs),
90
+ });
91
+ }
92
+ catch (err) {
93
+ throw new LensRequestError(`${method} ${path} failed: ${err.message}`);
94
+ }
95
+ if (!res.ok)
96
+ throw new LensRequestError(`${method} ${path} returned ${res.status}`, res.status);
97
+ try {
98
+ return (await res.json());
99
+ }
100
+ catch {
101
+ throw new LensRequestError(`${method} ${path} returned a body that isn't JSON`, res.status);
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,5 @@
1
+ export { BudgetExceeded, DatagolLensClient, LensRequestError } from "./client.js";
2
+ export type { DatagolLensClientOptions } from "./client.js";
3
+ export { TraceBuilder } from "./tracer.js";
4
+ export type { AddSpanOptions, TraceBuilderOptions } from "./tracer.js";
5
+ export type { BudgetCheckRequest, BudgetCheckResponse, BudgetStatus, BudgetWindow, EnforcedCompanies, SpanCreate, SpanOut, SpanType, TraceCreate, TraceOut, TraceStatus, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { BudgetExceeded, DatagolLensClient, LensRequestError } from "./client.js";
2
+ export { TraceBuilder } from "./tracer.js";
@@ -0,0 +1,70 @@
1
+ import type { DatagolLensClient } from "./client.js";
2
+ import type { SpanType, TraceOut, TraceStatus } from "./types.js";
3
+ export interface TraceBuilderOptions {
4
+ name: string;
5
+ input?: Record<string, unknown>;
6
+ tags?: string[] | Record<string, string>;
7
+ metadata?: Record<string, unknown>;
8
+ sessionId?: string;
9
+ externalUserId?: string;
10
+ /** Budget attribution (see TraceCreate.company_id). */
11
+ companyId?: string;
12
+ agentKey?: string;
13
+ agentName?: string;
14
+ }
15
+ export interface AddSpanOptions {
16
+ /** Defaults to a fresh UUID. The returned id is what `parentId` should reference. */
17
+ id?: string;
18
+ name: string;
19
+ spanType?: SpanType;
20
+ input?: Record<string, unknown>;
21
+ output?: Record<string, unknown> | null;
22
+ model?: string | null;
23
+ promptTokens?: number;
24
+ completionTokens?: number;
25
+ /** Defaults to promptTokens + completionTokens when omitted. */
26
+ totalTokens?: number;
27
+ cacheReadTokens?: number;
28
+ cacheWriteTokens?: number;
29
+ reasoningTokens?: number;
30
+ /** True when the cache counts are already inside promptTokens (OpenAI shape). */
31
+ cacheTokensInPrompt?: boolean;
32
+ status?: "ok" | "error";
33
+ error?: string | null;
34
+ cost?: number;
35
+ latencyMs?: number;
36
+ /** Defaults to `now - latencyMs` (spans are usually added right after the
37
+ * operation finishes, so "now" is the end time). */
38
+ startTime?: Date;
39
+ endTime?: Date;
40
+ metadata?: Record<string, unknown>;
41
+ parentId?: string;
42
+ }
43
+ /**
44
+ * Accumulates spans client-side, then ships one Trace (with nested spans) in a
45
+ * single POST — the same "collect, then flush" shape as the Python SDK's
46
+ * TraceContext, adapted for explicit `.flush()` instead of a `with` block
47
+ * (call it from a `finally`/`agent_settled`-style hook so it always runs).
48
+ */
49
+ export declare class TraceBuilder {
50
+ private readonly client;
51
+ private readonly options;
52
+ /** Sent with the trace so a retried flush is idempotent on the server. */
53
+ readonly id: string;
54
+ private readonly startedAt;
55
+ private readonly spans;
56
+ private output;
57
+ private error;
58
+ private model;
59
+ private totalTokens;
60
+ private promptTokens;
61
+ private completionTokens;
62
+ private totalCost;
63
+ private flushed;
64
+ constructor(client: DatagolLensClient, options: TraceBuilderOptions);
65
+ addSpan(span: AddSpanOptions): string;
66
+ setOutput(output: Record<string, unknown>): void;
67
+ setError(error: string): void;
68
+ /** Idempotent — safe to call more than once; only the first call ships anything. */
69
+ flush(status?: TraceStatus, projectId?: string): Promise<TraceOut | undefined>;
70
+ }
package/dist/tracer.js ADDED
@@ -0,0 +1,113 @@
1
+ function newId() {
2
+ const c = globalThis.crypto;
3
+ return c?.randomUUID ? c.randomUUID() : `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}`;
4
+ }
5
+ /**
6
+ * Accumulates spans client-side, then ships one Trace (with nested spans) in a
7
+ * single POST — the same "collect, then flush" shape as the Python SDK's
8
+ * TraceContext, adapted for explicit `.flush()` instead of a `with` block
9
+ * (call it from a `finally`/`agent_settled`-style hook so it always runs).
10
+ */
11
+ export class TraceBuilder {
12
+ client;
13
+ options;
14
+ /** Sent with the trace so a retried flush is idempotent on the server. */
15
+ id = newId();
16
+ startedAt = new Date();
17
+ spans = [];
18
+ output = null;
19
+ error = null;
20
+ model;
21
+ totalTokens = 0;
22
+ promptTokens = 0;
23
+ completionTokens = 0;
24
+ totalCost = 0;
25
+ flushed = false;
26
+ constructor(client, options) {
27
+ this.client = client;
28
+ this.options = options;
29
+ }
30
+ addSpan(span) {
31
+ const id = span.id ?? newId();
32
+ const latencyMs = span.latencyMs;
33
+ const endTime = span.endTime ?? new Date();
34
+ const startTime = span.startTime ?? (latencyMs !== undefined ? new Date(endTime.getTime() - latencyMs) : endTime);
35
+ const totalTokens = span.totalTokens ?? (span.promptTokens ?? 0) + (span.completionTokens ?? 0);
36
+ const cost = span.cost ?? 0;
37
+ this.spans.push({
38
+ id,
39
+ name: span.name,
40
+ span_type: span.spanType ?? "llm",
41
+ input: span.input ?? {},
42
+ output: span.output ?? null,
43
+ model: span.model ?? null,
44
+ total_tokens: totalTokens,
45
+ prompt_tokens: span.promptTokens ?? 0,
46
+ completion_tokens: span.completionTokens ?? 0,
47
+ cache_read_tokens: span.cacheReadTokens ?? 0,
48
+ cache_write_tokens: span.cacheWriteTokens ?? 0,
49
+ reasoning_tokens: span.reasoningTokens ?? 0,
50
+ cache_tokens_in_prompt: span.cacheTokensInPrompt ?? false,
51
+ status: span.status ?? (span.error ? "error" : "ok"),
52
+ error: span.error ?? null,
53
+ cost,
54
+ latency_ms: latencyMs,
55
+ start_time: startTime.toISOString(),
56
+ end_time: endTime.toISOString(),
57
+ metadata: span.metadata ?? {},
58
+ parent_id: span.parentId,
59
+ });
60
+ this.totalTokens += totalTokens;
61
+ this.promptTokens += span.promptTokens ?? 0;
62
+ this.completionTokens += span.completionTokens ?? 0;
63
+ this.totalCost += cost;
64
+ if (span.model)
65
+ this.model = span.model;
66
+ return id;
67
+ }
68
+ setOutput(output) {
69
+ this.output = output;
70
+ }
71
+ setError(error) {
72
+ this.error = error;
73
+ }
74
+ /** Idempotent — safe to call more than once; only the first call ships anything. */
75
+ async flush(status, projectId) {
76
+ if (this.flushed)
77
+ return undefined;
78
+ this.flushed = true;
79
+ const now = new Date();
80
+ const trace = {
81
+ id: this.id,
82
+ name: this.options.name,
83
+ input: this.options.input ?? {},
84
+ output: this.output,
85
+ error: this.error,
86
+ start_time: this.startedAt.toISOString(),
87
+ end_time: now.toISOString(),
88
+ latency_ms: now.getTime() - this.startedAt.getTime(),
89
+ total_tokens: this.totalTokens,
90
+ prompt_tokens: this.promptTokens,
91
+ completion_tokens: this.completionTokens,
92
+ total_cost: this.totalCost,
93
+ model: this.model,
94
+ status: status ?? (this.error ? "error" : "success"),
95
+ tags: this.options.tags ?? [],
96
+ metadata: this.options.metadata ?? {},
97
+ external_user_id: this.options.externalUserId,
98
+ session_id: this.options.sessionId,
99
+ company_id: this.options.companyId,
100
+ agent_key: this.options.agentKey,
101
+ agent_name: this.options.agentName,
102
+ spans: this.spans,
103
+ };
104
+ try {
105
+ return await this.client.postTrace(trace, projectId);
106
+ }
107
+ catch {
108
+ // Never let a tracing failure crash the caller — matches the Python SDK's
109
+ // TraceContext._flush, which also swallows post_trace errors.
110
+ return undefined;
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Wire types for the Datagol Lens tracing API. Mirror backend/app/schemas.py's
3
+ * SpanCreate/SpanOut/TraceCreate/TraceOut — keep these two in sync by hand (no
4
+ * shared codegen between the Python backend and this package yet).
5
+ */
6
+ export type SpanType = "llm" | "tool" | "agent" | "retriever" | "workflow" | "guardrail" | "routing_attempt";
7
+ export type TraceStatus = "running" | "success" | "error";
8
+ export interface SpanCreate {
9
+ /** Optional client id so `parent_id` can reference a sibling span. */
10
+ id?: string;
11
+ name: string;
12
+ span_type?: SpanType;
13
+ input?: Record<string, unknown>;
14
+ output?: Record<string, unknown> | null;
15
+ /** ISO 8601. Omit to let the server default to "now". */
16
+ start_time?: string;
17
+ end_time?: string;
18
+ latency_ms?: number;
19
+ total_tokens?: number;
20
+ prompt_tokens?: number;
21
+ completion_tokens?: number;
22
+ cache_read_tokens?: number;
23
+ cache_write_tokens?: number;
24
+ reasoning_tokens?: number;
25
+ /** True when the cache counts are already inside prompt_tokens (OpenAI shape). */
26
+ cache_tokens_in_prompt?: boolean;
27
+ cost?: number;
28
+ model?: string | null;
29
+ status?: "ok" | "error";
30
+ error?: string | null;
31
+ recovered?: boolean;
32
+ metadata?: Record<string, unknown>;
33
+ parent_id?: string | null;
34
+ redacted?: boolean;
35
+ redacted_fields?: number;
36
+ }
37
+ export interface SpanOut {
38
+ id: string;
39
+ trace_id: string;
40
+ parent_id: string | null;
41
+ name: string;
42
+ span_type: SpanType;
43
+ input: unknown;
44
+ output: unknown;
45
+ start_time: string;
46
+ end_time: string | null;
47
+ latency_ms: number | null;
48
+ total_tokens: number;
49
+ cost: number;
50
+ model: string | null;
51
+ redacted: boolean;
52
+ redacted_fields: number;
53
+ }
54
+ export interface TraceCreate {
55
+ /** Client id; re-posting it is idempotent on the server. */
56
+ id?: string;
57
+ name: string;
58
+ input?: Record<string, unknown>;
59
+ output?: Record<string, unknown> | null;
60
+ error?: string | null;
61
+ start_time?: string;
62
+ end_time?: string;
63
+ latency_ms?: number;
64
+ total_tokens?: number;
65
+ prompt_tokens?: number;
66
+ completion_tokens?: number;
67
+ total_cost?: number;
68
+ model?: string | null;
69
+ status?: TraceStatus;
70
+ /** Flat labels (["chat", "production"]) or key-value tags (e.g.
71
+ * { user_id, user_email }) — Datagol Lens's grouped trace view groups by
72
+ * a tag key, so key-value tags are what make a trace groupable. */
73
+ tags?: string[] | Record<string, string>;
74
+ metadata?: Record<string, unknown>;
75
+ /** The end-user of the *calling application* (e.g. the DataGOL platform user
76
+ * running the agent) — not the tracing credential. Use this + session_id to
77
+ * answer "who ran what" for a given trace. */
78
+ external_user_id?: string | null;
79
+ session_id?: string | null;
80
+ /** Budget attribution: the calling platform's company id (e.g. a DataGOL
81
+ * companyId), and the agent's stable key and display name. Lens counts the
82
+ * trace's cost against that company, agent and external_user_id. */
83
+ company_id?: string | null;
84
+ agent_key?: string | null;
85
+ agent_name?: string | null;
86
+ virtual_key_id?: string | null;
87
+ redacted?: boolean;
88
+ redacted_fields?: number;
89
+ blocked?: boolean;
90
+ guardrail_violations?: Record<string, unknown>[];
91
+ spans?: SpanCreate[];
92
+ }
93
+ export interface TraceOut {
94
+ id: string;
95
+ project_id: string;
96
+ name: string;
97
+ input: unknown;
98
+ output: unknown;
99
+ error: string | null;
100
+ start_time: string;
101
+ end_time: string | null;
102
+ latency_ms: number | null;
103
+ total_tokens: number;
104
+ prompt_tokens: number;
105
+ completion_tokens: number;
106
+ total_cost: number;
107
+ model: string | null;
108
+ status: TraceStatus;
109
+ tags: unknown;
110
+ external_user_id: string | null;
111
+ session_id: string | null;
112
+ virtual_key_id: string | null;
113
+ redacted: boolean;
114
+ redacted_fields: number;
115
+ blocked: boolean;
116
+ guardrail_violations: unknown;
117
+ created_at: string;
118
+ spans: SpanOut[];
119
+ }
120
+ export type BudgetWindow = "hourly" | "daily" | "monthly";
121
+ /** POST /api/v1/projects/{project_id}/budget/check body. */
122
+ export interface BudgetCheckRequest {
123
+ company_id: string;
124
+ user_id?: string | null;
125
+ agent_key?: string | null;
126
+ /** Values for any other scope type, by its id (e.g. { model: "gpt-4o" }). */
127
+ scopes?: Record<string, string>;
128
+ }
129
+ export interface BudgetStatus {
130
+ budget_id: string;
131
+ scope_type: string;
132
+ scope_external_id: string | null;
133
+ window: BudgetWindow;
134
+ limit_usd: number;
135
+ spent_usd: number;
136
+ warn_at_pct: number;
137
+ /** ISO 8601 UTC. */
138
+ resets_at: string;
139
+ }
140
+ export interface BudgetCheckResponse {
141
+ allowed: boolean;
142
+ decision: "allow" | "warn" | "block";
143
+ blocked_by: BudgetStatus | null;
144
+ warnings: BudgetStatus[];
145
+ }
146
+ export interface EnforcedCompanies {
147
+ company_ids: string[];
148
+ version: string;
149
+ }
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Wire types for the Datagol Lens tracing API. Mirror backend/app/schemas.py's
3
+ * SpanCreate/SpanOut/TraceCreate/TraceOut — keep these two in sync by hand (no
4
+ * shared codegen between the Python backend and this package yet).
5
+ */
6
+ export {};
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@datagol/lens-js",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.1",
4
+ "description": "Node/TypeScript SDK for DataGOL Lens: trace LLM calls and agent tool spans, attribute cost, check budgets.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "license": "MIT",
18
+ "homepage": "https://datagol.ai",
19
+ "keywords": [
20
+ "llm",
21
+ "observability",
22
+ "tracing",
23
+ "datagol"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "test": "npm run build && node --test test/client.test.mjs",
28
+ "prepublishOnly": "npm test"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.5.0",
35
+ "@types/node": "^20.0.0"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ }
40
+ }