bitfab 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
@@ -97,7 +97,7 @@ var __version__, __packageName__;
97
97
  var init_version_generated = __esm({
98
98
  "src/version.generated.ts"() {
99
99
  "use strict";
100
- __version__ = "0.38.10";
100
+ __version__ = "0.40.0";
101
101
  __packageName__ = "bitfab";
102
102
  }
103
103
  });
@@ -1214,6 +1214,16 @@ var init_transport = __esm({
1214
1214
  });
1215
1215
 
1216
1216
  // src/http.ts
1217
+ var http_exports = {};
1218
+ __export(http_exports, {
1219
+ BitfabError: () => BitfabError,
1220
+ HttpClient: () => HttpClient,
1221
+ awaitOnExit: () => awaitOnExit,
1222
+ awaitPendingRequests: () => awaitPendingRequests,
1223
+ flushTraces: () => flushTraces,
1224
+ parseRetryAfterMs: () => parseRetryAfterMs,
1225
+ serializePayloadBody: () => serializePayloadBody
1226
+ });
1217
1227
  function awaitOnExit(promise) {
1218
1228
  pendingTracePromises.add(promise);
1219
1229
  void promise.finally(() => {
@@ -1738,6 +1748,10 @@ var init_http = __esm({
1738
1748
  const response = await this.get(endpoint);
1739
1749
  return response.span;
1740
1750
  }
1751
+ /**
1752
+ * GET a JSON endpoint on the service with the client's API key. Throws a
1753
+ * `BitfabError` carrying the status text for any non-2xx response.
1754
+ */
1741
1755
  async get(endpoint) {
1742
1756
  const url = `${this.serviceUrl}${endpoint}`;
1743
1757
  const controller = new AbortController();
@@ -1751,7 +1765,10 @@ var init_http = __esm({
1751
1765
  if (!response.ok) {
1752
1766
  const errorText = await response.text();
1753
1767
  throw new BitfabError(
1754
- `HTTP ${response.status}: ${errorText.slice(0, 500)}`
1768
+ `HTTP ${response.status}: ${errorText.slice(0, 500)}`,
1769
+ void 0,
1770
+ response.status,
1771
+ parseRetryAfterMs(readHeader(response, "retry-after"))
1755
1772
  );
1756
1773
  }
1757
1774
  return await response.json();
@@ -2558,7 +2575,7 @@ function buildMockTree(rootNode) {
2558
2575
  }
2559
2576
  return { spans };
2560
2577
  }
2561
- async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, resolvedOverrides, replayedTraceId, includeDbBranchLease, dbBranchSettings, adaptInputs) {
2578
+ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy, resolvedOverrides, replayedTraceId, includeDbBranchLease, dbBranchSettings, adaptInputs, dryRun) {
2562
2579
  let lease = includeDbBranchLease ? serverItem.dbBranchLease : void 0;
2563
2580
  let leaseError = includeDbBranchLease ? serverItem.dbBranchLeaseError : void 0;
2564
2581
  let dbSnapshotRef = serverItem.dbSnapshotRef;
@@ -2653,6 +2670,32 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
2653
2670
  }
2654
2671
  return pending;
2655
2672
  } : void 0;
2673
+ if (dryRun) {
2674
+ return {
2675
+ traceId: null,
2676
+ originalTraceId,
2677
+ originalSpanId,
2678
+ sourceTraceId: originalTraceId,
2679
+ sourceSpanId: originalSpanId,
2680
+ input: inputs,
2681
+ result: void 0,
2682
+ originalOutput,
2683
+ ...serverItem.ingestionType && {
2684
+ ingestionType: serverItem.ingestionType
2685
+ },
2686
+ error: null,
2687
+ traceError: null,
2688
+ replayError: null,
2689
+ durationMs: null,
2690
+ originalDurationMs: serverItem.originalDurationMs ?? serverItem.durationMs ?? null,
2691
+ originalTokens: serverItem.originalTokens ?? serverItem.tokens ?? null,
2692
+ originalModel: serverItem.originalModel ?? serverItem.model ?? null,
2693
+ tokens: null,
2694
+ model: serverItem.originalModel ?? serverItem.model ?? null,
2695
+ dbSnapshotRef: dbSnapshotRef ?? null,
2696
+ dbBranchTimings: dbBranchTimings ?? null
2697
+ };
2698
+ }
2656
2699
  try {
2657
2700
  replayStarted = performance.now();
2658
2701
  const maybePromise = runWithReplayContext(
@@ -2716,6 +2759,9 @@ async function processItem(httpClient, serverItem, fn, testRunId, mockStrategy,
2716
2759
  input: inputs,
2717
2760
  result,
2718
2761
  originalOutput,
2762
+ ...serverItem.ingestionType && {
2763
+ ingestionType: serverItem.ingestionType
2764
+ },
2719
2765
  error,
2720
2766
  traceError,
2721
2767
  replayError,
@@ -2863,13 +2909,23 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2863
2909
  options?.name,
2864
2910
  codeChangeDescription,
2865
2911
  codeChangeFiles,
2866
- dbBranchEnabled(options?.dbBranch),
2867
- // includeDbBranchLease
2912
+ // A dry run executes nothing, so a branch would be provisioned (and billed)
2913
+ // for code that never runs, and a seeded source's refusal would fail the item
2914
+ // before it could report its resolved inputs.
2915
+ dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true,
2868
2916
  options?.experimentGroupId,
2869
2917
  options?.datasetId,
2870
2918
  options?.graderIds,
2871
2919
  resolveDbBranchSettings(options?.dbBranch)
2872
2920
  );
2921
+ if (serverItems.length === 0) {
2922
+ try {
2923
+ console.warn(
2924
+ `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
2925
+ );
2926
+ } catch {
2927
+ }
2928
+ }
2873
2929
  const mockStrategy = options?.mock ?? "marked";
2874
2930
  const maxConcurrency = options?.maxConcurrency ?? 10;
2875
2931
  const fullTestRunUrl = `${serviceUrl}${testRunUrl}`;
@@ -2888,9 +2944,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2888
2944
  mockStrategy,
2889
2945
  resolvedOverrides,
2890
2946
  replayedTraceIds[index],
2891
- dbBranchEnabled(options?.dbBranch),
2947
+ dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true,
2892
2948
  resolveDbBranchSettings(options?.dbBranch),
2893
- options?.adaptInputs
2949
+ options?.adaptInputs,
2950
+ options?.dryRun === true
2894
2951
  )
2895
2952
  );
2896
2953
  const total = tasks.length;
@@ -2998,6 +3055,16 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
2998
3055
  }
2999
3056
  } : void 0
3000
3057
  );
3058
+ if (options?.dryRun === true) {
3059
+ await httpClient.completeReplay(testRunId).catch(() => void 0);
3060
+ const dryResult = {
3061
+ items: resultItems,
3062
+ testRunId,
3063
+ testRunUrl: fullTestRunUrl
3064
+ };
3065
+ await writeReplayResultFile(dryResult);
3066
+ return dryResult;
3067
+ }
3001
3068
  const deliveredTraceIds = await preserveReplayFailure(
3002
3069
  () => waitForReplayPersistence(httpClient, testRunId, replayedTraceIds),
3003
3070
  resultItems,
@@ -3153,6 +3220,7 @@ __export(node_exports, {
3153
3220
  BitfabOpenAITracingProcessor: () => BitfabOpenAITracingProcessor,
3154
3221
  BitfabVercelAiHandler: () => BitfabVercelAiHandler,
3155
3222
  DEFAULT_SERVICE_URL: () => DEFAULT_SERVICE_URL,
3223
+ DatasetsClient: () => DatasetsClient,
3156
3224
  DbBranchReplayError: () => DbBranchReplayError,
3157
3225
  HttpClient: () => HttpClient,
3158
3226
  NO_MOCK_OVERRIDE: () => NO_MOCK_OVERRIDE,
@@ -3166,6 +3234,7 @@ __export(node_exports, {
3166
3234
  getCurrentSpan: () => getCurrentSpan,
3167
3235
  getCurrentTrace: () => getCurrentTrace,
3168
3236
  reportReplayProgress: () => reportReplayProgress,
3237
+ seedFromRegistry: () => seedFromRegistry,
3169
3238
  serializeReplayResult: () => serializeReplayResult
3170
3239
  });
3171
3240
  module.exports = __toCommonJS(node_exports);
@@ -4238,6 +4307,131 @@ async function runFunctionWithBaml(bamlSource, inputs, providers, envVars) {
4238
4307
  // src/client.ts
4239
4308
  init_constants();
4240
4309
 
4310
+ // src/datasets.ts
4311
+ var DEFAULT_RERUN_TIMEOUT_MS = 9e4;
4312
+ var DEFAULT_RERUN_POLL_INTERVAL_MS = 1e3;
4313
+ var TERMINAL_RERUN_STATUSES = /* @__PURE__ */ new Set([
4314
+ "completed",
4315
+ "errored"
4316
+ ]);
4317
+ function sleep(ms) {
4318
+ return new Promise((resolve) => setTimeout(resolve, ms));
4319
+ }
4320
+ function datasetPath(datasetId, suffix = "") {
4321
+ return `/api/sdk/datasets/${encodeURIComponent(datasetId)}${suffix}`;
4322
+ }
4323
+ var DatasetsClient = class {
4324
+ constructor(httpClient) {
4325
+ this.httpClient = httpClient;
4326
+ }
4327
+ /**
4328
+ * Create a dataset, or update the one already named this way under the same
4329
+ * trace function. `created` reports which happened. An omitted description
4330
+ * leaves an existing one untouched.
4331
+ */
4332
+ async save(params) {
4333
+ return this.httpClient.request("/api/sdk/datasets", {
4334
+ traceFunctionKey: params.traceFunctionKey,
4335
+ name: params.name,
4336
+ ...params.description === void 0 ? {} : { description: params.description }
4337
+ });
4338
+ }
4339
+ /**
4340
+ * List datasets, scoped to one trace function when `traceFunctionKey` is
4341
+ * given and organization-wide otherwise.
4342
+ */
4343
+ async list(params = {}) {
4344
+ const query = params.traceFunctionKey === void 0 ? "" : `?traceFunctionKey=${encodeURIComponent(params.traceFunctionKey)}`;
4345
+ const response = await this.httpClient.get(
4346
+ `/api/sdk/datasets${query}`
4347
+ );
4348
+ return response.datasets;
4349
+ }
4350
+ /** Fetch one dataset by id. Rejects with a 404 `BitfabError` when it is not in this organization. */
4351
+ async get(datasetId) {
4352
+ const response = await this.httpClient.get(
4353
+ datasetPath(datasetId)
4354
+ );
4355
+ return response.dataset;
4356
+ }
4357
+ /** The ids of every trace in the dataset, the same membership a replay with `datasetId` selects. */
4358
+ async listTraces(datasetId) {
4359
+ return this.httpClient.get(
4360
+ datasetPath(datasetId, "/traces")
4361
+ );
4362
+ }
4363
+ /**
4364
+ * Add traces to the dataset (1 to 100 ids per call). Traces outside the
4365
+ * organization or under another trace function are reported in
4366
+ * `skippedTraceIds` rather than failing the call.
4367
+ */
4368
+ async addTraces(datasetId, traceIds) {
4369
+ return this.httpClient.request(
4370
+ datasetPath(datasetId, "/traces"),
4371
+ { traceIds }
4372
+ );
4373
+ }
4374
+ /** Remove traces from the dataset. The traces themselves are never deleted. */
4375
+ async removeTraces(datasetId, traceIds) {
4376
+ return this.httpClient.request(
4377
+ datasetPath(datasetId, "/removeTraces"),
4378
+ { traceIds }
4379
+ );
4380
+ }
4381
+ /**
4382
+ * Assign graders to the dataset (1 to 100 ids per call). Graders outside the
4383
+ * organization or under another trace function are reported in
4384
+ * `skippedGraderIds` rather than failing the call.
4385
+ */
4386
+ async addGraders(datasetId, graderIds) {
4387
+ return this.httpClient.request(
4388
+ datasetPath(datasetId, "/graders"),
4389
+ { graderIds }
4390
+ );
4391
+ }
4392
+ /** Unassign graders from the dataset. */
4393
+ async removeGraders(datasetId, graderIds) {
4394
+ return this.httpClient.request(
4395
+ datasetPath(datasetId, "/removeGraders"),
4396
+ { graderIds }
4397
+ );
4398
+ }
4399
+ /**
4400
+ * Re-run graders over every trace in the dataset. Defaults to every assigned
4401
+ * grader; an unassigned id is rejected. Waits for the run to finish (up to
4402
+ * `timeoutMs`, default 90s) unless `wait` is `false`, and returns the last
4403
+ * run state seen either way. A request matching an in-flight run joins it.
4404
+ */
4405
+ async rerunGraders(datasetId, options = {}) {
4406
+ const started = await this.httpClient.request(
4407
+ datasetPath(datasetId, "/rerunGraders"),
4408
+ options.graderIds === void 0 ? {} : { graderIds: options.graderIds }
4409
+ );
4410
+ if (options.wait === false) {
4411
+ return started;
4412
+ }
4413
+ const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_RERUN_TIMEOUT_MS);
4414
+ const interval = options.pollIntervalMs ?? DEFAULT_RERUN_POLL_INTERVAL_MS;
4415
+ let run = started.run;
4416
+ while (!TERMINAL_RERUN_STATUSES.has(run.status) && Date.now() < deadline) {
4417
+ await sleep(interval);
4418
+ run = await this.getGraderRerun(datasetId, run.id) ?? run;
4419
+ }
4420
+ return { run, joinedExisting: started.joinedExisting };
4421
+ }
4422
+ /**
4423
+ * The dataset's active grader re-run, or the run named by `runId`. Returns
4424
+ * `null` when nothing is active or the run does not belong to this dataset.
4425
+ */
4426
+ async getGraderRerun(datasetId, runId) {
4427
+ const query = runId === void 0 ? "" : `?runId=${encodeURIComponent(runId)}`;
4428
+ const response = await this.httpClient.get(
4429
+ datasetPath(datasetId, `/rerunGraders${query}`)
4430
+ );
4431
+ return response.run;
4432
+ }
4433
+ };
4434
+
4241
4435
  // src/dbSnapshot.ts
4242
4436
  init_errors();
4243
4437
  var SUPPORTED_PROVIDERS = ["neon"];
@@ -5973,6 +6167,7 @@ var Bitfab = class {
5973
6167
  serviceUrl: this.serviceUrl,
5974
6168
  timeout: this.timeout
5975
6169
  });
6170
+ this.datasets = new DatasetsClient(this.httpClient);
5976
6171
  }
5977
6172
  /**
5978
6173
  * Decorate a class method as an automatically expanded trace root.
@@ -6883,6 +7078,7 @@ var Bitfab = class {
6883
7078
  inputSourceTraceId: traceState?.inputSourceTraceId,
6884
7079
  dbSnapshotRef: traceState?.dbSnapshotRef,
6885
7080
  dropped: traceState?.dropped,
7081
+ ingestionType: traceState?.ingestionType,
6886
7082
  // Built AFTER the wrapped fn finished, so `accessed` reflects
6887
7083
  // whether customer code obtained the branch URL during this
6888
7084
  // item. Omitted entirely when no lease was attached, so the
@@ -7106,6 +7302,7 @@ var Bitfab = class {
7106
7302
  Object.defineProperty(wrappedFn, "_bitfabTraceFunctionKey", {
7107
7303
  value: traceFunctionKey
7108
7304
  });
7305
+ Object.defineProperty(wrappedFn, "_bitfabWrappedFn", { value: fn });
7109
7306
  return wrappedFn;
7110
7307
  }
7111
7308
  /**
@@ -7268,6 +7465,9 @@ var Bitfab = class {
7268
7465
  if (params.dbSnapshotRef) {
7269
7466
  rawTrace.db_snapshot_ref = params.dbSnapshotRef;
7270
7467
  }
7468
+ if (params.ingestionType) {
7469
+ rawTrace.ingestion_type = params.ingestionType;
7470
+ }
7271
7471
  if (params.dbSnapshotUsage) {
7272
7472
  rawTrace.db_snapshot_usage = {
7273
7473
  neon_branch_id: params.dbSnapshotUsage.neonBranchId,
@@ -7414,6 +7614,68 @@ var Bitfab = class {
7414
7614
  clearMockOverrides() {
7415
7615
  this.mockOverrides.length = 0;
7416
7616
  }
7617
+ /**
7618
+ * Write a replayable trace from a case, without running anything.
7619
+ *
7620
+ * Use this to turn a corpus you already hold (a Braintrust dataset, a
7621
+ * spreadsheet, hand-written cases) into traces that {@link replay} can
7622
+ * select. The recorded root span carries `input` as its input and `expected`
7623
+ * as its output, so replay reports each item against the value you expected
7624
+ * rather than against a previous run.
7625
+ *
7626
+ * A seeded trace has no child spans and no database pin, so replay mocking
7627
+ * has nothing recorded to substitute and `dbBranch` refuses it. Pass
7628
+ * `mockOverride` at replay time for calls that must not run.
7629
+ *
7630
+ * @returns The trace ID, usable with `replay({ traceIds: [...] })`.
7631
+ */
7632
+ seedTrace(traceFunctionKey, options) {
7633
+ const { input } = options;
7634
+ const fn = options.fn?._bitfabWrappedFn ?? options.fn;
7635
+ if (fn && input.length < fn.length) {
7636
+ throw new BitfabError(
7637
+ `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.`
7638
+ );
7639
+ }
7640
+ const traceId = randomUuid();
7641
+ const startedAt = nowIsoTimestamp();
7642
+ activeTraceStates.set(traceId, {
7643
+ traceId,
7644
+ startedAt,
7645
+ contexts: [],
7646
+ ingestionType: "seeded",
7647
+ ...options.sessionId !== void 0 && { sessionId: options.sessionId },
7648
+ ...options.metadata !== void 0 && { metadata: options.metadata }
7649
+ });
7650
+ try {
7651
+ this.sendWrapperSpan({
7652
+ traceFunctionKey,
7653
+ spanName: options.spanName ?? traceFunctionKey,
7654
+ traceId,
7655
+ spanId: randomUuid(),
7656
+ parentSpanId: null,
7657
+ inputs: input,
7658
+ result: options.expected,
7659
+ startedAt,
7660
+ endedAt: startedAt,
7661
+ spanType: options.spanType ?? "agent",
7662
+ captureContent: true
7663
+ });
7664
+ this.sendTraceCompletion({
7665
+ traceFunctionKey,
7666
+ traceId,
7667
+ startedAt,
7668
+ endedAt: startedAt,
7669
+ sessionId: options.sessionId,
7670
+ metadata: options.metadata,
7671
+ contexts: [],
7672
+ ingestionType: "seeded"
7673
+ });
7674
+ } finally {
7675
+ activeTraceStates.delete(traceId);
7676
+ }
7677
+ return traceId;
7678
+ }
7417
7679
  async replay(traceFunctionKey, fn, options) {
7418
7680
  const wrappedKey = fn._bitfabTraceFunctionKey;
7419
7681
  let replayFn = fn;
@@ -7674,6 +7936,37 @@ init_replay();
7674
7936
  function defineReplayRegistry(registry) {
7675
7937
  return registry;
7676
7938
  }
7939
+ async function seedFromRegistry(registry, pipeline, cases) {
7940
+ const registration = registry[pipeline];
7941
+ if (registration === void 0) {
7942
+ throw new BitfabError(
7943
+ `Unknown pipeline '${pipeline}'. Registered: ${Object.keys(registry).join(", ")}`
7944
+ );
7945
+ }
7946
+ const traceFunctionKey = resolveTraceFunctionKey(registration);
7947
+ const traceIds = cases.map(
7948
+ (seedCase) => registration.client.seedTrace(traceFunctionKey, {
7949
+ input: seedCase.input,
7950
+ expected: seedCase.expected,
7951
+ fn: registration.fn,
7952
+ metadata: seedCase.metadata,
7953
+ sessionId: seedCase.sessionId
7954
+ })
7955
+ );
7956
+ const { flushTraces: flushTraces2 } = await Promise.resolve().then(() => (init_http(), http_exports));
7957
+ await flushTraces2(3e4);
7958
+ return { pipeline, traceFunctionKey, traceIds };
7959
+ }
7960
+ function resolveTraceFunctionKey(registration) {
7961
+ const wrappedKey = registration.fn._bitfabTraceFunctionKey;
7962
+ const key = registration.traceFunctionKey ?? wrappedKey;
7963
+ if (key === void 0) {
7964
+ throw new BitfabError(
7965
+ "Replay registry entry uses a plain function. Set traceFunctionKey to the key its production handler records."
7966
+ );
7967
+ }
7968
+ return key;
7969
+ }
7677
7970
 
7678
7971
  // src/node.ts
7679
7972
  init_asyncStorage();
@@ -7692,6 +7985,7 @@ assertAsyncStorageRegistered();
7692
7985
  BitfabOpenAITracingProcessor,
7693
7986
  BitfabVercelAiHandler,
7694
7987
  DEFAULT_SERVICE_URL,
7988
+ DatasetsClient,
7695
7989
  DbBranchReplayError,
7696
7990
  HttpClient,
7697
7991
  NO_MOCK_OVERRIDE,
@@ -7705,6 +7999,7 @@ assertAsyncStorageRegistered();
7705
7999
  getCurrentSpan,
7706
8000
  getCurrentTrace,
7707
8001
  reportReplayProgress,
8002
+ seedFromRegistry,
7708
8003
  serializeReplayResult
7709
8004
  });
7710
8005
  //# sourceMappingURL=node.cjs.map