llm-exe 3.0.0 → 3.0.2

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.mjs CHANGED
@@ -464,6 +464,48 @@ function formatErrorList(values, options) {
464
464
  }
465
465
  return parts.join(", ");
466
466
  }
467
+ function formatLlmExeErrorForLog(error) {
468
+ if (!error || typeof error !== "object") {
469
+ return formatErrorValue(error);
470
+ }
471
+ const e = error;
472
+ const name = typeof e.name === "string" && e.name ? e.name : "Error";
473
+ const message = typeof e.message === "string" ? e.message : "";
474
+ const code = typeof e.code === "string" ? e.code : void 0;
475
+ const category = typeof e.category === "string" ? e.category : void 0;
476
+ let header;
477
+ if (code) {
478
+ header = `${name} [${code}]: ${message}`;
479
+ } else if (category) {
480
+ header = `${name} [${category}]: ${message}`;
481
+ } else {
482
+ header = `${name}: ${message}`;
483
+ }
484
+ const chain = [];
485
+ let current = e.cause;
486
+ let depth = 0;
487
+ const seen = /* @__PURE__ */ new WeakSet();
488
+ while (current && depth < 5) {
489
+ if (typeof current === "object") {
490
+ if (seen.has(current)) {
491
+ chain.push("Caused by: [Circular]");
492
+ break;
493
+ }
494
+ seen.add(current);
495
+ }
496
+ const c = current;
497
+ const cName = typeof c.name === "string" && c.name ? c.name : "Error";
498
+ const cMsg = typeof c.message === "string" ? c.message : String(current);
499
+ const cCode = typeof c.code === "string" ? c.code : void 0;
500
+ chain.push(
501
+ cCode ? `Caused by: ${cName} [${cCode}]: ${cMsg}` : `Caused by: ${cName}: ${cMsg}`
502
+ );
503
+ current = c.cause;
504
+ depth++;
505
+ }
506
+ return chain.length > 0 ? `${header}
507
+ ${chain.join("\n")}` : header;
508
+ }
467
509
 
468
510
  // src/errors/createLlmExeError.ts
469
511
  var DOCS_BASE_URL = "https://llm-exe.com";
@@ -985,7 +1027,9 @@ var BaseExecutor = class {
985
1027
  return this;
986
1028
  }
987
1029
  on(eventName, fn) {
988
- return this.setHooks({ [eventName]: fn });
1030
+ return this.setHooks({
1031
+ [eventName]: fn
1032
+ });
989
1033
  }
