@rawdash/connector-langsmith 0.1.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/dist/index.js ADDED
@@ -0,0 +1,613 @@
1
+ // ../../connector-shared/dist/index.js
2
+ var HTTP_CLIENT_VERSION = "0.0.0";
3
+ var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
4
+ function connectorUserAgent(connectorId) {
5
+ return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
6
+ }
7
+ function parseEpoch(value, unit) {
8
+ if (value === null || value === void 0) {
9
+ return null;
10
+ }
11
+ if (unit === "iso") {
12
+ if (typeof value !== "string") {
13
+ return null;
14
+ }
15
+ const ms = new Date(value).getTime();
16
+ return Number.isFinite(ms) ? ms : null;
17
+ }
18
+ if (typeof value === "string" && value.trim() === "") {
19
+ return null;
20
+ }
21
+ const n = typeof value === "number" ? value : Number(value);
22
+ if (!Number.isFinite(n)) {
23
+ return null;
24
+ }
25
+ const result = unit === "s" ? n * 1e3 : n;
26
+ return Number.isFinite(result) ? result : null;
27
+ }
28
+
29
+ // src/langsmith.ts
30
+ import {
31
+ BaseConnector,
32
+ defineConfigFields,
33
+ defineConnectorDoc,
34
+ defineResources,
35
+ makeChunkedCursorGuard,
36
+ paginateChunked,
37
+ schemasFromResources,
38
+ selectActivePhases
39
+ } from "@rawdash/core";
40
+ import { z } from "zod";
41
+ var configFields = defineConfigFields(
42
+ z.object({
43
+ apiKey: z.object({ $secret: z.string() }).meta({
44
+ label: "API key",
45
+ description: "LangSmith API key with read access to the tenant. Create one in LangSmith -> Settings -> API Keys.",
46
+ placeholder: "lsv2_pt_...",
47
+ secret: true
48
+ }),
49
+ endpoint: z.string().trim().regex(
50
+ /^https?:\/\/[^\s/]+$/,
51
+ "Use a base URL with protocol and no trailing slash, e.g. https://api.smith.langchain.com"
52
+ ).default("https://api.smith.langchain.com").meta({
53
+ label: "Endpoint",
54
+ description: "LangSmith API base URL. Defaults to https://api.smith.langchain.com (US cloud). Use https://eu.api.smith.langchain.com for the EU region or your self-hosted origin. No trailing slash.",
55
+ placeholder: "https://api.smith.langchain.com"
56
+ }),
57
+ lookbackDays: z.number().int().positive().max(365).optional().meta({
58
+ label: "Lookback days (full sync)",
59
+ description: "How many calendar days of history to backfill on a full sync. Defaults to 30.",
60
+ placeholder: "30"
61
+ }),
62
+ resources: z.array(z.enum(["runs", "runs_per_day", "feedback"])).nonempty().optional().meta({
63
+ label: "Resources",
64
+ description: "Which LangSmith resources to sync. Omit to sync all of them. Both `runs` and `runs_per_day` are produced from the same upstream query, so listing either pulls runs."
65
+ })
66
+ })
67
+ );
68
+ var doc = defineConnectorDoc({
69
+ displayName: "LangSmith",
70
+ category: "engineering",
71
+ brandColor: "#7FC8FF",
72
+ tagline: "Sync LangChain runs, daily run rollups (count, tokens, cost, latency), and feedback scores from a LangSmith tenant.",
73
+ vendor: {
74
+ name: "LangSmith",
75
+ domain: "langchain.com",
76
+ apiDocs: "https://docs.smith.langchain.com/reference",
77
+ website: "https://smith.langchain.com"
78
+ },
79
+ auth: {
80
+ summary: "A LangSmith API key with read access is required. The key is sent as the `x-api-key` header on every request.",
81
+ setup: [
82
+ "Open LangSmith -> Settings -> API Keys and create a Personal Access Token (or Service key) with read access.",
83
+ "Copy the key (it is shown once).",
84
+ "Set `endpoint` to your LangSmith region: https://api.smith.langchain.com (US, default), https://eu.api.smith.langchain.com (EU), or your self-hosted origin (no trailing slash).",
85
+ 'Store the API key as a secret and reference it from config as `apiKey: secret("LANGSMITH_API_KEY")`.'
86
+ ]
87
+ },
88
+ rateLimit: "LangSmith applies per-tenant rate limits and returns 429 with Retry-After on overrun; the shared HTTP client honors that header.",
89
+ limitations: [
90
+ "Run input/output payloads are not synced - only the run envelope plus aggregated cost, token, and latency.",
91
+ "Datasets, examples, prompts, and evaluation runs are out of scope for the initial release.",
92
+ "Feedback non-numeric values (string, boolean, JSON) are still counted but do not contribute to the score sample."
93
+ ]
94
+ });
95
+ var langsmithCredentials = {
96
+ apiKey: {
97
+ description: "LangSmith API key",
98
+ auth: "required"
99
+ }
100
+ };
101
+ var PHASE_ORDER = ["runs", "feedback"];
102
+ var isLangSmithSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
103
+ var RUNS_PAGE_SIZE = 100;
104
+ var FEEDBACK_PAGE_SIZE = 100;
105
+ var CHUNK_BUDGET_MS = 25e3;
106
+ var DEFAULT_LOOKBACK_DAYS = 30;
107
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
108
+ var RUN_ENTITY = "langsmith_run";
109
+ var RUNS_PER_DAY_METRIC = "langsmith_runs_per_day";
110
+ var FEEDBACK_METRIC = "langsmith_feedback";
111
+ var runSchema = z.object({
112
+ id: z.string().min(1),
113
+ name: z.string().nullish(),
114
+ run_type: z.string().nullish(),
115
+ status: z.string().nullish(),
116
+ session_id: z.string().nullish(),
117
+ session_name: z.string().nullish(),
118
+ parent_run_id: z.string().nullish(),
119
+ start_time: z.string().nullish(),
120
+ end_time: z.string().nullish(),
121
+ error: z.string().nullish(),
122
+ total_tokens: z.number().nullish(),
123
+ prompt_tokens: z.number().nullish(),
124
+ completion_tokens: z.number().nullish(),
125
+ total_cost: z.number().nullish(),
126
+ prompt_cost: z.number().nullish(),
127
+ completion_cost: z.number().nullish(),
128
+ latency: z.number().nullish()
129
+ });
130
+ var runsQueryResponseSchema = z.object({
131
+ runs: z.array(runSchema),
132
+ cursors: z.object({
133
+ next: z.string().nullish()
134
+ }).nullish()
135
+ });
136
+ var feedbackSchema = z.object({
137
+ id: z.string().min(1),
138
+ run_id: z.string().nullish(),
139
+ session_id: z.string().nullish(),
140
+ key: z.string().min(1),
141
+ score: z.number().nullish(),
142
+ comment: z.string().nullish(),
143
+ created_at: z.string().nullish(),
144
+ modified_at: z.string().nullish()
145
+ });
146
+ var feedbackListResponseSchema = z.array(feedbackSchema);
147
+ var langsmithResources = defineResources({
148
+ langsmith_run: {
149
+ shape: "entity",
150
+ filterable: [
151
+ {
152
+ field: "sessionId",
153
+ ops: ["eq"]
154
+ },
155
+ {
156
+ field: "runType",
157
+ ops: ["eq"],
158
+ values: ["chain", "tool", "llm", "embedding", "parser", "retriever"]
159
+ },
160
+ {
161
+ field: "status",
162
+ ops: ["eq"],
163
+ values: ["success", "error", "pending"]
164
+ }
165
+ ],
166
+ description: "LangSmith run rows, keyed by id, with name, owning session/project, parent run, run type, status, start/end timestamps, total/prompt/completion tokens, total/prompt/completion cost in USD, and end-to-end latency in milliseconds.",
167
+ endpoint: "POST /api/v1/runs/query",
168
+ notes: "Runs upsert by id on every run. Trace input/output payloads are not stored.",
169
+ fields: [
170
+ { name: "name", description: "Run name set by the SDK." },
171
+ {
172
+ name: "runType",
173
+ description: "Run type (chain, tool, llm, embedding, parser, retriever)."
174
+ },
175
+ {
176
+ name: "status",
177
+ description: "Run status (success, error, pending)."
178
+ },
179
+ {
180
+ name: "sessionId",
181
+ description: "Owning session (project) id, if any."
182
+ },
183
+ {
184
+ name: "sessionName",
185
+ description: "Owning session (project) name, if any."
186
+ },
187
+ {
188
+ name: "parentRunId",
189
+ description: "Parent run id for nested runs."
190
+ },
191
+ {
192
+ name: "startTime",
193
+ description: "ISO timestamp of run start."
194
+ },
195
+ {
196
+ name: "endTime",
197
+ description: "ISO timestamp of run end, if completed."
198
+ },
199
+ {
200
+ name: "totalTokens",
201
+ description: "Aggregate token count across the run."
202
+ },
203
+ {
204
+ name: "promptTokens",
205
+ description: "Prompt token count for the run."
206
+ },
207
+ {
208
+ name: "completionTokens",
209
+ description: "Completion token count for the run."
210
+ },
211
+ {
212
+ name: "totalCost",
213
+ description: "Aggregate run cost in USD."
214
+ },
215
+ {
216
+ name: "latencyMs",
217
+ description: "End-to-end latency in milliseconds."
218
+ },
219
+ {
220
+ name: "error",
221
+ description: "Error message if the run failed."
222
+ }
223
+ ],
224
+ responses: { runs: runsQueryResponseSchema }
225
+ },
226
+ langsmith_runs_per_day: {
227
+ shape: "metric",
228
+ description: "Per-run samples used to roll runs up to daily totals at query time. One sample is emitted per run at its start timestamp, tagged with project, run type, and status. The sample value is 1 (so summing yields the run count); token, cost, and latency are exposed as additional attribute fields.",
229
+ endpoint: "POST /api/v1/runs/query",
230
+ unit: "runs",
231
+ granularity: "Per-run (query-time rollup)",
232
+ notes: "No server-side aggregation - widgets group by day, project, or run type to produce the rollup.",
233
+ dimensions: [
234
+ {
235
+ name: "sessionId",
236
+ description: "Owning session (project) id, if any."
237
+ },
238
+ {
239
+ name: "sessionName",
240
+ description: "Owning session (project) name, if any."
241
+ },
242
+ {
243
+ name: "runType",
244
+ description: "Run type (chain, tool, llm, embedding, ...)."
245
+ },
246
+ {
247
+ name: "status",
248
+ description: "Run status (success, error, pending)."
249
+ },
250
+ {
251
+ name: "count",
252
+ description: "One per run; sum to get run counts. Also exposed as the sample value."
253
+ },
254
+ {
255
+ name: "totalTokens",
256
+ description: "Total tokens consumed by the run."
257
+ },
258
+ {
259
+ name: "promptTokens",
260
+ description: "Prompt tokens consumed by the run."
261
+ },
262
+ {
263
+ name: "completionTokens",
264
+ description: "Completion tokens produced by the run."
265
+ },
266
+ {
267
+ name: "costUsd",
268
+ description: "Aggregate run cost in USD."
269
+ },
270
+ {
271
+ name: "latencyMs",
272
+ description: "End-to-end run latency in milliseconds."
273
+ }
274
+ ],
275
+ responses: {}
276
+ },
277
+ langsmith_feedback: {
278
+ shape: "metric",
279
+ description: "Feedback rows from LangSmith, one sample per feedback row at its created_at timestamp. The sample value is the numeric score (zero for non-numeric feedback) and the attribute `count` is 1 so summing yields feedback counts per (day, project, key).",
280
+ endpoint: "GET /api/v1/feedback",
281
+ unit: "score",
282
+ granularity: "Per-feedback (query-time rollup)",
283
+ notes: "Non-numeric feedback (string, boolean, JSON value) is still emitted but with score 0; use `count` to count rows and average `score` for numeric trends.",
284
+ dimensions: [
285
+ {
286
+ name: "key",
287
+ description: "Feedback key as set by the SDK or annotator."
288
+ },
289
+ {
290
+ name: "sessionId",
291
+ description: "Owning session (project) id, if known."
292
+ },
293
+ {
294
+ name: "runId",
295
+ description: "Run the feedback is attached to, if any."
296
+ },
297
+ {
298
+ name: "count",
299
+ description: "One per feedback row; sum to count rows."
300
+ },
301
+ {
302
+ name: "score",
303
+ description: "Numeric score, or 0 for non-numeric feedback. Also exposed as the sample value."
304
+ },
305
+ {
306
+ name: "hasNumericScore",
307
+ description: "1 if the feedback row had a numeric score, 0 otherwise; sum these to compute strict numeric counts."
308
+ }
309
+ ],
310
+ responses: { feedback: feedbackListResponseSchema }
311
+ }
312
+ });
313
+ var id = "langsmith";
314
+ var LangSmithConnector = class _LangSmithConnector extends BaseConnector {
315
+ static id = id;
316
+ static resources = langsmithResources;
317
+ static schemas = schemasFromResources(langsmithResources);
318
+ static create(input, ctx) {
319
+ const parsed = configFields.parse(input);
320
+ return new _LangSmithConnector(
321
+ {
322
+ endpoint: parsed.endpoint,
323
+ lookbackDays: parsed.lookbackDays,
324
+ resources: parsed.resources
325
+ },
326
+ { apiKey: parsed.apiKey },
327
+ ctx
328
+ );
329
+ }
330
+ id = id;
331
+ credentials = langsmithCredentials;
332
+ get baseUrl() {
333
+ return this.settings.endpoint.replace(/\/+$/, "");
334
+ }
335
+ buildHeaders(extra) {
336
+ return {
337
+ "x-api-key": this.creds.apiKey,
338
+ Accept: "application/json",
339
+ "User-Agent": connectorUserAgent("langsmith"),
340
+ ...extra ?? {}
341
+ };
342
+ }
343
+ windowStartIso(options) {
344
+ const lookbackDays = this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;
345
+ const now = Date.now();
346
+ let startMs = now - lookbackDays * MS_PER_DAY;
347
+ if (options.since) {
348
+ const sinceMs = new Date(options.since).getTime();
349
+ if (Number.isFinite(sinceMs) && sinceMs < startMs) {
350
+ startMs = sinceMs;
351
+ }
352
+ }
353
+ return new Date(startMs).toISOString();
354
+ }
355
+ wantsRunEntity() {
356
+ return resourceIsActive(this.settings.resources, "runs");
357
+ }
358
+ wantsRunsPerDay() {
359
+ return resourceIsActive(this.settings.resources, "runs_per_day");
360
+ }
361
+ wantsFeedback() {
362
+ return resourceIsActive(this.settings.resources, "feedback");
363
+ }
364
+ async fetchRunsPage(options, page, signal) {
365
+ const offset = parseOffset(page);
366
+ const body = {
367
+ start_time: this.windowStartIso(options),
368
+ limit: RUNS_PAGE_SIZE,
369
+ offset,
370
+ order: "asc"
371
+ };
372
+ const res = await this.post(
373
+ `${this.baseUrl}/api/v1/runs/query`,
374
+ {
375
+ resource: "runs",
376
+ headers: this.buildHeaders({ "Content-Type": "application/json" }),
377
+ body: JSON.stringify(body),
378
+ signal
379
+ }
380
+ );
381
+ const runs = res.body.runs ?? [];
382
+ const sinceMs = options.since ? new Date(options.since).getTime() : null;
383
+ const allBeforeSince = sinceMs !== null && runs.length > 0 && runs.every((r) => {
384
+ const ts = parseEpoch(r.start_time ?? null, "iso");
385
+ return ts !== null && ts < sinceMs;
386
+ });
387
+ const explicitNext = res.body.cursors?.next ?? null;
388
+ let next;
389
+ if (allBeforeSince) {
390
+ next = null;
391
+ } else if (explicitNext) {
392
+ next = explicitNext;
393
+ } else if (runs.length === RUNS_PAGE_SIZE) {
394
+ next = String(offset + runs.length);
395
+ } else {
396
+ next = null;
397
+ }
398
+ return { items: runs, next };
399
+ }
400
+ async writeRunsBatch(storage, runs) {
401
+ const wantEntity = this.wantsRunEntity();
402
+ const wantMetric = this.wantsRunsPerDay();
403
+ if (!wantEntity && !wantMetric) {
404
+ return;
405
+ }
406
+ for (const run of runs) {
407
+ const startMs = parseEpoch(run.start_time ?? null, "iso");
408
+ const endMs = parseEpoch(run.end_time ?? null, "iso");
409
+ const latencyMs = computeLatencyMs(run, startMs, endMs);
410
+ if (wantEntity) {
411
+ const updatedAt = endMs ?? startMs ?? 0;
412
+ await storage.entity({
413
+ type: RUN_ENTITY,
414
+ id: run.id,
415
+ attributes: {
416
+ name: run.name ?? null,
417
+ runType: run.run_type ?? null,
418
+ status: run.status ?? null,
419
+ sessionId: run.session_id ?? null,
420
+ sessionName: run.session_name ?? null,
421
+ parentRunId: run.parent_run_id ?? null,
422
+ startTime: run.start_time ?? null,
423
+ endTime: run.end_time ?? null,
424
+ totalTokens: finiteNumberOrNull(run.total_tokens),
425
+ promptTokens: finiteNumberOrNull(run.prompt_tokens),
426
+ completionTokens: finiteNumberOrNull(run.completion_tokens),
427
+ totalCost: finiteNumberOrNull(run.total_cost),
428
+ promptCost: finiteNumberOrNull(run.prompt_cost),
429
+ completionCost: finiteNumberOrNull(run.completion_cost),
430
+ latencyMs,
431
+ error: run.error ?? null
432
+ },
433
+ updated_at: updatedAt
434
+ });
435
+ }
436
+ if (wantMetric && startMs !== null) {
437
+ await storage.metric({
438
+ name: RUNS_PER_DAY_METRIC,
439
+ ts: startMs,
440
+ value: 1,
441
+ attributes: {
442
+ sessionId: run.session_id ?? null,
443
+ sessionName: run.session_name ?? null,
444
+ runType: run.run_type ?? null,
445
+ status: run.status ?? null,
446
+ count: 1,
447
+ totalTokens: finiteNumber(run.total_tokens),
448
+ promptTokens: finiteNumber(run.prompt_tokens),
449
+ completionTokens: finiteNumber(run.completion_tokens),
450
+ costUsd: finiteNumber(run.total_cost),
451
+ latencyMs: latencyMs ?? 0
452
+ }
453
+ });
454
+ }
455
+ }
456
+ }
457
+ async fetchFeedbackPage(options, page, signal) {
458
+ const offset = parseOffset(page);
459
+ const url = new URL(`${this.baseUrl}/api/v1/feedback`);
460
+ url.searchParams.set("limit", String(FEEDBACK_PAGE_SIZE));
461
+ url.searchParams.set("offset", String(offset));
462
+ url.searchParams.set("start_time", this.windowStartIso(options));
463
+ const res = await this.get(url.toString(), {
464
+ resource: "feedback",
465
+ headers: this.buildHeaders(),
466
+ signal
467
+ });
468
+ const rows = Array.isArray(res.body) ? res.body : [];
469
+ const sinceMs = options.since ? new Date(options.since).getTime() : null;
470
+ const allBeforeSince = sinceMs !== null && rows.length > 0 && rows.every((f) => {
471
+ const ts = parseEpoch(f.created_at ?? null, "iso");
472
+ return ts !== null && ts < sinceMs;
473
+ });
474
+ const next = !allBeforeSince && rows.length === FEEDBACK_PAGE_SIZE ? String(offset + rows.length) : null;
475
+ return { items: rows, next };
476
+ }
477
+ async writeFeedbackBatch(storage, rows) {
478
+ for (const row of rows) {
479
+ const ts = parseEpoch(row.created_at ?? null, "iso");
480
+ if (ts === null) {
481
+ continue;
482
+ }
483
+ const numeric = typeof row.score === "number" && Number.isFinite(row.score);
484
+ const score = numeric ? row.score : 0;
485
+ await storage.metric({
486
+ name: FEEDBACK_METRIC,
487
+ ts,
488
+ value: score,
489
+ attributes: {
490
+ key: row.key,
491
+ sessionId: row.session_id ?? null,
492
+ runId: row.run_id ?? null,
493
+ count: 1,
494
+ score,
495
+ hasNumericScore: numeric ? 1 : 0
496
+ }
497
+ });
498
+ }
499
+ }
500
+ activePhases() {
501
+ return selectActivePhases(
502
+ (r) => {
503
+ switch (r) {
504
+ case "runs":
505
+ case "runs_per_day":
506
+ return "runs";
507
+ case "feedback":
508
+ return "feedback";
509
+ }
510
+ },
511
+ PHASE_ORDER,
512
+ this.settings.resources
513
+ );
514
+ }
515
+ async clearScopeOnFirstPage(storage, phase, isFull) {
516
+ switch (phase) {
517
+ case "runs":
518
+ if (isFull && this.wantsRunEntity()) {
519
+ await storage.entities([], { types: [RUN_ENTITY] });
520
+ }
521
+ if (this.wantsRunsPerDay()) {
522
+ await storage.metrics([], { names: [RUNS_PER_DAY_METRIC] });
523
+ }
524
+ return;
525
+ case "feedback":
526
+ await storage.metrics([], { names: [FEEDBACK_METRIC] });
527
+ return;
528
+ }
529
+ }
530
+ async sync(options, storage, signal) {
531
+ const cursor = isLangSmithSyncCursor(options.cursor) ? options.cursor : void 0;
532
+ const isFull = options.mode === "full";
533
+ const phases = this.activePhases();
534
+ return paginateChunked({
535
+ phases,
536
+ cursor,
537
+ signal,
538
+ logger: this.logger,
539
+ maxChunkMs: CHUNK_BUDGET_MS,
540
+ fetchPage: async (phase, page, sig) => {
541
+ switch (phase) {
542
+ case "runs":
543
+ return this.fetchRunsPage(options, page, sig);
544
+ case "feedback":
545
+ return this.fetchFeedbackPage(options, page, sig);
546
+ }
547
+ },
548
+ writeBatch: async (phase, items, page) => {
549
+ if (page === null) {
550
+ await this.clearScopeOnFirstPage(storage, phase, isFull);
551
+ }
552
+ switch (phase) {
553
+ case "runs":
554
+ await this.writeRunsBatch(storage, items);
555
+ return;
556
+ case "feedback":
557
+ if (this.wantsFeedback()) {
558
+ await this.writeFeedbackBatch(storage, items);
559
+ }
560
+ return;
561
+ }
562
+ }
563
+ });
564
+ }
565
+ };
566
+ function parseOffset(page) {
567
+ if (page === null) {
568
+ return 0;
569
+ }
570
+ const n = Number(page);
571
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
572
+ }
573
+ function finiteNumber(value, fallback = 0) {
574
+ if (value === null || value === void 0) {
575
+ return fallback;
576
+ }
577
+ const n = typeof value === "number" ? value : Number(value);
578
+ return Number.isFinite(n) ? n : fallback;
579
+ }
580
+ function finiteNumberOrNull(value) {
581
+ if (value === null || value === void 0 || value === "") {
582
+ return null;
583
+ }
584
+ const n = typeof value === "number" ? value : Number(value);
585
+ return Number.isFinite(n) ? n : null;
586
+ }
587
+ function computeLatencyMs(run, startMs, endMs) {
588
+ if (typeof run.latency === "number" && Number.isFinite(run.latency)) {
589
+ return run.latency;
590
+ }
591
+ if (startMs !== null && endMs !== null && endMs >= startMs) {
592
+ return endMs - startMs;
593
+ }
594
+ return null;
595
+ }
596
+ function resourceIsActive(allowlist, resource) {
597
+ if (!allowlist || allowlist.length === 0) {
598
+ return true;
599
+ }
600
+ return allowlist.includes(resource);
601
+ }
602
+
603
+ // src/index.ts
604
+ var index_default = LangSmithConnector;
605
+ export {
606
+ LangSmithConnector,
607
+ configFields,
608
+ index_default as default,
609
+ doc,
610
+ id,
611
+ langsmithResources as resources
612
+ };
613
+ //# sourceMappingURL=index.js.map