extrait 0.5.1 → 0.5.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/README.md CHANGED
@@ -70,6 +70,7 @@ const llm = createLLM({
70
70
  mode: "loose" | "strict", // loose allows repair
71
71
  selfHeal: 0 | 1 | 2, // retry attempts
72
72
  debug: false, // show repair logs
73
+ timeout: { request: 30_000 }, // optional default timeouts
73
74
  },
74
75
  });
75
76
  ```
@@ -159,11 +160,50 @@ const result = await llm.structured(
159
160
  request: {
160
161
  signal: abortController.signal, // optional AbortSignal
161
162
  },
163
+ timeout: {
164
+ request: 30_000, // ms per LLM HTTP request
165
+ tool: 10_000, // ms per MCP tool call
166
+ },
162
167
  }
163
168
  );
164
169
  ```
165
170
 
166
- `prompt()` builds an ordered `messages` payload. Use `prompt\`...\`` for a single string prompt, or the fluent builder for multi-turn conversations. The `LLMMessage` type is exported if you need to type your own message arrays.
171
+ `prompt()` builds an ordered `messages` payload. Use ``prompt`...` `` for a single string prompt, or the fluent builder for multi-turn conversations. The `LLMMessage` type is exported if you need to type your own message arrays.
172
+
173
+ ### Images (multimodal)
174
+
175
+ Use `images()` to build base64 image content blocks for vision-capable models.
176
+
177
+ ```typescript
178
+ import { images } from "extrait";
179
+ import { readFileSync } from "fs";
180
+
181
+ const base64 = readFileSync("photo.png").toString("base64");
182
+
183
+ // Single image
184
+ const result = await llm.structured(Schema, {
185
+ messages: [
186
+ {
187
+ role: "user",
188
+ content: [
189
+ { type: "text", text: "Describe this image." },
190
+ ...images({ base64, mimeType: "image/png" }),
191
+ ],
192
+ },
193
+ ],
194
+ });
195
+
196
+ // Multiple images
197
+ const content = [
198
+ { type: "text", text: "Compare these two images." },
199
+ ...images([
200
+ { base64: base64A, mimeType: "image/png" },
201
+ { base64: base64B, mimeType: "image/jpeg" },
202
+ ]),
203
+ ];
204
+ ```
205
+
206
+ `images()` accepts a single `{ base64, mimeType }` object or an array, and always returns an `LLMImageContent[]` that spreads directly into a content array.
167
207
 
168
208
  ### Result Object
169
209
 
