@jaypie/llm 1.2.39 → 1.2.40

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.
@@ -7,12 +7,15 @@ var kit = require('@jaypie/kit');
7
7
  var RandomLib = require('random');
8
8
  var openai = require('openai');
9
9
  var zod = require('openai/helpers/zod');
10
+ var module$1 = require('module');
11
+ var url = require('url');
10
12
  var pdfLib = require('pdf-lib');
11
13
  var promises = require('fs/promises');
12
14
  var path = require('path');
13
15
  var aws = require('@jaypie/aws');
14
16
  var openmeteo = require('openmeteo');
15
17
 
18
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
16
19
  const FIRST_CLASS_PROVIDER = {
17
20
  // https://docs.anthropic.com/en/docs/about-claude/models/overview
18
21
  ANTHROPIC: {
@@ -4837,6 +4840,198 @@ class Toolkit {
4837
4840
  }
4838
4841
  }
4839
4842
 
4843
+ //
4844
+ //
4845
+ // Constants
4846
+ //
4847
+ const ENV = {
4848
+ DD_LLMOBS_ENABLED: "DD_LLMOBS_ENABLED",
4849
+ };
4850
+ const MODULE = {
4851
+ // Computed at runtime so bundlers (esbuild) do not attempt to include
4852
+ // dd-trace, which is provided by the Datadog Lambda layer at runtime.
4853
+ DD_TRACE: ["dd", "trace"].join("-"),
4854
+ };
4855
+ //
4856
+ //
4857
+ // Helpers
4858
+ //
4859
+ // CJS/ESM compatible require - handles bundling to CJS where import.meta.url
4860
+ // becomes undefined (mirrors packages/testkit/src/mock/original.ts).
4861
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
4862
+ // @ts-ignore - __filename exists in CJS context when ESM is bundled to CJS
4863
+ const requireModule = typeof __filename !== "undefined"
4864
+ ? module$1.createRequire(url.pathToFileURL(__filename).href)
4865
+ : module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
4866
+ let resolved = false;
4867
+ let cachedSdk$4 = null;
4868
+ /**
4869
+ * Lazily resolve the dd-trace `llmobs` SDK. Returns null (and never throws)
4870
+ * when dd-trace is absent or the SDK surface is unexpected. Cached after the
4871
+ * first attempt.
4872
+ */
4873
+ function resolveLlmObs() {
4874
+ if (resolved) {
4875
+ return cachedSdk$4;
4876
+ }
4877
+ resolved = true;
4878
+ try {
4879
+ const ddTrace = requireModule(MODULE.DD_TRACE);
4880
+ const tracer = ddTrace?.default ?? ddTrace;
4881
+ const llmobs = tracer?.llmobs;
4882
+ if (llmobs &&
4883
+ typeof llmobs.trace === "function" &&
4884
+ typeof llmobs.annotate === "function") {
4885
+ cachedSdk$4 = llmobs;
4886
+ }
4887
+ }
4888
+ catch {
4889
+ cachedSdk$4 = null;
4890
+ }
4891
+ return cachedSdk$4;
4892
+ }
4893
+ //
4894
+ //
4895
+ // Main
4896
+ //
4897
+ /**
4898
+ * True when LLM Observability emission is opted in via `DD_LLMOBS_ENABLED`.
4899
+ * Accepts any truthy value except "false"/"0".
4900
+ */
4901
+ function isLlmObsEnabled() {
4902
+ const value = process.env[ENV.DD_LLMOBS_ENABLED];
4903
+ if (!value) {
4904
+ return false;
4905
+ }
4906
+ const normalized = value.trim().toLowerCase();
4907
+ return normalized !== "" && normalized !== "false" && normalized !== "0";
4908
+ }
4909
+ /** Resolve the SDK only when enabled and available. */
4910
+ function activeSdk() {
4911
+ if (!isLlmObsEnabled()) {
4912
+ return null;
4913
+ }
4914
+ return resolveLlmObs();
4915
+ }
4916
+ /**
4917
+ * Run `fn` inside an LLM Observability span. When emission is disabled or
4918
+ * dd-trace is absent, `fn` is invoked directly with no overhead. The active
4919
+ * span is established for the duration of `fn`, so nested `withLlmObsSpan`
4920
+ * calls (and `annotateLlmObs`) attach as children/annotations automatically.
4921
+ */
4922
+ async function withLlmObsSpan(options, fn) {
4923
+ const sdk = activeSdk();
4924
+ if (!sdk) {
4925
+ return fn();
4926
+ }
4927
+ let started = false;
4928
+ try {
4929
+ return (await sdk.trace(options, () => {
4930
+ started = true;
4931
+ return fn();
4932
+ }));
4933
+ }
4934
+ catch (error) {
4935
+ // If `fn` ran, propagate its error (the SDK re-throws after finishing the
4936
+ // span). Only swallow failures from the instrumentation itself, re-running
4937
+ // `fn` so observability never breaks the underlying call.
4938
+ if (started) {
4939
+ throw error;
4940
+ }
4941
+ getLogger$6().warn("[llmobs] span emission failed");
4942
+ getLogger$6().var({ error });
4943
+ return fn();
4944
+ }
4945
+ }
4946
+ /**
4947
+ * Annotate the currently active LLM Observability span. No-op when disabled.
4948
+ */
4949
+ function annotateLlmObs(annotation) {
4950
+ const sdk = activeSdk();
4951
+ if (!sdk) {
4952
+ return;
4953
+ }
4954
+ try {
4955
+ sdk.annotate(undefined, annotation);
4956
+ }
4957
+ catch (error) {
4958
+ getLogger$6().warn("[llmobs] annotate failed");
4959
+ getLogger$6().var({ error });
4960
+ }
4961
+ }
4962
+ /**
4963
+ * Open a manual span that is finished later. Used by streaming, where work is
4964
+ * yielded incrementally and cannot be enclosed in a single callback. The span
4965
+ * is kept open via a deferred promise resolved by `finish()`. Returns null when
4966
+ * emission is disabled or dd-trace is absent.
4967
+ *
4968
+ * Note: because the span is held open across `yield` boundaries (outside the
4969
+ * SDK's AsyncLocalStorage scope), manual spans are emitted flat rather than
4970
+ * nested. Annotations are applied to the explicit span reference.
4971
+ */
4972
+ function openLlmObsSpan(options) {
4973
+ const sdk = activeSdk();
4974
+ if (!sdk) {
4975
+ return null;
4976
+ }
4977
+ try {
4978
+ let capturedSpan;
4979
+ let release;
4980
+ const held = new Promise((resolve) => {
4981
+ release = resolve;
4982
+ });
4983
+ // trace() runs the callback synchronously, capturing the span; the returned
4984
+ // pending promise keeps the span open until finish() resolves it.
4985
+ void sdk.trace(options, (span) => {
4986
+ capturedSpan = span;
4987
+ return held;
4988
+ });
4989
+ let finished = false;
4990
+ return {
4991
+ annotate: (annotation) => {
4992
+ try {
4993
+ sdk.annotate(capturedSpan, annotation);
4994
+ }
4995
+ catch (error) {
4996
+ getLogger$6().warn("[llmobs] annotate failed");
4997
+ getLogger$6().var({ error });
4998
+ }
4999
+ },
5000
+ finish: () => {
5001
+ if (finished) {
5002
+ return;
5003
+ }
5004
+ finished = true;
5005
+ release?.();
5006
+ },
5007
+ };
5008
+ }
5009
+ catch (error) {
5010
+ getLogger$6().warn("[llmobs] span emission failed");
5011
+ getLogger$6().var({ error });
5012
+ return null;
5013
+ }
5014
+ }
5015
+ //
5016
+ //
5017
+ // Mapping Helpers
5018
+ //
5019
+ /**
5020
+ * Sum an `LlmUsage` array into LLM Observability metric keys. Returns undefined
5021
+ * when there is no usage data so callers can omit the `metrics` field.
5022
+ */
5023
+ function usageToLlmObsMetrics(usage) {
5024
+ if (!usage || usage.length === 0) {
5025
+ return undefined;
5026
+ }
5027
+ const metrics = usage.reduce((sum, item) => ({
5028
+ input_tokens: sum.input_tokens + (item.input ?? 0),
5029
+ output_tokens: sum.output_tokens + (item.output ?? 0),
5030
+ total_tokens: sum.total_tokens + (item.total ?? 0),
5031
+ }), { input_tokens: 0, output_tokens: 0, total_tokens: 0 });
5032
+ return metrics;
5033
+ }
5034
+
4840
5035
  //
4841
5036
  //
4842
5037
  // Main
@@ -5708,30 +5903,46 @@ class OperateLoop {
5708
5903
  // Initialize state
5709
5904
  const state = await this.initializeState(input, options);
5710
5905
  const context = this.createContext(options);
5711
- // Build initial request
5712
- let request = this.buildInitialRequest(state, options);
5713
- // Multi-turn loop
5714
- while (state.currentTurn < state.maxTurns) {
5715
- state.currentTurn++;
5716
- // Execute one turn with retry logic
5717
- const shouldContinue = await this.executeOneTurn(request, state, context, options);
5718
- if (!shouldContinue) {
5719
- break;
5906
+ const modelName = options.model ?? this.adapter.defaultModel;
5907
+ // Enclosing LLM Observability span (no-op when DD_LLMOBS_ENABLED is unset).
5908
+ // Child llm/tool spans nest under it via the SDK's active-span context.
5909
+ return withLlmObsSpan({
5910
+ kind: state.toolkit ? "agent" : "llm",
5911
+ modelName,
5912
+ modelProvider: this.adapter.name,
5913
+ name: "jaypie.llm.operate",
5914
+ }, async () => {
5915
+ // Build initial request
5916
+ let request = this.buildInitialRequest(state, options);
5917
+ // Multi-turn loop
5918
+ while (state.currentTurn < state.maxTurns) {
5919
+ state.currentTurn++;
5920
+ // Execute one turn with retry logic
5921
+ const shouldContinue = await this.executeOneTurn(request, state, context, options);
5922
+ if (!shouldContinue) {
5923
+ break;
5924
+ }
5925
+ // Rebuild request with updated history for next turn
5926
+ request = {
5927
+ format: state.formattedFormat,
5928
+ instructions: options.instructions,
5929
+ messages: state.currentInput,
5930
+ model: modelName,
5931
+ providerOptions: options.providerOptions,
5932
+ system: options.system,
5933
+ temperature: options.temperature,
5934
+ tools: state.formattedTools,
5935
+ user: options.user,
5936
+ };
5720
5937
  }
5721
- // Rebuild request with updated history for next turn
5722
- request = {
5723
- format: state.formattedFormat,
5724
- instructions: options.instructions,
5725
- messages: state.currentInput,
5726
- model: options.model ?? this.adapter.defaultModel,
5727
- providerOptions: options.providerOptions,
5728
- system: options.system,
5729
- temperature: options.temperature,
5730
- tools: state.formattedTools,
5731
- user: options.user,
5732
- };
5733
- }
5734
- return state.responseBuilder.build();
5938
+ const response = state.responseBuilder.build();
5939
+ annotateLlmObs({
5940
+ inputData: input,
5941
+ metrics: usageToLlmObsMetrics(response.usage),
5942
+ outputData: response.content,
5943
+ });
5944
+ return response;
5945
+ });
5735
5946
  }
5736
5947
  //
5737
5948
  // Private Methods
@@ -5833,20 +6044,34 @@ class OperateLoop {
5833
6044
  options,
5834
6045
  providerRequest,
5835
6046
  });
5836
- // Execute with retry (RetryExecutor handles error hooks and throws appropriate errors)
5837
- const response = await retryExecutor.execute((signal) => this.adapter.executeRequest(this.client, providerRequest, signal), {
5838
- context: {
5839
- input: state.currentInput,
5840
- options,
5841
- providerRequest,
5842
- },
5843
- hooks: context.hooks,
6047
+ // Execute with retry inside a child llm span (no-op when llmobs disabled).
6048
+ // RetryExecutor handles error hooks and throws appropriate errors.
6049
+ const { parsed, response } = await withLlmObsSpan({
6050
+ kind: "llm",
6051
+ modelName: options.model ?? this.adapter.defaultModel,
6052
+ modelProvider: this.adapter.name,
6053
+ name: "jaypie.llm.model",
6054
+ }, async () => {
6055
+ const response = await retryExecutor.execute((signal) => this.adapter.executeRequest(this.client, providerRequest, signal), {
6056
+ context: {
6057
+ input: state.currentInput,
6058
+ options,
6059
+ providerRequest,
6060
+ },
6061
+ hooks: context.hooks,
6062
+ });
6063
+ // Log what was returned from the model
6064
+ log.trace("[operate] Model response received");
6065
+ log.var({ "operate.response": response });
6066
+ // Parse response
6067
+ const parsed = this.adapter.parseResponse(response, options);
6068
+ annotateLlmObs({
6069
+ inputData: state.currentInput,
6070
+ metrics: usageToLlmObsMetrics(parsed.usage ? [parsed.usage] : undefined),
6071
+ outputData: parsed.content ?? "",
6072
+ });
6073
+ return { parsed, response };
5844
6074
  });
5845
- // Log what was returned from the model
5846
- log.trace("[operate] Model response received");
5847
- log.var({ "operate.response": response });
5848
- // Parse response
5849
- const parsed = this.adapter.parseResponse(response, options);
5850
6075
  // Track usage
5851
6076
  if (parsed.usage) {
5852
6077
  state.responseBuilder.addUsage(parsed.usage);
@@ -5891,11 +6116,19 @@ class OperateLoop {
5891
6116
  args: toolCall.arguments,
5892
6117
  toolName: toolCall.name,
5893
6118
  });
5894
- // Call the tool
6119
+ // Call the tool inside a child tool span (no-op when disabled)
5895
6120
  log.trace(`[operate] Calling tool - ${toolCall.name}`);
5896
- const result = await state.toolkit.call({
5897
- arguments: toolCall.arguments,
5898
- name: toolCall.name,
6121
+ const result = await withLlmObsSpan({ kind: "tool", name: toolCall.name }, async () => {
6122
+ const result = await state.toolkit.call({
6123
+ arguments: toolCall.arguments,
6124
+ name: toolCall.name,
6125
+ });
6126
+ annotateLlmObs({
6127
+ inputData: toolCall.arguments,
6128
+ metadata: { tool: toolCall.name },
6129
+ outputData: result,
6130
+ });
6131
+ return result;
5899
6132
  });
5900
6133
  // Execute afterEachTool hook
5901
6134
  await this.hookRunnerInstance.runAfterTool(context.hooks, {
@@ -6145,7 +6378,7 @@ class StreamLoop {
6145
6378
  }
6146
6379
  // If we have tool calls, process them
6147
6380
  if (toolCalls && toolCalls.length > 0 && state.toolkit) {
6148
- yield* this.processToolCalls(toolCalls, state, context, options);
6381
+ yield* this.processToolCalls(toolCalls, state, context);
6149
6382
  // Check if we've reached max turns
6150
6383
  if (state.currentTurn >= state.maxTurns) {
6151
6384
  const error = new errors.TooManyRequestsError();
@@ -6252,6 +6485,16 @@ class StreamLoop {
6252
6485
  options,
6253
6486
  providerRequest,
6254
6487
  });
6488
+ // Open a manual llm span held open across the streamed turn. Flat (not
6489
+ // nested) because it spans yield boundaries; no-op when llmobs disabled.
6490
+ const llmSpan = openLlmObsSpan({
6491
+ kind: "llm",
6492
+ modelName: options.model ?? this.adapter.defaultModel,
6493
+ modelProvider: this.adapter.name,
6494
+ name: "jaypie.llm.model",
6495
+ });
6496
+ const inputSnapshot = [...state.currentInput];
6497
+ let streamedText = "";
6255
6498
  // Collect tool calls from the stream
6256
6499
  const collectedToolCalls = [];
6257
6500
  // Retry loop for connection-level failures
@@ -6271,6 +6514,7 @@ class StreamLoop {
6271
6514
  // Pass through text chunks
6272
6515
  if (chunk.type === exports.LlmStreamChunkType.Text) {
6273
6516
  chunksYielded = true;
6517
+ streamedText += chunk.content ?? "";
6274
6518
  yield chunk;
6275
6519
  }
6276
6520
  // Collect tool calls
@@ -6317,6 +6561,12 @@ class StreamLoop {
6317
6561
  title: "Stream Error",
6318
6562
  },
6319
6563
  };
6564
+ llmSpan?.annotate({
6565
+ inputData: inputSnapshot,
6566
+ metrics: usageToLlmObsMetrics(state.usageItems),
6567
+ outputData: streamedText,
6568
+ });
6569
+ llmSpan?.finish();
6320
6570
  return { shouldContinue: false };
6321
6571
  }
6322
6572
  // Check if we've exhausted retries or error is not retryable
@@ -6325,6 +6575,7 @@ class StreamLoop {
6325
6575
  log.error(`Stream request failed after ${this.retryPolicy.maxRetries} retries`);
6326
6576
  log.var({ error });
6327
6577
  const errorMessage = error instanceof Error ? error.message : String(error);
6578
+ llmSpan?.finish();
6328
6579
  throw new errors.BadGatewayError(errorMessage);
6329
6580
  }
6330
6581
  const delay = this.retryPolicy.getDelayForAttempt(attempt);
@@ -6338,6 +6589,13 @@ class StreamLoop {
6338
6589
  finally {
6339
6590
  guard.remove();
6340
6591
  }
6592
+ // Annotate and finish the streamed llm span (no-op when disabled)
6593
+ llmSpan?.annotate({
6594
+ inputData: inputSnapshot,
6595
+ metrics: usageToLlmObsMetrics(state.usageItems),
6596
+ outputData: streamedText,
6597
+ });
6598
+ llmSpan?.finish();
6341
6599
  // Execute afterEachModelResponse hook
6342
6600
  await this.hookRunnerInstance.runAfterModelResponse(context.hooks, {
6343
6601
  content: "",
@@ -6372,7 +6630,7 @@ class StreamLoop {
6372
6630
  }
6373
6631
  return { shouldContinue: false };
6374
6632
  }
6375
- async *processToolCalls(toolCalls, state, context, _options) {
6633
+ async *processToolCalls(toolCalls, state, context) {
6376
6634
  const log = getLogger$6();
6377
6635
  for (const toolCall of toolCalls) {
6378
6636
  try {
@@ -6381,11 +6639,19 @@ class StreamLoop {
6381
6639
  args: toolCall.arguments,
6382
6640
  toolName: toolCall.name,
6383
6641
  });
6384
- // Call the tool
6642
+ // Call the tool inside a child tool span (no-op when disabled)
6385
6643
  log.trace(`[stream] Calling tool - ${toolCall.name}`);
6386
- const result = await state.toolkit.call({
6387
- arguments: toolCall.arguments,
6388
- name: toolCall.name,
6644
+ const result = await withLlmObsSpan({ kind: "tool", name: toolCall.name }, async () => {
6645
+ const result = await state.toolkit.call({
6646
+ arguments: toolCall.arguments,
6647
+ name: toolCall.name,
6648
+ });
6649
+ annotateLlmObs({
6650
+ inputData: toolCall.arguments,
6651
+ metadata: { tool: toolCall.name },
6652
+ outputData: result,
6653
+ });
6654
+ return result;
6389
6655
  });
6390
6656
  // Execute afterEachTool hook
6391
6657
  await this.hookRunnerInstance.runAfterTool(context.hooks, {