990
1034
  off(eventName, fn) {
991
1035
  return this.removeHook(eventName, fn);
@@ -1083,8 +1127,8 @@ var CoreExecutor = class extends BaseExecutor {
1083
1127
  __publicField(this, "_handler");
1084
1128
  this._handler = fn.handler.bind(null);
1085
1129
  }
1086
- async handler(_input) {
1087
- return this._handler.call(null, _input);
1130
+ async handler(_input, _options, _context) {
1131
+ return this._handler.call(null, _input, _context);
1088
1132
  }
1089
1133
  };
1090
1134
 
@@ -3236,6 +3280,7 @@ __export(utils_exports, {
3236
3280
  assert: () => assert,
3237
3281
  asyncCallWithTimeout: () => asyncCallWithTimeout,
3238
3282
  defineSchema: () => defineSchema,
3283
+ escapeTemplateString: () => escapeTemplateString,
3239
3284
  filterObjectOnSchema: () => filterObjectOnSchema,
3240
3285
  guessProviderFromModel: () => guessProviderFromModel,
3241
3286
  importHelpers: () => importHelpers,
@@ -3308,6 +3353,12 @@ function importHelpers(_helpers) {
3308
3353
  return helpers;
3309
3354
  }
3310
3355
 
3356
+ // src/utils/modules/escapeTemplateString.ts
3357
+ function escapeTemplateString(input) {
3358
+ if (typeof input !== "string") return input;
3359
+ return input.replace(/\{\{/g, "\\{{");
3360
+ }
3361
+
3311
3362
  // src/utils/modules/replaceTemplateStringAsync.ts
3312
3363
  async function replaceTemplateStringAsync(templateString, substitutions = {}, configuration = {
3313
3364
  helpers: [],
@@ -3973,9 +4024,53 @@ function OutputOpenAIChat(result, _config) {
3973
4024
  };
3974
4025
  }
3975
4026
 
4027
+ // src/llm/config/_utils/imageContent.ts
4028
+ function isImageUrlContentBlock(block) {
4029
+ if (typeof block !== "object" || block === null) {
4030
+ return false;
4031
+ }
4032
+ const imageUrl = block.image_url;
4033
+ return typeof imageUrl === "object" && imageUrl !== null && typeof imageUrl.url === "string";
4034
+ }
4035
+ var DATA_URI_PATTERN = /^data:([^;,]+);base64,(.+)$/;
4036
+ function parseImageUrl(url, context) {
4037
+ if (typeof url !== "string" || url.trim() === "") {
4038
+ throw new LlmExeError("Image content block has an empty url", {
4039
+ code: "prompt.invalid_messages",
4040
+ context: {
4041
+ ...context,
4042
+ received: url,
4043
+ expected: "an https URL or a data: URI",
4044
+ resolution: "Set image_url.url to an https URL or a base64 data: URI (data:image/png;base64,...)."
4045
+ }
4046
+ });
4047
+ }
4048
+ if (url.startsWith("data:")) {
4049
+ const match = url.match(DATA_URI_PATTERN);
4050
+ if (!match) {
4051
+ throw new LlmExeError("Malformed data: URI in image content block", {
4052
+ code: "prompt.invalid_messages",
4053
+ context: {
4054
+ ...context,
4055
+ received: `${url.slice(0, 48)}...`,
4056
+ expected: "data:<media-type>;base64,<data>",
4057
+ resolution: "Images must be base64-encoded with an explicit media type, e.g. data:image/png;base64,iVBOR..."
4058
+ }
4059
+ });
4060
+ }
4061
+ return { kind: "base64", mediaType: match[1], data: match[2] };
4062
+ }
4063
+ return { kind: "url", url };
4064
+ }
4065
+
3976
4066
  // src/llm/config/openai/promptSanitizeMessageCallback.ts
3977
4067
  function openaiPromptMessageCallback(_message) {
3978
4068
  let message = { ..._message };
4069
+ if (Array.isArray(message.content)) {
4070
+ message.content = message.content.map(
4071
+ (block) => isImageUrlContentBlock(block) && block.type !== "image_url" ? { ...block, type: "image_url" } : block
4072
+ );
4073
+ }
3979
4074
  if (message.role === "function") {
3980
4075
  message.role = "tool";
3981
4076
  message.tool_call_id = message.id;
@@ -4181,6 +4276,9 @@ var openai = {
4181
4276
  "openai.chat.v1": openAiChatV1,
4182
4277
  "openai.chat-mock.v1": openAiChatMockV1,
4183
4278
  // GPT-5 family
4279
+ "openai.gpt-5.5": withDefaultModel(openAiChatV1, "gpt-5.5"),
4280
+ "openai.gpt-5.4": withDefaultModel(openAiChatV1, "gpt-5.4"),
4281
+ "openai.gpt-5.4-mini": withDefaultModel(openAiChatV1, "gpt-5.4-mini"),
4184
4282
  "openai.gpt-5.2": withDefaultModel(openAiChatV1, "gpt-5.2"),
4185
4283
  "openai.gpt-5-mini": withDefaultModel(openAiChatV1, "gpt-5-mini"),
4186
4284
  "openai.gpt-5-nano": withDefaultModel(openAiChatV1, "gpt-5-nano"),
@@ -4202,8 +4300,46 @@ var openai = {
4202
4300
  };
4203
4301
 
4204
4302
  // src/llm/config/anthropic/promptSanitizeMessageCallback.ts
4205
- function anthropicPromptMessageCallback(_message) {
4303
+ function anthropicPromptMessageCallback(_message, options = {}) {
4304
+ const { provider = "anthropic.chat", allowImageUrlSources = true } = options;
4206
4305
  let message = { ..._message };
4306
+ if (Array.isArray(message.content)) {
4307
+ message.content = message.content.map((block) => {
4308
+ if (!isImageUrlContentBlock(block)) {
4309
+ return block;
4310
+ }
4311
+ const parsed = parseImageUrl(block.image_url.url, {
4312
+ operation: "anthropicPromptMessageCallback",
4313
+ provider
4314
+ });
4315
+ if (parsed.kind === "base64") {
4316
+ return {
4317
+ type: "image",
4318
+ source: {
4319
+ type: "base64",
4320
+ media_type: parsed.mediaType,
4321
+ data: parsed.data
4322
+ }
4323
+ };
4324
+ }
4325
+ if (!allowImageUrlSources) {
4326
+ throw new LlmExeError("Image URLs are not supported by this provider", {
4327
+ code: "prompt.invalid_messages",
4328
+ context: {
4329
+ operation: "anthropicPromptMessageCallback",
4330
+ provider,
4331
+ received: parsed.url,
4332
+ expected: "a base64 data: URI",
4333
+ resolution: "Bedrock does not fetch remote images. Base64-encode the image and pass it as a data: URI (data:image/png;base64,...)."
4334
+ }
4335
+ });
4336
+ }
4337
+ return {
4338
+ type: "image",
4339
+ source: { type: "url", url: parsed.url }
4340
+ };
4341
+ });
4342
+ }
4207
4343
  if (message.role === "function") {
4208
4344
  message.role = "user";
4209
4345
  message.content = [
@@ -4249,10 +4385,11 @@ function mergeConsecutiveSameRole(messages) {
4249
4385
  }
4250
4386
  return merged;
4251
4387
  }
4252
- function anthropicPromptSanitize(_messages, _inputBodyObj, _outputObj) {
4388
+ function anthropicPromptSanitize(_messages, _inputBodyObj, _outputObj, _options = {}) {
4389
+ const toAnthropicMessage = (m) => anthropicPromptMessageCallback(m, _options);
4253
4390
  if (typeof _messages === "string") {
4254
4391
  return [{ role: "user", content: _messages }].map(
4255
- anthropicPromptMessageCallback
4392
+ toAnthropicMessage
4256
4393
  );
4257
4394
  }
4258
4395
  const [first, ...messages] = [
@@ -4262,7 +4399,7 @@ function anthropicPromptSanitize(_messages, _inputBodyObj, _outputObj) {
4262
4399
  return [
4263
4400
  { role: "user", content: first.content },
4264
4401
  ...messages
4265
- ].map(anthropicPromptMessageCallback);
4402
+ ].map(toAnthropicMessage);
4266
4403
  }
4267
4404
  if (first.role === "system" && messages.length > 0) {
4268
4405
  _outputObj.system = first.content;
@@ -4271,7 +4408,7 @@ function anthropicPromptSanitize(_messages, _inputBodyObj, _outputObj) {
4271
4408
  return { ...m, role: "user" };
4272
4409
  }
4273
4410
  return m;
4274
- }).map(anthropicPromptMessageCallback);
4411
+ }).map(toAnthropicMessage);
4275
4412
  return mergeConsecutiveSameRole(result2);
4276
4413
  }
4277
4414
  const result = [
@@ -4282,7 +4419,7 @@ function anthropicPromptSanitize(_messages, _inputBodyObj, _outputObj) {
4282
4419
  }
4283
4420
  return m;
4284
4421
  })
4285
- ].map(anthropicPromptMessageCallback);
4422
+ ].map(toAnthropicMessage);
4286
4423
  return mergeConsecutiveSameRole(result);
4287
4424
  }
4288
4425
 
@@ -4395,7 +4532,11 @@ var amazonAnthropicChatV1 = {
4395
4532
  mapBody: {
4396
4533
  prompt: {
4397
4534
  key: "messages",
4398
- transform: anthropicPromptSanitize
4535
+ transform: (v, i, o) => anthropicPromptSanitize(v, i, o, {
4536
+ provider: "amazon:anthropic.chat",
4537
+ // Bedrock's invoke API only accepts base64 image sources
4538
+ allowImageUrlSources: false
4539
+ })
4399
4540
  },
4400
4541
  topP: {
4401
4542
  key: "top_p"
@@ -4451,6 +4592,21 @@ var amazonMetaChatV1 = {
4451
4592
  if (typeof messages === "string") {
4452
4593
  return messages;
4453
4594
  } else {
4595
+ const hasImageContent = messages.some(
4596
+ (message) => Array.isArray(message?.content) && message.content.some(isImageUrlContentBlock)
4597
+ );
4598
+ if (hasImageContent) {
4599
+ throw new LlmExeError("Image content is not supported", {
4600
+ code: "prompt.invalid_messages",
4601
+ context: {
4602
+ operation: "amazonMetaChatV1.prompt.transform",
4603
+ provider: "amazon:meta.chat",
4604
+ received: "a message with image content",
4605
+ expected: "text-only messages",
4606
+ resolution: "This model accepts a flattened text prompt and cannot receive images."
4607
+ }
4608
+ });
4609
+ }
4454
4610
  return replaceTemplateString(`{{>DialogueHistory key='messages'}}`, {
4455
4611
  messages
4456
4612
  });
@@ -4478,7 +4634,11 @@ var bedrock = {
4478
4634
 
4479
4635
  // src/llm/config/anthropic/index.ts
4480
4636
  var ANTHROPIC_VERSION = "2023-06-01";
4481
- var MODELS_REJECTING_SAMPLING_PARAMS = ["claude-opus-4-7", "claude-opus-4-8"];
4637
+ var MODELS_REJECTING_SAMPLING_PARAMS = [
4638
+ "claude-opus-4-7",
4639
+ "claude-opus-4-8",
4640
+ "claude-fable-5"
4641
+ ];
4482
4642
  var isClaude4x = (model) => /^claude-(opus|sonnet|haiku)-4-/.test(model);
4483
4643
  var dropIfModelRejectsSamplingParams = (v, body) => MODELS_REJECTING_SAMPLING_PARAMS.includes(body.model) ? void 0 : v;
4484
4644
  var topPTransform = (v, body) => {
@@ -4499,6 +4659,15 @@ var anthropicChatV1 = {
4499
4659
  required: [true, "maxTokens required"],
4500
4660
  default: 4096
4501
4661
  },
4662
+ // Every key mapped in mapBody must also be declared here:
4663
+ // stateFromOptions picks only declared option keys, so an undeclared
4664
+ // key never reaches mapBody (see issue #661).
4665
+ temperature: {},
4666
+ topP: {},
4667
+ topK: {},
4668
+ stopSequences: {},
4669
+ metadata: {},
4670
+ serviceTier: {},
4502
4671
  anthropicApiKey: {
4503
4672
  default: getEnvironmentVariable("ANTHROPIC_API_KEY")
4504
4673
  }
@@ -4532,9 +4701,8 @@ var anthropicChatV1 = {
4532
4701
  stopSequences: {
4533
4702
  key: "stop_sequences"
4534
4703
  },
4535
- stream: {
4536
- key: "stream"
4537
- },
4704
+ // No `stream` mapping: the request pipeline has no SSE support, so
4705
+ // forwarding stream=true would return an unparseable response.
4538
4706
  metadata: {
4539
4707
  key: "metadata"
4540
4708
  },
@@ -4563,6 +4731,11 @@ var anthropicChatV1 = {
4563
4731
  };
4564
4732
  var anthropic = {
4565
4733
  "anthropic.chat.v1": anthropicChatV1,
4734
+ // Claude Fable 5 models
4735
+ "anthropic.claude-fable-5": withDefaultModel(
4736
+ anthropicChatV1,
4737
+ "claude-fable-5"
4738
+ ),
4566
4739
  // Claude 4.8 models
4567
4740
  "anthropic.claude-opus-4-8": withDefaultModel(
4568
4741
  anthropicChatV1,
@@ -4596,10 +4769,9 @@ var anthropic = {
4596
4769
  config: withDefaultModel(anthropicChatV1, "claude-opus-4-6"),
4597
4770
  message: 'Shorthand "anthropic.claude-opus-4-6" is deprecated and may be removed in a future release.'
4598
4771
  }),
4599
- ...deprecateShorthand("anthropic.claude-opus-4-1", {
4600
- config: withDefaultModel(anthropicChatV1, "claude-opus-4-1-20250805"),
4601
- message: 'Shorthand "anthropic.claude-opus-4-1" is deprecated and may be removed in a future release.'
4602
- }),
4772
+ // NOTE: "anthropic.claude-opus-4-1" (claude-opus-4-1-20250805) was removed —
4773
+ // Anthropic retires that model on Aug 5, 2026. Migrate to
4774
+ // anthropic.claude-opus-4-5/-6/-7/-8.
4603
4775
  ...deprecateShorthand("anthropic.claude-sonnet-4", {
4604
4776
  config: withDefaultModel(anthropicChatV1, "claude-sonnet-4-0"),
4605
4777
  message: 'Shorthand "anthropic.claude-sonnet-4" is deprecated and may be removed in a future release.'
@@ -4745,6 +4917,60 @@ function OutputOllamaChat(result, _config) {
4745
4917
  };
4746
4918
  }
4747
4919
 
4920
+ // src/llm/config/ollama/promptSanitize.ts
4921
+ function ollamaPromptSanitize(_messages) {
4922
+ if (typeof _messages === "string") {
4923
+ return [{ role: "user", content: _messages }];
4924
+ }
4925
+ return _messages.map((_message) => {
4926
+ if (!Array.isArray(_message.content)) {
4927
+ return _message;
4928
+ }
4929
+ const message = { ..._message };
4930
+ const textParts = [];
4931
+ const images = [];
4932
+ for (const block of message.content) {
4933
+ if (isImageUrlContentBlock(block)) {
4934
+ const parsed = parseImageUrl(block.image_url.url, {
4935
+ operation: "ollamaPromptSanitize",
4936
+ provider: "ollama.chat"
4937
+ });
4938
+ if (parsed.kind === "url") {
4939
+ throw new LlmExeError("Image URLs are not supported by ollama", {
4940
+ code: "prompt.invalid_messages",
4941
+ context: {
4942
+ operation: "ollamaPromptSanitize",
4943
+ provider: "ollama.chat",
4944
+ received: parsed.url,
4945
+ expected: "a base64 data: URI",
4946
+ resolution: "Ollama does not fetch remote images. Base64-encode the image and pass it as a data: URI (data:image/png;base64,...)."
4947
+ }
4948
+ });
4949
+ }
4950
+ images.push(parsed.data);
4951
+ } else if (typeof block?.text === "string") {
4952
+ textParts.push(block.text);
4953
+ } else {
4954
+ throw new LlmExeError("Unsupported content block for ollama", {
4955
+ code: "prompt.invalid_messages",
4956
+ context: {
4957
+ operation: "ollamaPromptSanitize",
4958
+ provider: "ollama.chat",
4959
+ received: block,
4960
+ expected: "text or image_url content blocks",
4961
+ resolution: "Ollama messages only support text and base64 images."
4962
+ }
4963
+ });
4964
+ }
4965
+ }
4966
+ message.content = textParts.join("\n");
4967
+ if (images.length > 0) {
4968
+ message.images = images;
4969
+ }
4970
+ return message;
4971
+ });
4972
+ }
4973
+
4748
4974
  // src/llm/config/ollama/index.ts
4749
4975
  var ollamaChatV1 = {
4750
4976
  key: "ollama.chat.v1",
@@ -4762,12 +4988,7 @@ var ollamaChatV1 = {
4762
4988
  mapBody: {
4763
4989
  prompt: {
4764
4990
  key: "messages",
4765
- transform: (v) => {
4766
- if (typeof v === "string") {
4767
- return [{ role: "user", content: v }];
4768
- }
4769
- return v;
4770
- }
4991
+ transform: (v) => ollamaPromptSanitize(v)
4771
4992
  },
4772
4993
  model: {
4773
4994
  key: "model"
@@ -4804,6 +5025,37 @@ var ollama = {
4804
5025
  };
4805
5026
 
4806
5027
  // src/llm/config/google/promptSanitizeMessageCallback.ts
5028
+ var GOOGLE_SUPPORTED_FILE_URI = /^(gs:\/\/|https:\/\/generativelanguage\.googleapis\.com\/)/;
5029
+ function googleGeminiContentBlockToPart(block) {
5030
+ if (isImageUrlContentBlock(block)) {
5031
+ const parsed = parseImageUrl(block.image_url.url, {
5032
+ operation: "googleGeminiPromptMessageCallback",
5033
+ provider: "google.chat"
5034
+ });
5035
+ if (parsed.kind === "base64") {
5036
+ return {
5037
+ inlineData: { mimeType: parsed.mediaType, data: parsed.data }
5038
+ };
5039
+ }
5040
+ if (GOOGLE_SUPPORTED_FILE_URI.test(parsed.url)) {
5041
+ return { fileData: { fileUri: parsed.url } };
5042
+ }
5043
+ throw new LlmExeError("Gemini cannot load images from arbitrary URLs", {
5044
+ code: "prompt.invalid_messages",
5045
+ context: {
5046
+ operation: "googleGeminiPromptMessageCallback",
5047
+ provider: "google.chat",
5048
+ received: parsed.url,
5049
+ expected: "a base64 data: URI, a Files API URI, or a gs:// URI",
5050
+ resolution: "Pass the image as a data: URI (data:image/png;base64,...), or upload it with the Gemini Files API and pass the returned file URI."
5051
+ }
5052
+ });
5053
+ }
5054
+ if (typeof block?.text === "string") {
5055
+ return { text: block.text };
5056
+ }
5057
+ return block;
5058
+ }
4807
5059
  function googleGeminiPromptMessageCallback(_message) {
4808
5060
  let message = { ..._message };
4809
5061
  const parts = [];
@@ -4817,6 +5069,9 @@ function googleGeminiPromptMessageCallback(_message) {
4817
5069
  if (typeof payload.content === "string" && message.role !== "function") {
4818
5070
  parts.push({ text: message.content });
4819
5071
  }
5072
+ if (Array.isArray(payload.content) && message.role !== "function") {
5073
+ parts.push(...payload.content.map(googleGeminiContentBlockToPart));
5074
+ }
4820
5075
  if (message.role === "function") {
4821
5076
  role = "user";
4822
5077
  parts.push({
@@ -7633,10 +7888,12 @@ export {
7633
7888
  createState,
7634
7889
  createStateItem,
7635
7890
  defineSchema,
7891
+ formatLlmExeErrorForLog,
7636
7892
  guards_exports as guards,
7637
7893
  isLlmExeError,
7638
7894
  registerHelpers,
7639
7895
  registerPartials,
7896
+ serializeLlmExeError,
7640
7897
  useExecutors,
7641
7898
  useLlm,
7642
7899
  useLlmConfiguration,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-exe",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "Simplify building LLM-powered apps with easy-to-use base components, supporting text and chat-based prompts with handlebars template engine, output parsers, and flexible function calling capabilities.",
5
5
  "keywords": [
6
6
  "ai",
@@ -48,6 +48,7 @@
48
48
  "predocs:build": "npm run build:docs-examples && bash scripts/generate-llms-txt.sh",
49
49
  "docs:dev": "eval $(cat docs/.env) && concurrently \"vitepress dev docs\" \"npm run build:watch:docs-examples\"",
50
50
  "docs:build": "eval $(cat docs/.env) && vitepress build docs",
51
+ "postdocs:build": "node docs/.vitepress/scripts/verify-docs-build.mjs",
51
52
  "docs:update-providers": "ts-node --transpile-only -r tsconfig-paths/register docs/.vitepress/scripts/updateProviders.ts",
52
53
  "lint": "eslint .",
53
54
  "format:check": "prettier --check \"src\"",
package/readme.md CHANGED
@@ -14,7 +14,7 @@ A package that provides simplified base components to make building and maintain
14
14
  - Allow LLM's to call functions (or call other LLM executors).
15
15
  - Not very opinionated. You have control on how you use it.
16
16
 
17
- ![llm-exe](https://assets.llm-exe.com/llm-exe-featured-2025.png)
17
+ ![llm-exe project logo](https://assets.llm-exe.com/llm-exe-featured-2025.png)
18
18
 
19
19
  See full docs here: [https://llm-exe.com](https://llm-exe.com)
20
20