@@ -246,6 +286,34 @@ const result = await llm.structured(
246
286
  await mcpClient.close?.();
247
287
  ```
248
288
 
289
+ ### Timeouts
290
+
291
+ Use `timeout` to set per-request and per-tool-call time limits without managing `AbortSignal` manually.
292
+
293
+ ```typescript
294
+ const result = await llm.structured(Schema, prompt`...`, {
295
+ timeout: {
296
+ request: 30_000, // abort the LLM HTTP request after 30s
297
+ tool: 5_000, // abort each MCP tool call after 5s
298
+ },
299
+ });
300
+ ```
301
+
302
+ Both fields are optional. `timeout.request` creates an `AbortSignal.timeout` internally; it is ignored if you also pass `request.signal` (your signal takes precedence). `timeout.tool` wraps each MCP client transparently.
303
+
304
+ You can also set defaults on the client:
305
+
306
+ ```typescript
307
+ const llm = createLLM({
308
+ provider: "openai-compatible",
309
+ model: "gpt-5-nano",
310
+ transport: { apiKey: process.env.LLM_API_KEY },
311
+ defaults: {
312
+ timeout: { request: 60_000 },
313
+ },
314
+ });
315
+ ```
316
+
249
317
  ## Examples
250
318
 
251
319
  Run examples with: `bun run dev <example-name>`
@@ -254,17 +322,20 @@ Available examples:
254
322
  - `streaming` - Real LLM streaming + snapshot self-check ([streaming.ts](examples/streaming.ts))
255
323
  - `streaming-with-tools` - Real text streaming with MCP tools + self-check ([streaming-with-tools.ts](examples/streaming-with-tools.ts))
256
324
  - `abort-signal` - Start a generation then cancel quickly with `AbortSignal` ([abort-signal.ts](examples/abort-signal.ts))
325
+ - `timeout` - Set per-request and per-tool timeouts via the `timeout` option ([timeout.ts](examples/timeout.ts))
257
326
  - `simple` - Basic structured output with streaming ([simple.ts](examples/simple.ts))
258
327
  - `sentiment-analysis` - Enum validation, strict mode ([sentiment-analysis.ts](examples/sentiment-analysis.ts))
259
328
  - `data-extraction` - Complex nested schemas, self-healing ([data-extraction.ts](examples/data-extraction.ts))
260
329
  - `multi-step-reasoning` - Chained structured calls ([multi-step-reasoning.ts](examples/multi-step-reasoning.ts))
261
330
  - `calculator-tool` - MCP tool integration ([calculator-tool.ts](examples/calculator-tool.ts))
331
+ - `image-analysis` - Multimodal structured extraction from an image file ([image-analysis.ts](examples/image-analysis.ts))
262
332
 
263
333
  Pass arguments after the example name:
264
334
  ```bash
265
335
  bun run dev streaming
266
336
  bun run dev streaming-with-tools
267
337
  bun run dev abort-signal 120 "JSON cancellation demo"
338
+ bun run dev timeout 5000
268
339
  bun run dev simple "Bun.js runtime"
269
340
  bun run dev sentiment-analysis "I love this product."
270
341
  bun run dev multi-step-reasoning "Why is the sky blue?"
@@ -0,0 +1,8 @@
1
+ import type { LLMImageContent } from "./types";
2
+ export interface ImageInput {
3
+ base64: string;
4
+ mimeType: string;
5
+ }
6
+ export type ImageSize = "low" | "mid" | "high" | "xhigh" | "raw" | number;
7
+ export declare function images(input: ImageInput | ImageInput[]): LLMImageContent[];
8
+ export declare function resizeImage(source: string | Uint8Array | ArrayBuffer, size: ImageSize, mimeType?: string): Promise<ImageInput>;
package/dist/index.cjs CHANGED
@@ -45,11 +45,13 @@ __export(exports_src, {
45
45
  sanitizeThink: () => sanitizeThink,
46
46
  s: () => s,
47
47
  resolveSchemaInstruction: () => resolveSchemaInstruction,
48
+ resizeImage: () => resizeImage,
48
49
  registerBuiltinProviders: () => registerBuiltinProviders,
49
50
  prompt: () => prompt,
50
51
  parseLLMOutput: () => parseLLMOutput,
51
52
  inspectSchemaMetadata: () => inspectSchemaMetadata,
52
53
  inferSchemaExample: () => inferSchemaExample,
54
+ images: () => images,
53
55
  formatZodIssues: () => formatZodIssues,
54
56
  formatPrompt: () => formatPrompt,
55
57
  extractMarkdownCodeBlocks: () => extractMarkdownCodeBlocks,
@@ -3927,19 +3929,24 @@ async function structured(adapter, schemaOrOptions, promptInput, callOptions) {
3927
3929
  const resolvedPrompt = applyPromptOutdent(resolvePrompt(normalized.prompt, { mode }), useOutdent);
3928
3930
  const resolvedSystemPrompt = applyOutdentToOptionalPrompt(normalized.systemPrompt, useOutdent);
3929
3931
  const preparedPrompt = prepareStructuredPromptPayload(resolvedPrompt, resolvedSystemPrompt, normalized.schema, normalized.schemaInstruction);
3932
+ const resolvedRequest = normalized.timeout?.tool !== undefined && normalized.request?.mcpClients !== undefined ? {
3933
+ ...normalized.request,
3934
+ mcpClients: applyToolTimeout(normalized.request.mcpClients, normalized.timeout.tool)
3935
+ } : normalized.request;
3930
3936
  const first = await executeAttempt(adapter, {
3931
3937
  prompt: preparedPrompt.prompt,
3932
3938
  messages: preparedPrompt.messages,
3933
3939
  schema: normalized.schema,
3934
3940
  parseOptions,
3935
3941
  stream: streamConfig,
3936
- request: normalized.request,
3942
+ request: resolvedRequest,
3937
3943
  systemPrompt: preparedPrompt.systemPrompt,
3938
3944
  observe: normalized.observe,
3939
3945
  debug: debugConfig,
3940
3946
  attemptNumber: 1,
3941
3947
  selfHeal: false,
3942
- selfHealEnabled: selfHealConfig.enabled
3948
+ selfHealEnabled: selfHealConfig.enabled,
3949
+ timeout: normalized.timeout
3943
3950
  });
3944
3951
  attempts.push(first.trace);
3945
3952
  if (first.trace.success) {
@@ -3993,13 +4000,14 @@ async function structured(adapter, schemaOrOptions, promptInput, callOptions) {
3993
4000
  schema: normalized.schema,
3994
4001
  parseOptions,
3995
4002
  stream: streamConfig,
3996
- request: normalized.request,
4003
+ request: resolvedRequest,
3997
4004
  systemPrompt: preparedPrompt.systemPrompt,
3998
4005
  observe: normalized.observe,
3999
4006
  debug: debugConfig,
4000
4007
  attemptNumber,
4001
4008
  selfHeal: true,
4002
- selfHealEnabled: selfHealConfig.enabled
4009
+ selfHealEnabled: selfHealConfig.enabled,
4010
+ timeout: normalized.timeout
4003
4011
  });
4004
4012
  attempts.push(healed.trace);
4005
4013
  if (healed.trace.success) {
@@ -4131,6 +4139,19 @@ function injectStructuredFormatIntoMessages(messages, schema, schemaInstruction)
4131
4139
  throw new Error("Structured prompts with messages must include at least one user message.");
4132
4140
  }
4133
4141
  const target = messages[lastUserIndex];
4142
+ if (Array.isArray(target?.content)) {
4143
+ const parts = target.content;
4144
+ const textIndex = parts.findIndex((p) => p.type === "text");
4145
+ const existingText = textIndex !== -1 ? (parts[textIndex]?.text ?? "").trim() : "";
4146
+ const formatted2 = shouldInjectFormat(existingText, schemaInstruction) ? formatPrompt(schema, existingText, { schemaInstruction }) : existingText;
4147
+ let newParts;
4148
+ if (textIndex !== -1) {
4149
+ newParts = parts.map((p, i) => i === textIndex ? { ...p, text: formatted2 } : p);
4150
+ } else {
4151
+ newParts = [{ type: "text", text: formatted2 }, ...parts];
4152
+ }
4153
+ return messages.map((message, index) => index === lastUserIndex ? { ...message, content: newParts } : message);
4154
+ }
4134
4155
  const content = typeof target?.content === "string" ? target.content.trim() : stringifyPromptContent(target?.content);
4135
4156
  const formatted = shouldInjectFormat(content, schemaInstruction) ? formatPrompt(schema, content, { schemaInstruction }) : content.trim();
4136
4157
  return messages.map((message, index) => index === lastUserIndex ? {
@@ -4345,7 +4366,8 @@ async function executeAttempt(adapter, input) {
4345
4366
  debug: input.debug,
4346
4367
  attempt: input.attemptNumber,
4347
4368
  selfHeal: input.selfHeal,
4348
- selfHealEnabled: input.selfHealEnabled
4369
+ selfHealEnabled: input.selfHealEnabled,
4370
+ timeout: input.timeout
4349
4371
  });
4350
4372
  const parsed = parseWithObserve(response.text, input.schema, input.parseOptions, {
4351
4373
  observe: input.observe,
@@ -4372,7 +4394,29 @@ async function executeAttempt(adapter, input) {
4372
4394
  trace
4373
4395
  };
4374
4396
  }
4397
+ function withToolTimeout(client, toolTimeoutMs) {
4398
+ return {
4399
+ id: client.id,
4400
+ listTools: client.listTools.bind(client),
4401
+ close: client.close?.bind(client),
4402
+ async callTool(params) {
4403
+ let timeoutId;
4404
+ const timeoutPromise = new Promise((_, reject) => {
4405
+ timeoutId = setTimeout(() => reject(new Error(`Tool call timed out after ${toolTimeoutMs}ms`)), toolTimeoutMs);
4406
+ });
4407
+ try {
4408
+ return await Promise.race([client.callTool(params), timeoutPromise]);
4409
+ } finally {
4410
+ clearTimeout(timeoutId);
4411
+ }
4412
+ }
4413
+ };
4414
+ }
4415
+ function applyToolTimeout(clients, toolTimeoutMs) {
4416
+ return clients.map((client) => withToolTimeout(client, toolTimeoutMs));
4417
+ }
4375
4418
  async function callModel(adapter, options) {
4419
+ const requestSignal = options.request?.signal ?? (options.timeout?.request !== undefined ? AbortSignal.timeout(options.timeout.request) : undefined);
4376
4420
  const requestPayload = {
4377
4421
  prompt: options.prompt,
4378
4422
  messages: options.messages,
@@ -4386,7 +4430,7 @@ async function callModel(adapter, options) {
4386
4430
  onToolExecution: options.request?.onToolExecution,
4387
4431
  toolDebug: options.request?.toolDebug,
4388
4432
  body: options.request?.body,
4389
- signal: options.request?.signal
4433
+ signal: requestSignal
4390
4434
  };
4391
4435
  emitDebugRequest(options.debug, {
4392
4436
  provider: adapter.provider,
@@ -4769,7 +4813,8 @@ function mergeStructuredOptions(defaults, overrides) {
4769
4813
  },
4770
4814
  stream: mergeObjectLike(defaults?.stream, overrides?.stream),
4771
4815
  selfHeal: mergeObjectLike(defaults?.selfHeal, overrides?.selfHeal),
4772
- debug: mergeObjectLike(defaults?.debug, overrides?.debug)
4816
+ debug: mergeObjectLike(defaults?.debug, overrides?.debug),
4817
+ timeout: mergeObjectLike(defaults?.timeout, overrides?.timeout)
4773
4818
  };
4774
4819
  }
4775
4820
  function mergeObjectLike(defaults, overrides) {
@@ -4858,6 +4903,53 @@ function toImplementation(clientInfo) {
4858
4903
  version: clientInfo?.version ?? "0.1.0"
4859
4904
  };
4860
4905
  }
4906
+ // src/image.ts
4907
+ var import_path = require("path");
4908
+ var IMAGE_SIZE_MAP = {
4909
+ low: 256,
4910
+ mid: 512,
4911
+ high: 1024,
4912
+ xhigh: 1280
4913
+ };
4914
+ var IMAGE_MIME_TYPES = {
4915
+ ".png": "image/png",
4916
+ ".jpg": "image/jpeg",
4917
+ ".jpeg": "image/jpeg",
4918
+ ".gif": "image/gif",
4919
+ ".webp": "image/webp"
4920
+ };
4921
+ var MIME_TO_SHARP_FORMAT = {
4922
+ "image/jpeg": "jpeg",
4923
+ "image/png": "png",
4924
+ "image/webp": "webp",
4925
+ "image/gif": "gif"
4926
+ };
4927
+ function images(input) {
4928
+ const inputs = Array.isArray(input) ? input : [input];
4929
+ return inputs.map(({ base64, mimeType }) => ({
4930
+ type: "image_url",
4931
+ image_url: { url: `data:${mimeType};base64,${base64}` }
4932
+ }));
4933
+ }
4934
+ async function resizeImage(source, size, mimeType) {
4935
+ const resolvedMime = mimeType ?? (typeof source === "string" ? IMAGE_MIME_TYPES[import_path.extname(source).toLowerCase()] ?? "image/jpeg" : "image/jpeg");
4936
+ let sharp;
4937
+ try {
4938
+ sharp = (await import("sharp")).default;
4939
+ } catch {
4940
+ throw new Error('resizeImage() requires "sharp" to be installed. Run: bun add sharp');
4941
+ }
4942
+ const input = source instanceof ArrayBuffer ? Buffer.from(source) : source;
4943
+ let img = sharp(input);
4944
+ if (size !== "raw") {
4945
+ const targetPx = typeof size === "number" ? size : IMAGE_SIZE_MAP[size];
4946
+ img = img.resize(targetPx, targetPx, { fit: "inside", withoutEnlargement: true });
4947
+ }
4948
+ const sharpFormat = MIME_TO_SHARP_FORMAT[resolvedMime] ?? "jpeg";
4949
+ const outputMime = MIME_TO_SHARP_FORMAT[resolvedMime] ? resolvedMime : "image/jpeg";
4950
+ const buf = await img.toFormat(sharpFormat).toBuffer();
4951
+ return { base64: buf.toString("base64"), mimeType: outputMime };
4952
+ }
4861
4953
  // src/prompt.ts
4862
4954
  function toPromptString(value) {
4863
4955
  if (value === null || value === undefined) {
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export { sanitizeThink } from "./think";
5
5
  export { createLLM, type CreateLLMOptions, type LLMClient } from "./llm";
6
6
  export { formatZodIssues, parseLLMOutput } from "./parse";
7
7
  export { createMCPClient, wrapMCPClient, type CreateMCPClientOptions, type MCPClientInfo, type MCPInMemoryTransportConfig, type MCPStdioTransportConfig, type MCPStreamableHTTPTransportConfig, type MCPTransportConfig, type ManagedMCPToolClient, } from "./mcp";
8
+ export { images, resizeImage, type ImageInput, type ImageSize } from "./image";
8
9
  export { prompt, type PromptMessageBuilder } from "./prompt";
9
10
  export { s, inspectSchemaMetadata, inferSchemaExample } from "./schema-builder";
10
11
  export { buildDefaultStructuredPrompt, DEFAULT_LOOSE_PARSE_OPTIONS, DEFAULT_SELF_HEAL_BY_MODE, DEFAULT_SELF_HEAL_CONTEXT_LABEL, DEFAULT_SELF_HEAL_FIX_INSTRUCTION, DEFAULT_SELF_HEAL_MAX_CONTEXT_CHARS, DEFAULT_SELF_HEAL_NO_ISSUES_MESSAGE, DEFAULT_SELF_HEAL_PROTOCOL, DEFAULT_SELF_HEAL_RAW_OUTPUT_LABEL, DEFAULT_SELF_HEAL_RETURN_INSTRUCTION, DEFAULT_SELF_HEAL_STOP_ON_NO_PROGRESS, DEFAULT_SELF_HEAL_VALIDATION_LABEL, DEFAULT_STRICT_PARSE_OPTIONS, DEFAULT_STRUCTURED_OBJECT_INSTRUCTION, DEFAULT_STRUCTURED_STYLE_INSTRUCTION, buildSelfHealPrompt, structured, StructuredParseError, type BuildDefaultStructuredPromptOptions, type SelfHealPromptTextOptions, } from "./structured";
@@ -12,4 +13,4 @@ export { createOpenAICompatibleAdapter, type OpenAICompatibleAdapterOptions, } f
12
13
  export { createAnthropicCompatibleAdapter, DEFAULT_ANTHROPIC_MAX_TOKENS, DEFAULT_ANTHROPIC_VERSION, type AnthropicCompatibleAdapterOptions, } from "./providers/anthropic-compatible";
13
14
  export { DEFAULT_MAX_TOOL_ROUNDS } from "./providers/mcp-runtime";
14
15
  export { createDefaultProviderRegistry, createModelAdapter, createProviderRegistry, registerBuiltinProviders, type BuiltinProviderKind, type ModelAdapterConfig, type ProviderFactory, type ProviderRegistry, type ProviderTransportConfig, } from "./providers/registry";
15
- export type { CandidateDiagnostics, ExtractJsonCandidatesOptions, ExtractionCandidate, ExtractionHeuristicsOptions, ExtractionParseHint, HTTPHeaders, LLMAdapter, LLMMessage, LLMRequest, LLMResponse, LLMStreamCallbacks, LLMStreamChunk, LLMToolCall, LLMToolDebugOptions, LLMToolExecution, LLMToolOutputTransformer, LLMToolArgumentsTransformer, LLMToolChoice, MCPCallToolParams, MCPListToolsResult, MCPToolClient, MCPToolDescriptor, MCPToolSchema, LLMUsage, MarkdownCodeBlock, MarkdownCodeOptions, ParseLLMOutputOptions, ParseLLMOutputResult, ParseTraceEvent, PipelineError, StructuredAttempt, StructuredCallOptions, StructuredDebugOptions, StructuredError, StructuredMode, StructuredOptions, StructuredPromptBuilder, StructuredPromptContext, StructuredPromptPayload, StructuredPromptResolver, StructuredPromptValue, StructuredResult, StructuredStreamData, StructuredStreamEvent, StructuredStreamInput, StructuredStreamOptions, StructuredSelfHealInput, ThinkDiagnostics, ThinkBlock, StructuredTraceEvent, } from "./types";
16
+ export type { CandidateDiagnostics, LLMImageContent, LLMMessageContent, LLMTextContent, ExtractJsonCandidatesOptions, ExtractionCandidate, ExtractionHeuristicsOptions, ExtractionParseHint, HTTPHeaders, LLMAdapter, LLMMessage, LLMRequest, LLMResponse, LLMStreamCallbacks, LLMStreamChunk, LLMToolCall, LLMToolDebugOptions, LLMToolExecution, LLMToolOutputTransformer, LLMToolArgumentsTransformer, LLMToolChoice, MCPCallToolParams, MCPListToolsResult, MCPToolClient, MCPToolDescriptor, MCPToolSchema, LLMUsage, MarkdownCodeBlock, MarkdownCodeOptions, ParseLLMOutputOptions, ParseLLMOutputResult, ParseTraceEvent, PipelineError, StructuredAttempt, StructuredCallOptions, StructuredDebugOptions, StructuredError, StructuredMode, StructuredOptions, StructuredPromptBuilder, StructuredPromptContext, StructuredPromptPayload, StructuredPromptResolver, StructuredPromptValue, StructuredResult, StructuredStreamData, StructuredStreamEvent, StructuredStreamInput, StructuredStreamOptions, StructuredSelfHealInput, StructuredTimeoutOptions, ThinkDiagnostics, ThinkBlock, StructuredTraceEvent, } from "./types";
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
1
4
  // src/extract.ts
2
5
  import { jsonrepair } from "jsonrepair";
3
6
 
@@ -3838,19 +3841,24 @@ async function structured(adapter, schemaOrOptions, promptInput, callOptions) {
3838
3841
  const resolvedPrompt = applyPromptOutdent(resolvePrompt(normalized.prompt, { mode }), useOutdent);
3839
3842
  const resolvedSystemPrompt = applyOutdentToOptionalPrompt(normalized.systemPrompt, useOutdent);
3840
3843
  const preparedPrompt = prepareStructuredPromptPayload(resolvedPrompt, resolvedSystemPrompt, normalized.schema, normalized.schemaInstruction);
3844
+ const resolvedRequest = normalized.timeout?.tool !== undefined && normalized.request?.mcpClients !== undefined ? {
3845
+ ...normalized.request,
3846
+ mcpClients: applyToolTimeout(normalized.request.mcpClients, normalized.timeout.tool)
3847
+ } : normalized.request;
3841
3848
  const first = await executeAttempt(adapter, {
3842
3849
  prompt: preparedPrompt.prompt,
3843
3850
  messages: preparedPrompt.messages,
3844
3851
  schema: normalized.schema,
3845
3852
  parseOptions,
3846
3853
  stream: streamConfig,
3847
- request: normalized.request,
3854
+ request: resolvedRequest,
3848
3855
  systemPrompt: preparedPrompt.systemPrompt,
3849
3856
  observe: normalized.observe,
3850
3857
  debug: debugConfig,
3851
3858
  attemptNumber: 1,
3852
3859
  selfHeal: false,
3853
- selfHealEnabled: selfHealConfig.enabled
3860
+ selfHealEnabled: selfHealConfig.enabled,
3861
+ timeout: normalized.timeout
3854
3862
  });
3855
3863
  attempts.push(first.trace);
3856
3864
  if (first.trace.success) {
@@ -3904,13 +3912,14 @@ async function structured(adapter, schemaOrOptions, promptInput, callOptions) {
3904
3912
  schema: normalized.schema,
3905
3913
  parseOptions,
3906
3914
  stream: streamConfig,
3907
- request: normalized.request,
3915
+ request: resolvedRequest,
3908
3916
  systemPrompt: preparedPrompt.systemPrompt,
3909
3917
  observe: normalized.observe,
3910
3918
  debug: debugConfig,
3911
3919
  attemptNumber,
3912
3920
  selfHeal: true,
3913
- selfHealEnabled: selfHealConfig.enabled
3921
+ selfHealEnabled: selfHealConfig.enabled,
3922
+ timeout: normalized.timeout
3914
3923
  });
3915
3924
  attempts.push(healed.trace);
3916
3925
  if (healed.trace.success) {
@@ -4042,6 +4051,19 @@ function injectStructuredFormatIntoMessages(messages, schema, schemaInstruction)
4042
4051
  throw new Error("Structured prompts with messages must include at least one user message.");
4043
4052
  }
4044
4053
  const target = messages[lastUserIndex];
4054
+ if (Array.isArray(target?.content)) {
4055
+ const parts = target.content;
4056
+ const textIndex = parts.findIndex((p) => p.type === "text");
4057
+ const existingText = textIndex !== -1 ? (parts[textIndex]?.text ?? "").trim() : "";
4058
+ const formatted2 = shouldInjectFormat(existingText, schemaInstruction) ? formatPrompt(schema, existingText, { schemaInstruction }) : existingText;
4059
+ let newParts;
4060
+ if (textIndex !== -1) {
4061
+ newParts = parts.map((p, i) => i === textIndex ? { ...p, text: formatted2 } : p);
4062
+ } else {
4063
+ newParts = [{ type: "text", text: formatted2 }, ...parts];
4064
+ }
4065
+ return messages.map((message, index) => index === lastUserIndex ? { ...message, content: newParts } : message);
4066
+ }
4045
4067
  const content = typeof target?.content === "string" ? target.content.trim() : stringifyPromptContent(target?.content);
4046
4068
  const formatted = shouldInjectFormat(content, schemaInstruction) ? formatPrompt(schema, content, { schemaInstruction }) : content.trim();
4047
4069
  return messages.map((message, index) => index === lastUserIndex ? {
@@ -4256,7 +4278,8 @@ async function executeAttempt(adapter, input) {
4256
4278
  debug: input.debug,
4257
4279
  attempt: input.attemptNumber,
4258
4280
  selfHeal: input.selfHeal,
4259
- selfHealEnabled: input.selfHealEnabled
4281
+ selfHealEnabled: input.selfHealEnabled,
4282
+ timeout: input.timeout
4260
4283
  });
4261
4284
  const parsed = parseWithObserve(response.text, input.schema, input.parseOptions, {
4262
4285
  observe: input.observe,
@@ -4283,7 +4306,29 @@ async function executeAttempt(adapter, input) {
4283
4306
  trace
4284
4307
  };
4285
4308
  }
4309
+ function withToolTimeout(client, toolTimeoutMs) {
4310
+ return {
4311
+ id: client.id,
4312
+ listTools: client.listTools.bind(client),
4313
+ close: client.close?.bind(client),
4314
+ async callTool(params) {
4315
+ let timeoutId;
4316
+ const timeoutPromise = new Promise((_, reject) => {
4317
+ timeoutId = setTimeout(() => reject(new Error(`Tool call timed out after ${toolTimeoutMs}ms`)), toolTimeoutMs);
4318
+ });
4319
+ try {
4320
+ return await Promise.race([client.callTool(params), timeoutPromise]);
4321
+ } finally {
4322
+ clearTimeout(timeoutId);
4323
+ }
4324
+ }
4325
+ };
4326
+ }
4327
+ function applyToolTimeout(clients, toolTimeoutMs) {
4328
+ return clients.map((client) => withToolTimeout(client, toolTimeoutMs));
4329
+ }
4286
4330
  async function callModel(adapter, options) {
4331
+ const requestSignal = options.request?.signal ?? (options.timeout?.request !== undefined ? AbortSignal.timeout(options.timeout.request) : undefined);
4287
4332
  const requestPayload = {
4288
4333
  prompt: options.prompt,
4289
4334
  messages: options.messages,
@@ -4297,7 +4342,7 @@ async function callModel(adapter, options) {
4297
4342
  onToolExecution: options.request?.onToolExecution,
4298
4343
  toolDebug: options.request?.toolDebug,
4299
4344
  body: options.request?.body,
4300
- signal: options.request?.signal
4345
+ signal: requestSignal
4301
4346
  };
4302
4347
  emitDebugRequest(options.debug, {
4303
4348
  provider: adapter.provider,
@@ -4680,7 +4725,8 @@ function mergeStructuredOptions(defaults, overrides) {
4680
4725
  },
4681
4726
  stream: mergeObjectLike(defaults?.stream, overrides?.stream),
4682
4727
  selfHeal: mergeObjectLike(defaults?.selfHeal, overrides?.selfHeal),
4683
- debug: mergeObjectLike(defaults?.debug, overrides?.debug)
4728
+ debug: mergeObjectLike(defaults?.debug, overrides?.debug),
4729
+ timeout: mergeObjectLike(defaults?.timeout, overrides?.timeout)
4684
4730
  };
4685
4731
  }
4686
4732
  function mergeObjectLike(defaults, overrides) {
@@ -4773,6 +4819,53 @@ function toImplementation(clientInfo) {
4773
4819
  version: clientInfo?.version ?? "0.1.0"
4774
4820
  };
4775
4821
  }
4822
+ // src/image.ts
4823
+ import { extname } from "path";
4824
+ var IMAGE_SIZE_MAP = {
4825
+ low: 256,
4826
+ mid: 512,
4827
+ high: 1024,
4828
+ xhigh: 1280
4829
+ };
4830
+ var IMAGE_MIME_TYPES = {
4831
+ ".png": "image/png",
4832
+ ".jpg": "image/jpeg",
4833
+ ".jpeg": "image/jpeg",
4834
+ ".gif": "image/gif",
4835
+ ".webp": "image/webp"
4836
+ };
4837
+ var MIME_TO_SHARP_FORMAT = {
4838
+ "image/jpeg": "jpeg",
4839
+ "image/png": "png",
4840
+ "image/webp": "webp",
4841
+ "image/gif": "gif"
4842
+ };
4843
+ function images(input) {
4844
+ const inputs = Array.isArray(input) ? input : [input];
4845
+ return inputs.map(({ base64, mimeType }) => ({
4846
+ type: "image_url",
4847
+ image_url: { url: `data:${mimeType};base64,${base64}` }
4848
+ }));
4849
+ }
4850
+ async function resizeImage(source, size, mimeType) {
4851
+ const resolvedMime = mimeType ?? (typeof source === "string" ? IMAGE_MIME_TYPES[extname(source).toLowerCase()] ?? "image/jpeg" : "image/jpeg");
4852
+ let sharp;
4853
+ try {
4854
+ sharp = (await import("sharp")).default;
4855
+ } catch {
4856
+ throw new Error('resizeImage() requires "sharp" to be installed. Run: bun add sharp');
4857
+ }
4858
+ const input = source instanceof ArrayBuffer ? Buffer.from(source) : source;
4859
+ let img = sharp(input);
4860
+ if (size !== "raw") {
4861
+ const targetPx = typeof size === "number" ? size : IMAGE_SIZE_MAP[size];
4862
+ img = img.resize(targetPx, targetPx, { fit: "inside", withoutEnlargement: true });
4863
+ }
4864
+ const sharpFormat = MIME_TO_SHARP_FORMAT[resolvedMime] ?? "jpeg";
4865
+ const outputMime = MIME_TO_SHARP_FORMAT[resolvedMime] ? resolvedMime : "image/jpeg";
4866
+ const buf = await img.toFormat(sharpFormat).toBuffer();
4867
+ return { base64: buf.toString("base64"), mimeType: outputMime };
4868
+ }
4776
4869
  // src/prompt.ts
4777
4870
  function toPromptString(value) {
4778
4871
  if (value === null || value === undefined) {
@@ -5058,11 +5151,13 @@ export {
5058
5151
  sanitizeThink,
5059
5152
  s,
5060
5153
  resolveSchemaInstruction,
5154
+ resizeImage,
5061
5155
  registerBuiltinProviders,
5062
5156
  prompt,
5063
5157
  parseLLMOutput,
5064
5158
  inspectSchemaMetadata,
5065
5159
  inferSchemaExample,
5160
+ images,
5066
5161
  formatZodIssues,
5067
5162
  formatPrompt,
5068
5163
  extractMarkdownCodeBlocks,
package/dist/types.d.ts CHANGED
@@ -119,9 +119,20 @@ export interface MCPToolClient {
119
119
  callTool(params: MCPCallToolParams): Promise<unknown>;
120
120
  close?(): Promise<void>;
121
121
  }
122
+ export interface LLMTextContent {
123
+ type: "text";
124
+ text: string;
125
+ }
126
+ export interface LLMImageContent {
127
+ type: "image_url";
128
+ image_url: {
129
+ url: string;
130
+ };
131
+ }
132
+ export type LLMMessageContent = string | (LLMTextContent | LLMImageContent)[];
122
133
  export interface LLMMessage {
123
134
  role: "system" | "user" | "assistant" | "tool";
124
- content: unknown;
135
+ content: LLMMessageContent;
125
136
  }
126
137
  export interface LLMRequest {
127
138
  prompt?: string;
@@ -250,6 +261,12 @@ export interface StructuredSelfHealOptions {
250
261
  maxContextChars?: number;
251
262
  }
252
263
  export type StructuredSelfHealInput = boolean | number | StructuredSelfHealOptions;
264
+ export interface StructuredTimeoutOptions {
265
+ /** Timeout in ms for each LLM HTTP request. Creates an AbortSignal.timeout internally if no signal is already provided. */
266
+ request?: number;
267
+ /** Timeout in ms for each MCP tool call. */
268
+ tool?: number;
269
+ }
253
270
  export type StructuredStreamData<T> = T extends Array<infer TItem> ? Array<StructuredStreamData<TItem>> : T extends object ? {
254
271
  [K in keyof T]?: StructuredStreamData<T[K]> | null;
255
272
  } : T | null;
@@ -277,6 +294,7 @@ export interface StructuredCallOptions<TSchema extends z.ZodTypeAny> {
277
294
  systemPrompt?: string;
278
295
  request?: Omit<LLMRequest, "prompt" | "systemPrompt" | "messages">;
279
296
  schemaInstruction?: string;
297
+ timeout?: StructuredTimeoutOptions;
280
298
  }
281
299
  export interface StructuredOptions<TSchema extends z.ZodTypeAny> extends StructuredCallOptions<TSchema> {
282
300
  schema: TSchema;
package/package.json CHANGED
@@ -1,17 +1,22 @@
1
1
  {
2
2
  "name": "extrait",
3
- "version": "0.5.1",
4
- "license": "MIT",
3
+ "version": "0.5.2",
5
4
  "repository": {
6
5
  "type": "git",
7
6
  "url": "git+https://github.com/tterrasson/extrait.git"
8
7
  },
9
- "bugs": {
10
- "url": "https://github.com/tterrasson/extrait/issues"
11
- },
12
8
  "main": "./dist/index.cjs",
13
9
  "module": "./dist/index.js",
14
- "types": "./dist/index.d.ts",
10
+ "dependencies": {
11
+ "@modelcontextprotocol/sdk": "^1.27.1",
12
+ "jsonrepair": "^3.13.2",
13
+ "zod": "^4.3.6"
14
+ },
15
+ "devDependencies": {
16
+ "@types/bun": "^1.3.10",
17
+ "@types/sharp": "^0.32.0",
18
+ "typescript": "^5.9.3"
19
+ },
15
20
  "exports": {
16
21
  ".": {
17
22
  "types": "./dist/index.d.ts",
@@ -20,12 +25,29 @@
20
25
  "default": "./dist/index.js"
21
26
  }
22
27
  },
28
+ "bugs": {
29
+ "url": "https://github.com/tterrasson/extrait/issues"
30
+ },
23
31
  "files": [
24
32
  "dist",
25
33
  "README.md",
26
34
  "LICENSE"
27
35
  ],
28
- "type": "module",
36
+ "license": "MIT",
37
+ "overrides": {
38
+ "zod": "^4.3.6"
39
+ },
40
+ "peerDependencies": {
41
+ "sharp": "^0.34.5"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "sharp": {
45
+ "optional": true
46
+ }
47
+ },
48
+ "resolutions": {
49
+ "zod": "^4.3.6"
50
+ },
29
51
  "scripts": {
30
52
  "dev": "bun run examples/runner.ts",
31
53
  "build": "bun run build:esm && bun run build:cjs",
@@ -38,19 +60,6 @@
38
60
  "typecheck": "bunx tsc --noEmit",
39
61
  "pack": "bun run build:types && bun run build && npm pack"
40
62
  },
41
- "dependencies": {
42
- "@modelcontextprotocol/sdk": "^1.27.1",
43
- "jsonrepair": "^3.13.2",
44
- "zod": "^4.3.6"
45
- },
46
- "devDependencies": {
47
- "@types/bun": "^1.3.10",
48
- "typescript": "^5.9.3"
49
- },
50
- "overrides": {
51
- "zod": "^4.3.6"
52
- },
53
- "resolutions": {
54
- "zod": "^4.3.6"
55
- }
63
+ "type": "module",
64
+ "types": "./dist/index.d.ts"
56
65
  }