@bitfab/sdk 0.38.10 → 0.40.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/node.cjs CHANGED
@@ -88,7 +88,7 @@ var __version__, __packageName__;
88
88
  var init_version_generated = __esm({
89
89
  "src/version.generated.ts"() {
90
90
  "use strict";
91
- __version__ = "0.38.10";
91
+ __version__ = "0.40.0";
92
92
  __packageName__ = "@bitfab/sdk";
93
93
  }
94
94
  });
@@ -1205,6 +1205,16 @@ var init_transport = __esm({
1205
1205
  });
1206
1206
 
1207
1207
  // src/http.ts
1208
+ var http_exports = {};
1209
+ __export(http_exports, {
1210
+ BitfabError: () => BitfabError,
1211
+ HttpClient: () => HttpClient,
1212
+ awaitOnExit: () => awaitOnExit,
1213
+ awaitPendingRequests: () => awaitPendingRequests,
1214
+ flushTraces: () => flushTraces,
1215
+ parseRetryAfterMs: () => parseRetryAfterMs,
1216
+ serializePayloadBody: () => serializePayloadBody
1217
+ });
1208
1218
  function awaitOnExit(promise) {
1209
1219
  pendingTracePromises.add(promise);
1210
1220
  void promise.finally(() => {
@@ -1729,6 +1739,10 @@ var init_http = __esm({
1729
1739
  const response = await this.get(endpoint);
1730
1740
  return response.span;
1731
1741
  }
1742
+ /**
1743
+ * GET a JSON endpoint on the service with the client's API key. Throws a
1744
+ * `BitfabError` carrying the status text for any non-2xx response.
1745
+ */
1732
1746
  async get(endpoint) {
1733
1747
  const url = `${this.serviceUrl}${endpoint}`;
1734
1748
  const controller = new AbortController();
@@ -1742,7 +1756,10 @@ var init_http = __esm({
1742
1756
  if (!response.ok) {
1743
1757
  const errorText = await response.text();
1744
1758
  throw new BitfabError(
1745
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
1759
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`,
1760
+ void 0,
1761
+ response.status,
1762
+ parseRetryAfterMs(readHeader(response, "retry-after"))
1746
1763
  );
1747
1764
  }
1748
1765
  return await response.json();
@@ -2549,7 +2566,7 @@ function buildMockTree(rootNode) {
2549
2566
  }
2550
2567
  return { spans };
2551
2568
  }
2552
- async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, resolvedOverrides, replayedTraceId, includeDbBranchLease, dbBranchSettings, adaptInputs) {
2569
+ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, resolvedOverrides, replayedTraceId, includeDbBranchLease, dbBranchSettings, adaptInputs, dryRun) {
2553
2570
  let lease = includeDbBranchLease ? serverItem.dbBranchLease : void 0;
2554
2571
  let leaseError = includeDbBranchLease ? serverItem.dbBranchLeaseError : void 0;
2555
2572
  let dbSnapshotRef = serverItem.dbSnapshotRef;
@@ -2644,6 +2661,32 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
2644
2661
  }
2645
2662
  return pending;
2646
2663
  } : void 0;
2664
+ if (dryRun) {
2665
+ return {
2666
+ traceId: null,
2667
+ originalTraceId,
2668
+ originalSpanId,
2669
+ sourceTraceId: originalTraceId,
2670
+ sourceSpanId: originalSpanId,
2671
+ input: inputs,
2672
+ result: void 0,
2673
+ originalOutput,
2674
+ ...serverItem.ingestionType && {
2675
+ ingestionType: serverItem.ingestionType
2676
+ },
2677
+ error: null,
2678
+ traceError: null,
2679
+ replayError: null,
2680
+ durationMs: null,
2681
+ originalDurationMs: serverItem.originalDurationMs ?? serverItem.durationMs ?? null,
2682
+ originalTokens: serverItem.originalTokens ?? serverItem.tokens ?? null,
2683
+ originalModel: serverItem.originalModel ?? serverItem.model ?? null,
2684
+ tokens: null,
2685
+ model: serverItem.originalModel ?? serverItem.model ?? null,
2686
+ dbSnapshotRef: dbSnapshotRef ?? null,
2687
+ dbBranchTimings: dbBranchTimings ?? null
2688
+ };
2689
+ }
2647
2690
  try {
2648
2691
  replayStarted = performance.now();
2649
2692
  const maybePromise = runWithReplayContext(
@@ -2707,6 +2750,9 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
2707
2750
  input: inputs,
2708
2751
  result,
2709
2752
  originalOutput,
2753
+ ...serverItem.ingestionType && {
2754
+ ingestionType: serverItem.ingestionType
2755
+ },
2710
2756
  error,
2711
2757
  traceError,
2712
2758
  replayError,
@@ -2854,13 +2900,23 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2854
2900
  options?.name,
2855
2901
  codeChangeDescription,
2856
2902
  codeChangeFiles,
2857
- dbBranchEnabled(options?.dbBranch),
2858
- // includeDbBranchLease
2903
+ // A dry run executes nothing, so a branch would be provisioned (and billed)
2904
+ // for code that never runs, and a seeded source's refusal would fail the item
2905
+ // before it could report its resolved inputs.
2906
+ dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true,
2859
2907
  options?.experimentGroupId,
2860
2908
  options?.datasetId,
2861
2909
  options?.graderIds,
2862
2910
  resolveDbBranchSettings(options?.dbBranch)
2863
2911
  );
2912
+ if (serverItems.length === 0) {
2913
+ try {
2914
+ console.warn(
2915
+ `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
2916
+ );
2917
+ } catch {
2918
+ }
2919
+ }
2864
2920
  const mockStrategy = options?.mock ?? "marked";
2865
2921
  const maxConcurrency = options?.maxConcurrency ?? 10;
2866
2922
  const fullTestRunUrl = `${serviceUrl}${testRunUrl}`;
@@ -2879,9 +2935,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2879
2935
  mockStrategy,
2880
2936
  resolvedOverrides,
2881
2937
  replayedTraceIds[index],
2882
- dbBranchEnabled(options?.dbBranch),
2938
+ dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true,
2883
2939
  resolveDbBranchSettings(options?.dbBranch),
2884
- options?.adaptInputs
2940
+ options?.adaptInputs,
2941
+ options?.dryRun === true
2885
2942
  )
2886
2943
  );
2887
2944
  const total = tasks.length;
@@ -2989,6 +3046,16 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2989
3046
  }
2990
3047
  } : void 0
2991
3048
  );
3049
+ if (options?.dryRun === true) {
3050
+ await httpClient.completeReplay(testRunId).catch(() => void 0);
3051
+ const dryResult = {
3052
+ items: resultItems,
3053
+ testRunId,
3054
+ testRunUrl: fullTestRunUrl
3055
+ };
3056
+ await writeReplayResultFile(dryResult);
3057
+ return dryResult;
3058
+ }
2992
3059
  const deliveredTraceIds = await preserveReplayFailure(
2993
3060
  () => waitForReplayPersistence(httpClient, testRunId, replayedTraceIds),
2994
3061
  resultItems,
@@ -3144,6 +3211,7 @@ __export(node_exports, {
3144
3211
  BitfabOpenAITracingProcessor: () => BitfabOpenAITracingProcessor,
3145
3212
  BitfabVercelAiHandler: () => BitfabVercelAiHandler,
3146
3213
  DEFAULT_SERVICE_URL: () => DEFAULT_SERVICE_URL,
3214
+ DatasetsClient: () => DatasetsClient,
3147
3215
  DbBranchReplayError: () => DbBranchReplayError,
3148
3216
  HttpClient: () => HttpClient,
3149
3217
  NO_MOCK_OVERRIDE: () => NO_MOCK_OVERRIDE,
@@ -3157,6 +3225,7 @@ __export(node_exports, {
3157
3225
  getCurrentSpan: () => getCurrentSpan,
3158
3226
  getCurrentTrace: () => getCurrentTrace,
3159
3227
  reportReplayProgress: () => reportReplayProgress,
3228
+ seedFromRegistry: () => seedFromRegistry,
3160
3229
  serializeReplayResult: () => serializeReplayResult
3161
3230
  });
3162
3231
  module.exports = __toCommonJS(node_exports);
@@ -4229,6 +4298,131 @@ async function runFunctionWithBaml(bamlSource, inputs, providers, envVars) {
4229
4298
  // src/client.ts
4230
4299
  init_constants();
4231
4300
 
4301
+ // src/datasets.ts
4302
+ var DEFAULT_RERUN_TIMEOUT_MS = 9e4;
4303
+ var DEFAULT_RERUN_POLL_INTERVAL_MS = 1e3;
4304
+ var TERMINAL_RERUN_STATUSES = /* @__PURE__ */ new Set([
4305
+ "completed",
4306
+ "errored"
4307
+ ]);
4308
+ function sleep(ms) {
4309
+ return new Promise((resolve) => setTimeout(resolve, ms));
4310
+ }
4311
+ function datasetPath(datasetId, suffix = "") {
4312
+ return `/api/sdk/datasets/${encodeURIComponent(datasetId)}${suffix}`;
4313
+ }
4314
+ var DatasetsClient = class {
4315
+ constructor(httpClient) {
4316
+ this.httpClient = httpClient;
4317
+ }
4318
+ /**
4319
+ * Create a dataset, or update the one already named this way under the same
4320
+ * trace function. `created` reports which happened. An omitted description
4321
+ * leaves an existing one untouched.
4322
+ */
4323
+ async save(params) {
4324
+ return this.httpClient.request("/api/sdk/datasets", {
4325
+ traceFunctionKey: params.traceFunctionKey,
4326
+ name: params.name,
4327
+ ...params.description === void 0 ? {} : { description: params.description }
4328
+ });
4329
+ }
4330
+ /**
4331
+ * List datasets, scoped to one trace function when `traceFunctionKey` is
4332
+ * given and organization-wide otherwise.
4333
+ */
4334
+ async list(params = {}) {
4335
+ const query = params.traceFunctionKey === void 0 ? "" : `?traceFunctionKey=${encodeURIComponent(params.traceFunctionKey)}`;
4336
+ const response = await this.httpClient.get(
4337
+ `/api/sdk/datasets${query}`
4338
+ );
4339
+ return response.datasets;
4340
+ }
4341
+ /** Fetch one dataset by id. Rejects with a 404 `BitfabError` when it is not in this organization. */
4342
+ async get(datasetId) {
4343
+ const response = await this.httpClient.get(
4344
+ datasetPath(datasetId)
4345
+ );
4346
+ return response.dataset;
4347
+ }
4348
+ /** The ids of every trace in the dataset, the same membership a replay with `datasetId` selects. */
4349
+ async listTraces(datasetId) {
4350
+ return this.httpClient.get(
4351
+ datasetPath(datasetId, "/traces")
4352
+ );
4353
+ }
4354
+ /**
4355
+ * Add traces to the dataset (1 to 100 ids per call). Traces outside the
4356
+ * organization or under another trace function are reported in
4357
+ * `skippedTraceIds` rather than failing the call.
4358
+ */
4359
+ async addTraces(datasetId, traceIds) {
4360
+ return this.httpClient.request(
4361
+ datasetPath(datasetId, "/traces"),
4362
+ { traceIds }
4363
+ );
4364
+ }
4365
+ /** Remove traces from the dataset. The traces themselves are never deleted. */
4366
+ async removeTraces(datasetId, traceIds) {
4367
+ return this.httpClient.request(
4368
+ datasetPath(datasetId, "/removeTraces"),
4369
+ { traceIds }
4370
+ );
4371
+ }
4372
+ /**
4373
+ * Assign graders to the dataset (1 to 100 ids per call). Graders outside the
4374
+ * organization or under another trace function are reported in
4375
+ * `skippedGraderIds` rather than failing the call.
4376
+ */
4377
+ async addGraders(datasetId, graderIds) {
4378
+ return this.httpClient.request(
4379
+ datasetPath(datasetId, "/graders"),
4380
+ { graderIds }
4381
+ );
4382
+ }
4383
+ /** Unassign graders from the dataset. */
4384
+ async removeGraders(datasetId, graderIds) {
4385
+ return this.httpClient.request(
4386
+ datasetPath(datasetId, "/removeGraders"),
4387
+ { graderIds }
4388
+ );
4389
+ }
4390
+ /**
4391
+ * Re-run graders over every trace in the dataset. Defaults to every assigned
4392
+ * grader; an unassigned id is rejected. Waits for the run to finish (up to
4393
+ * `timeoutMs`, default 90s) unless `wait` is `false`, and returns the last
4394
+ * run state seen either way. A request matching an in-flight run joins it.
4395
+ */
4396
+ async rerunGraders(datasetId, options = {}) {
4397
+ const started = await this.httpClient.request(
4398
+ datasetPath(datasetId, "/rerunGraders"),
4399
+ options.graderIds === void 0 ? {} : { graderIds: options.graderIds }
4400
+ );
4401
+ if (options.wait === false) {
4402
+ return started;
4403
+ }
4404
+ const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_RERUN_TIMEOUT_MS);
4405
+ const interval = options.pollIntervalMs ?? DEFAULT_RERUN_POLL_INTERVAL_MS;
4406
+ let run = started.run;
4407
+ while (!TERMINAL_RERUN_STATUSES.has(run.status) && Date.now() < deadline) {
4408
+ await sleep(interval);
4409
+ run = await this.getGraderRerun(datasetId, run.id) ?? run;
4410
+ }
4411
+ return { run, joinedExisting: started.joinedExisting };
4412
+ }
4413
+ /**
4414
+ * The dataset's active grader re-run, or the run named by `runId`. Returns
4415
+ * `null` when nothing is active or the run does not belong to this dataset.
4416
+ */
4417
+ async getGraderRerun(datasetId, runId) {
4418
+ const query = runId === void 0 ? "" : `?runId=${encodeURIComponent(runId)}`;
4419
+ const response = await this.httpClient.get(
4420
+ datasetPath(datasetId, `/rerunGraders${query}`)
4421
+ );
4422
+ return response.run;
4423
+ }
4424
+ };
4425
+
4232
4426
  // src/dbSnapshot.ts
4233
4427
  init_errors();
4234
4428
  var SUPPORTED_PROVIDERS = ["neon"];
@@ -5964,6 +6158,7 @@ var Bitfab = class {
5964
6158
  serviceUrl: this.serviceUrl,
5965
6159
  timeout: this.timeout
5966
6160
  });
6161
+ this.datasets = new DatasetsClient(this.httpClient);
5967
6162
  }
5968
6163
  /**
5969
6164
  * Decorate a class method as an automatically expanded trace root.
@@ -6874,6 +7069,7 @@ var Bitfab = class {
6874
7069
  inputSourceTraceId: traceState?.inputSourceTraceId,
6875
7070
  dbSnapshotRef: traceState?.dbSnapshotRef,
6876
7071
  dropped: traceState?.dropped,
7072
+ ingestionType: traceState?.ingestionType,
6877
7073
  // Built AFTER the wrapped fn finished, so `accessed` reflects
6878
7074
  // whether customer code obtained the branch URL during this
6879
7075
  // item. Omitted entirely when no lease was attached, so the
@@ -7097,6 +7293,7 @@ var Bitfab = class {
7097
7293
  Object.defineProperty(wrappedFn, "_bitfabTraceFunctionKey", {
7098
7294
  value: traceFunctionKey
7099
7295
  });
7296
+ Object.defineProperty(wrappedFn, "_bitfabWrappedFn", { value: fn });
7100
7297
  return wrappedFn;
7101
7298
  }
7102
7299
  /**
@@ -7259,6 +7456,9 @@ var Bitfab = class {
7259
7456
  if (params.dbSnapshotRef) {
7260
7457
  rawTrace.db_snapshot_ref = params.dbSnapshotRef;
7261
7458
  }
7459
+ if (params.ingestionType) {
7460
+ rawTrace.ingestion_type = params.ingestionType;
7461
+ }
7262
7462
  if (params.dbSnapshotUsage) {
7263
7463
  rawTrace.db_snapshot_usage = {
7264
7464
  neon_branch_id: params.dbSnapshotUsage.neonBranchId,
@@ -7405,6 +7605,68 @@ var Bitfab = class {
7405
7605
  clearMockOverrides() {
7406
7606
  this.mockOverrides.length = 0;
7407
7607
  }
7608
+ /**
7609
+ * Write a replayable trace from a case, without running anything.
7610
+ *
7611
+ * Use this to turn a corpus you already hold (a Braintrust dataset, a
7612
+ * spreadsheet, hand-written cases) into traces that {@link replay} can
7613
+ * select. The recorded root span carries `input` as its input and `expected`
7614
+ * as its output, so replay reports each item against the value you expected
7615
+ * rather than against a previous run.
7616
+ *
7617
+ * A seeded trace has no child spans and no database pin, so replay mocking
7618
+ * has nothing recorded to substitute and `dbBranch` refuses it. Pass
7619
+ * `mockOverride` at replay time for calls that must not run.
7620
+ *
7621
+ * @returns The trace ID, usable with `replay({ traceIds: [...] })`.
7622
+ */
7623
+ seedTrace(traceFunctionKey, options) {
7624
+ const { input } = options;
7625
+ const fn = options.fn?._bitfabWrappedFn ?? options.fn;
7626
+ if (fn && input.length < fn.length) {
7627
+ throw new BitfabError(
7628
+ `Seeded case supplies ${input.length} argument(s) but ${fn.name === "" ? "the function" : fn.name} requires ${fn.length}. Fix the case, or omit fn to seed it anyway.`
7629
+ );
7630
+ }
7631
+ const traceId = randomUuid();
7632
+ const startedAt = nowIsoTimestamp();
7633
+ activeTraceStates.set(traceId, {
7634
+ traceId,
7635
+ startedAt,
7636
+ contexts: [],
7637
+ ingestionType: "seeded",
7638
+ ...options.sessionId !== void 0 && { sessionId: options.sessionId },
7639
+ ...options.metadata !== void 0 && { metadata: options.metadata }
7640
+ });
7641
+ try {
7642
+ this.sendWrapperSpan({
7643
+ traceFunctionKey,
7644
+ spanName: options.spanName ?? traceFunctionKey,
7645
+ traceId,
7646
+ spanId: randomUuid(),
7647
+ parentSpanId: null,
7648
+ inputs: input,
7649
+ result: options.expected,
7650
+ startedAt,
7651
+ endedAt: startedAt,
7652
+ spanType: options.spanType ?? "agent",
7653
+ captureContent: true
7654
+ });
7655
+ this.sendTraceCompletion({
7656
+ traceFunctionKey,
7657
+ traceId,
7658
+ startedAt,
7659
+ endedAt: startedAt,
7660
+ sessionId: options.sessionId,
7661
+ metadata: options.metadata,
7662
+ contexts: [],
7663
+ ingestionType: "seeded"
7664
+ });
7665
+ } finally {
7666
+ activeTraceStates.delete(traceId);
7667
+ }
7668
+ return traceId;
7669
+ }
7408
7670
  async replay(traceFunctionKey, fn, options) {
7409
7671
  const wrappedKey = fn._bitfabTraceFunctionKey;
7410
7672
  let replayFn = fn;
@@ -7665,6 +7927,37 @@ init_replay();
7665
7927
  function defineReplayRegistry(registry) {
7666
7928
  return registry;
7667
7929
  }
7930
+ async function seedFromRegistry(registry, pipeline, cases) {
7931
+ const registration = registry[pipeline];
7932
+ if (registration === void 0) {
7933
+ throw new BitfabError(
7934
+ `Unknown pipeline '${pipeline}'. Registered: ${Object.keys(registry).join(", ")}`
7935
+ );
7936
+ }
7937
+ const traceFunctionKey = resolveTraceFunctionKey(registration);
7938
+ const traceIds = cases.map(
7939
+ (seedCase) => registration.client.seedTrace(traceFunctionKey, {
7940
+ input: seedCase.input,
7941
+ expected: seedCase.expected,
7942
+ fn: registration.fn,
7943
+ metadata: seedCase.metadata,
7944
+ sessionId: seedCase.sessionId
7945
+ })
7946
+ );
7947
+ const { flushTraces: flushTraces2 } = await Promise.resolve().then(() => (init_http(), http_exports));
7948
+ await flushTraces2(3e4);
7949
+ return { pipeline, traceFunctionKey, traceIds };
7950
+ }
7951
+ function resolveTraceFunctionKey(registration) {
7952
+ const wrappedKey = registration.fn._bitfabTraceFunctionKey;
7953
+ const key = registration.traceFunctionKey ?? wrappedKey;
7954
+ if (key === void 0) {
7955
+ throw new BitfabError(
7956
+ "Replay registry entry uses a plain function. Set traceFunctionKey to the key its production handler records."
7957
+ );
7958
+ }
7959
+ return key;
7960
+ }
7668
7961
 
7669
7962
  // src/node.ts
7670
7963
  init_asyncStorage();
@@ -7683,6 +7976,7 @@ assertAsyncStorageRegistered();
7683
7976
  BitfabOpenAITracingProcessor,
7684
7977
  BitfabVercelAiHandler,
7685
7978
  DEFAULT_SERVICE_URL,
7979
+ DatasetsClient,
7686
7980
  DbBranchReplayError,
7687
7981
  HttpClient,
7688
7982
  NO_MOCK_OVERRIDE,
@@ -7696,6 +7990,7 @@ assertAsyncStorageRegistered();
7696
7990
  getCurrentSpan,
7697
7991
  getCurrentTrace,
7698
7992
  reportReplayProgress,
7993
+ seedFromRegistry,
7699
7994
  serializeReplayResult
7700
7995
  });
7701
7996
  //# sourceMappingURL=node.cjs.map