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 +72 -1
- package/dist/image.d.ts +8 -0
- package/dist/index.cjs +99 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.js +102 -7
- package/dist/types.d.ts +19 -1
- package/package.json +31 -22
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
|
|
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?"
|
package/dist/image.d.ts
ADDED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
42
|
-
|
|
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
|
}
|