@jaypie/llm 1.3.5 → 1.3.7

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.
@@ -101,7 +101,10 @@ const PROVIDER = {
101
101
  ANTHROPIC: {
102
102
  // https://docs.anthropic.com/en/docs/about-claude/models/overview
103
103
  MAX_TOKENS: {
104
- DEFAULT: 4096,
104
+ // Non-streaming ceiling: responses above ~16K output tokens risk HTTP
105
+ // timeouts; streaming requests resolve to the model maximum instead
106
+ // (see util/maxOutputTokens.ts)
107
+ DEFAULT: 16384,
105
108
  },
106
109
  MODEL: {
107
110
  DEFAULT: FIRST_CLASS_PROVIDER.ANTHROPIC.DEFAULT,
@@ -450,14 +453,12 @@ class BaseProviderAdapter {
450
453
  /**
451
454
  * Default implementation returns false - override for providers with native structured output
452
455
  */
453
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
454
456
  hasStructuredOutput(_response) {
455
457
  return false;
456
458
  }
457
459
  /**
458
460
  * Default implementation returns undefined - override for providers with native structured output
459
461
  */
460
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
461
462
  extractStructuredOutput(_response) {
462
463
  return undefined;
463
464
  }
@@ -489,6 +490,18 @@ var LlmResponseStatus;
489
490
  LlmResponseStatus["Incomplete"] = "incomplete";
490
491
  LlmResponseStatus["InProgress"] = "in_progress";
491
492
  })(LlmResponseStatus || (LlmResponseStatus = {}));
493
+ // Progress
494
+ exports.LlmProgressEventType = void 0;
495
+ (function (LlmProgressEventType) {
496
+ LlmProgressEventType["Done"] = "done";
497
+ LlmProgressEventType["ModelRequest"] = "model_request";
498
+ LlmProgressEventType["ModelResponse"] = "model_response";
499
+ LlmProgressEventType["Retry"] = "retry";
500
+ LlmProgressEventType["Start"] = "start";
501
+ LlmProgressEventType["ToolCall"] = "tool_call";
502
+ LlmProgressEventType["ToolError"] = "tool_error";
503
+ LlmProgressEventType["ToolResult"] = "tool_result";
504
+ })(exports.LlmProgressEventType || (exports.LlmProgressEventType = {}));
492
505
 
493
506
  //
494
507
  //
@@ -925,6 +938,52 @@ function jsonSchemaToOpenApi3(schema) {
925
938
 
926
939
  const getLogger$6 = () => log$1.log.lib({ lib: kit.JAYPIE.LIB.LLM });
927
940
 
941
+ //
942
+ //
943
+ // Constants
944
+ //
945
+ // Non-streaming requests above ~16K output tokens risk HTTP timeouts
946
+ // (Anthropic guidance is to stream anything larger), so non-streaming
947
+ // defaults are capped here and only streaming requests resolve to the
948
+ // full model maximum.
949
+ const NON_STREAMING_MAX_TOKENS = PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT;
950
+ // Maximum output tokens by model; first match wins.
951
+ // Providers without low output ceilings (OpenAI, xAI) are intentionally
952
+ // absent — their requests leave the limit unset.
953
+ const MODEL_MAX_OUTPUT_TOKENS = [
954
+ // Anthropic — https://platform.claude.com/docs/en/about-claude/models/overview
955
+ { pattern: /^claude-opus-4-(0$|1$|1-|2025)/, tokens: 32000 },
956
+ { pattern: /^claude-opus-4-5/, tokens: 64000 },
957
+ { pattern: /^claude-sonnet-4-(0$|5$|5-|2025)/, tokens: 64000 },
958
+ { pattern: /haiku/, tokens: 64000 },
959
+ { pattern: /claude|fable|mythos|opus|sonnet/, tokens: 128000 },
960
+ // Google — https://ai.google.dev/gemini-api/docs/models
961
+ { pattern: /gemini-(2\.5|3)/, tokens: 65536 },
962
+ { pattern: /gemini/, tokens: 8192 },
963
+ ];
964
+ //
965
+ //
966
+ // Main
967
+ //
968
+ /**
969
+ * Maximum output tokens the model supports, or undefined when unknown.
970
+ */
971
+ function maxOutputTokens(model) {
972
+ const match = MODEL_MAX_OUTPUT_TOKENS.find(({ pattern }) => pattern.test(model));
973
+ return match?.tokens;
974
+ }
975
+ /**
976
+ * Default output token limit for a request: the model maximum when
977
+ * streaming, capped at the non-streaming maximum otherwise. Returns
978
+ * undefined when the model's maximum is unknown.
979
+ */
980
+ function resolveMaxOutputTokens(model, { stream = false } = {}) {
981
+ const max = maxOutputTokens(model);
982
+ if (max === undefined)
983
+ return undefined;
984
+ return stream ? max : Math.min(max, NON_STREAMING_MAX_TOKENS);
985
+ }
986
+
928
987
  // Turn policy constants
929
988
  const MAX_TURNS_ABSOLUTE_LIMIT = 72;
930
989
  const MAX_TURNS_DEFAULT_LIMIT = 24;
@@ -1749,11 +1808,13 @@ class AnthropicAdapter extends BaseProviderAdapter {
1749
1808
  lastMsg.content = lastMsg.content + "\n\n" + request.instructions;
1750
1809
  }
1751
1810
  }
1811
+ const model = (request.model ||
1812
+ this.defaultModel);
1752
1813
  const anthropicRequest = {
1753
- model: (request.model ||
1754
- this.defaultModel),
1814
+ model,
1755
1815
  messages,
1756
- max_tokens: PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
1816
+ max_tokens: resolveMaxOutputTokens(model, { stream: request.stream }) ??
1817
+ PROVIDER.ANTHROPIC.MAX_TOKENS.DEFAULT,
1757
1818
  stream: false,
1758
1819
  };
1759
1820
  if (request.system) {
@@ -1816,7 +1877,6 @@ class AnthropicAdapter extends BaseProviderAdapter {
1816
1877
  // outputSchema is part of the interface contract but Anthropic now uses
1817
1878
  // native `output_format` (set in buildRequest), so we no longer inject a
1818
1879
  // synthetic structured-output tool here.
1819
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1820
1880
  _outputSchema) {
1821
1881
  return toolkit.tools.map((tool) => ({
1822
1882
  name: tool.name,
@@ -3003,6 +3063,18 @@ class GoogleAdapter extends BaseProviderAdapter {
3003
3063
  : structuredOutputInstruction,
3004
3064
  };
3005
3065
  }
3066
+ // Default output ceiling: Google's own default (8,192) silently
3067
+ // truncates long generations, so resolve to the model maximum (capped
3068
+ // for non-streaming); providerOptions below override
3069
+ const maxOutputTokens = resolveMaxOutputTokens(geminiRequest.model, {
3070
+ stream: request.stream,
3071
+ });
3072
+ if (maxOutputTokens !== undefined) {
3073
+ geminiRequest.config = {
3074
+ ...geminiRequest.config,
3075
+ maxOutputTokens,
3076
+ };
3077
+ }
3006
3078
  // Add provider-specific options
3007
3079
  if (request.providerOptions) {
3008
3080
  geminiRequest.config = {
@@ -3024,7 +3096,6 @@ class GoogleAdapter extends BaseProviderAdapter {
3024
3096
  // structured output via `responseJsonSchema`/`responseSchema` (or the
3025
3097
  // legacy fake-tool injected in buildRequest as a fallback). We no longer
3026
3098
  // inject a synthetic structured-output tool here.
3027
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
3028
3099
  _outputSchema) {
3029
3100
  return toolkit.tools.map((tool) => ({
3030
3101
  name: tool.name,
@@ -4689,7 +4760,6 @@ class OpenRouterAdapter extends BaseProviderAdapter {
4689
4760
  // native `response_format` (set in buildRequest), so we no longer inject a
4690
4761
  // synthetic structured-output tool here. The legacy fake-tool injection
4691
4762
  // happens in buildRequest only as a runtime fallback.
4692
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
4693
4763
  _outputSchema) {
4694
4764
  return toolkit.tools.map((tool) => ({
4695
4765
  name: tool.name,
@@ -6075,6 +6145,25 @@ class InputProcessor {
6075
6145
  // Export singleton instance for convenience
6076
6146
  const inputProcessor = new InputProcessor();
6077
6147
 
6148
+ /**
6149
+ * Deliver a progress event to the caller's onProgress callback.
6150
+ * Errors thrown by the callback are logged and swallowed — progress
6151
+ * reporting must never interrupt the operate loop.
6152
+ */
6153
+ async function emitProgress({ event, onProgress, }) {
6154
+ if (!onProgress) {
6155
+ return;
6156
+ }
6157
+ try {
6158
+ await onProgress(event);
6159
+ }
6160
+ catch (error) {
6161
+ const log = getLogger$6();
6162
+ log.warn(`[operate] onProgress callback threw on "${event.type}" event`);
6163
+ log.var({ error });
6164
+ }
6165
+ }
6166
+
6078
6167
  //
6079
6168
  //
6080
6169
  // Main
@@ -6500,12 +6589,21 @@ class OperateLoop {
6500
6589
  const log = getLogger$6();
6501
6590
  // Log what was passed to operate
6502
6591
  log.trace("[operate] Starting operate loop");
6503
- log.var({ "operate.input": input });
6504
- log.var({ "operate.options": options });
6592
+ log.trace.var({ "operate.input": input });
6593
+ log.trace.var({ "operate.options": options });
6505
6594
  // Initialize state
6506
6595
  const state = await this.initializeState(input, options);
6507
6596
  const context = this.createContext(options);
6508
6597
  const modelName = options.model ?? this.adapter.defaultModel;
6598
+ await emitProgress({
6599
+ event: {
6600
+ maxTurns: state.maxTurns,
6601
+ model: modelName,
6602
+ provider: this.adapter.name,
6603
+ type: exports.LlmProgressEventType.Start,
6604
+ },
6605
+ onProgress: options.onProgress,
6606
+ });
6509
6607
  // Enclosing LLM Observability span (no-op when DD_LLMOBS_ENABLED is unset).
6510
6608
  // Child llm/tool spans nest under it via the SDK's active-span context.
6511
6609
  return withLlmObsSpan({
@@ -6543,6 +6641,15 @@ class OperateLoop {
6543
6641
  metrics: usageToLlmObsMetrics(response.usage),
6544
6642
  outputData: response.content,
6545
6643
  });
6644
+ await emitProgress({
6645
+ event: {
6646
+ content: response.content,
6647
+ turn: state.currentTurn,
6648
+ type: exports.LlmProgressEventType.Done,
6649
+ usage: response.usage,
6650
+ },
6651
+ onProgress: options.onProgress,
6652
+ });
6546
6653
  return response;
6547
6654
  });
6548
6655
  }
@@ -6637,18 +6744,54 @@ class OperateLoop {
6637
6744
  });
6638
6745
  // Build provider-specific request
6639
6746
  const providerRequest = this.adapter.buildRequest(request);
6640
- // Log what was passed to the model
6747
+ // Log a draconian subset of the request; the full payload is available
6748
+ // via the beforeEachModelRequest hook
6641
6749
  log.trace("[operate] Calling model");
6642
- log.var({ "operate.request": providerRequest });
6750
+ log.trace.var({
6751
+ "operate.request": {
6752
+ latest: this.summarizeHistoryItem(request.messages[request.messages.length - 1]),
6753
+ messages: request.messages.length,
6754
+ model: request.model,
6755
+ turn: state.currentTurn,
6756
+ },
6757
+ });
6758
+ await emitProgress({
6759
+ event: {
6760
+ model: request.model,
6761
+ turn: state.currentTurn,
6762
+ type: exports.LlmProgressEventType.ModelRequest,
6763
+ },
6764
+ onProgress: options.onProgress,
6765
+ });
6643
6766
  // Execute beforeEachModelRequest hook
6644
6767
  await this.hookRunnerInstance.runBeforeModelRequest(context.hooks, {
6645
6768
  input: state.currentInput,
6646
6769
  options,
6647
6770
  providerRequest,
6648
6771
  });
6772
+ // Emit retry progress alongside the caller's onRetryableModelError hook
6773
+ const hooks = context.hooks;
6774
+ const hooksWithProgress = options.onProgress
6775
+ ? {
6776
+ ...hooks,
6777
+ onRetryableModelError: async (retryContext) => {
6778
+ await emitProgress({
6779
+ event: {
6780
+ error: retryContext.error instanceof Error
6781
+ ? retryContext.error.message
6782
+ : String(retryContext.error),
6783
+ turn: state.currentTurn,
6784
+ type: exports.LlmProgressEventType.Retry,
6785
+ },
6786
+ onProgress: options.onProgress,
6787
+ });
6788
+ return hooks?.onRetryableModelError?.(retryContext);
6789
+ },
6790
+ }
6791
+ : hooks;
6649
6792
  // Execute with retry inside a child llm span (no-op when llmobs disabled).
6650
6793
  // RetryExecutor handles error hooks and throws appropriate errors.
6651
- const { parsed, response } = await withLlmObsSpan({
6794
+ const { parsed, response, toolCalls } = await withLlmObsSpan({
6652
6795
  kind: "llm",
6653
6796
  modelName: options.model ?? this.adapter.defaultModel,
6654
6797
  modelProvider: this.adapter.name,
@@ -6660,19 +6803,43 @@ class OperateLoop {
6660
6803
  options,
6661
6804
  providerRequest,
6662
6805
  },
6663
- hooks: context.hooks,
6806
+ hooks: hooksWithProgress,
6664
6807
  });
6665
- // Log what was returned from the model
6666
- log.trace("[operate] Model response received");
6667
- log.var({ "operate.response": response });
6668
6808
  // Parse response
6669
6809
  const parsed = this.adapter.parseResponse(response, options);
6810
+ const toolCalls = parsed.hasToolCalls
6811
+ ? this.adapter.extractToolCalls(response)
6812
+ : [];
6813
+ // Log only the text or tool calls; the full payload is available
6814
+ // via the afterEachModelResponse hook
6815
+ log.trace("[operate] Model response received");
6816
+ log.trace.var({
6817
+ "operate.response": toolCalls.length > 0
6818
+ ? toolCalls.map(({ arguments: args, name }) => ({
6819
+ arguments: args,
6820
+ name,
6821
+ }))
6822
+ : parsed.content,
6823
+ });
6670
6824
  annotateLlmObs({
6671
6825
  inputData: state.currentInput,
6672
6826
  metrics: usageToLlmObsMetrics(parsed.usage ? [parsed.usage] : undefined),
6673
6827
  outputData: parsed.content ?? "",
6674
6828
  });
6675
- return { parsed, response };
6829
+ return { parsed, response, toolCalls };
6830
+ });
6831
+ await emitProgress({
6832
+ event: {
6833
+ content: parsed.content,
6834
+ toolCalls: toolCalls.map(({ arguments: args, name }) => ({
6835
+ arguments: args,
6836
+ name,
6837
+ })),
6838
+ turn: state.currentTurn,
6839
+ type: exports.LlmProgressEventType.ModelResponse,
6840
+ usage: parsed.usage ? [parsed.usage] : undefined,
6841
+ },
6842
+ onProgress: options.onProgress,
6676
6843
  });
6677
6844
  // Track usage
6678
6845
  if (parsed.usage) {
@@ -6701,7 +6868,6 @@ class OperateLoop {
6701
6868
  }
6702
6869
  // Handle tool calls
6703
6870
  if (parsed.hasToolCalls) {
6704
- const toolCalls = this.adapter.extractToolCalls(response);
6705
6871
  if (toolCalls.length > 0 && state.toolkit && state.maxTurns > 1) {
6706
6872
  // Track updated provider request for tool results
6707
6873
  let currentProviderRequest = providerRequest;
@@ -6713,6 +6879,14 @@ class OperateLoop {
6713
6879
  // Process each tool call
6714
6880
  for (const toolCall of toolCalls) {
6715
6881
  try {
6882
+ await emitProgress({
6883
+ event: {
6884
+ tool: { arguments: toolCall.arguments, name: toolCall.name },
6885
+ turn: state.currentTurn,
6886
+ type: exports.LlmProgressEventType.ToolCall,
6887
+ },
6888
+ onProgress: options.onProgress,
6889
+ });
6716
6890
  // Execute beforeEachTool hook
6717
6891
  await this.hookRunnerInstance.runBeforeTool(context.hooks, {
6718
6892
  args: toolCall.arguments,
@@ -6732,6 +6906,14 @@ class OperateLoop {
6732
6906
  });
6733
6907
  return result;
6734
6908
  });
6909
+ await emitProgress({
6910
+ event: {
6911
+ tool: { name: toolCall.name },
6912
+ turn: state.currentTurn,
6913
+ type: exports.LlmProgressEventType.ToolResult,
6914
+ },
6915
+ onProgress: options.onProgress,
6916
+ });
6735
6917
  // Execute afterEachTool hook
6736
6918
  await this.hookRunnerInstance.runAfterTool(context.hooks, {
6737
6919
  args: toolCall.arguments,
@@ -6755,6 +6937,15 @@ class OperateLoop {
6755
6937
  state.responseBuilder.appendToHistory(toolResultFormatted);
6756
6938
  }
6757
6939
  catch (error) {
6940
+ await emitProgress({
6941
+ event: {
6942
+ error: error.message,
6943
+ tool: { name: toolCall.name },
6944
+ turn: state.currentTurn,
6945
+ type: exports.LlmProgressEventType.ToolError,
6946
+ },
6947
+ onProgress: options.onProgress,
6948
+ });
6758
6949
  // Execute onToolError hook
6759
6950
  await this.hookRunnerInstance.runOnToolError(context.hooks, {
6760
6951
  args: toolCall.arguments,
@@ -6828,6 +7019,20 @@ class OperateLoop {
6828
7019
  }
6829
7020
  return false; // Stop loop
6830
7021
  }
7022
+ /**
7023
+ * Draconian summary of a history item for trace logging: string message
7024
+ * content is kept; everything else is reduced to its type.
7025
+ */
7026
+ summarizeHistoryItem(item) {
7027
+ if (!item) {
7028
+ return undefined;
7029
+ }
7030
+ const record = item;
7031
+ if (typeof record.content === "string") {
7032
+ return { content: record.content, role: record.role };
7033
+ }
7034
+ return { type: record.type ?? "message" };
7035
+ }
6831
7036
  /**
6832
7037
  * Reconcile structured output against the declared `format` contract. A
6833
7038
  * declared `format` is a schema contract: returned keys should match the
@@ -7021,6 +7226,7 @@ class StreamLoop {
7021
7226
  messages: state.currentInput,
7022
7227
  model: options.model ?? this.adapter.defaultModel,
7023
7228
  providerOptions: options.providerOptions,
7229
+ stream: true,
7024
7230
  system: options.system,
7025
7231
  temperature: options.temperature,
7026
7232
  tools: state.formattedTools,
@@ -7089,6 +7295,7 @@ class StreamLoop {
7089
7295
  messages: state.currentInput,
7090
7296
  model: options.model ?? this.adapter.defaultModel,
7091
7297
  providerOptions: options.providerOptions,
7298
+ stream: true,
7092
7299
  system: options.system,
7093
7300
  temperature: options.temperature,
7094
7301
  tools: state.formattedTools,