extrait 0.5.2 → 0.5.4

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
@@ -15,6 +15,7 @@ Structured JSON extraction from LLMs with validation, repair, and streaming.
15
15
  - Optional self-healing for validation failures
16
16
  - Streaming support
17
17
  - MCP tools
18
+ - Vector embeddings (OpenAI-compatible + Voyage AI)
18
19
 
19
20
  ## Installation
20
21
 
@@ -175,19 +176,27 @@ const result = await llm.structured(
175
176
  Use `images()` to build base64 image content blocks for vision-capable models.
176
177
 
177
178
  ```typescript
178
- import { images } from "extrait";
179
+ import { images, prompt } from "extrait";
179
180
  import { readFileSync } from "fs";
180
181
 
181
182
  const base64 = readFileSync("photo.png").toString("base64");
183
+ const img = { base64, mimeType: "image/png" };
182
184
 
183
- // Single image
185
+ // With prompt() builder — pass LLMMessageContent array to .user() or .assistant()
186
+ const result = await llm.structured(Schema,
187
+ prompt()
188
+ .system`You are a vision assistant.`
189
+ .user([{ type: "text", text: "Describe this image." }, ...images(img)])
190
+ );
191
+
192
+ // With raw messages array
184
193
  const result = await llm.structured(Schema, {
185
194
  messages: [
186
195
  {
187
196
  role: "user",
188
197
  content: [
189
198
  { type: "text", text: "Describe this image." },
190
- ...images({ base64, mimeType: "image/png" }),
199
+ ...images(img),
191
200
  ],
192
201
  },
193
202
  ],
@@ -205,6 +214,38 @@ const content = [
205
214
 
206
215
  `images()` accepts a single `{ base64, mimeType }` object or an array, and always returns an `LLMImageContent[]` that spreads directly into a content array.
207
216
 
217
+ ### Conversations (multi-turn history)
218
+
219
+ Use `conversation()` to build a `LLMMessage[]` from an existing conversation history. This is the idiomatic way to pass prior turns to the LLM.
220
+
221
+ ```typescript
222
+ import { conversation } from "extrait";
223
+
224
+ const messages = conversation("You are a helpful assistant.", [
225
+ { role: "user", text: "What is the speed of light?" },
226
+ { role: "assistant", text: "Approximately 299,792 km/s in a vacuum." },
227
+ { role: "user", text: "How long does light take to reach Earth from the Sun?" },
228
+ ]);
229
+
230
+ // Pass to adapter directly
231
+ const response = await llm.adapter.complete({ messages });
232
+
233
+ // Or to structured extraction
234
+ const result = await llm.structured(Schema, { messages });
235
+ ```
236
+
237
+ Entries with `images` produce multimodal content automatically:
238
+
239
+ ```typescript
240
+ const messages = conversation("You are a vision assistant.", [
241
+ {
242
+ role: "user",
243
+ text: "What is in this image?",
244
+ images: [{ base64, mimeType: "image/png" }],
245
+ },
246
+ ]);
247
+ ```
248
+
208
249
  ### Result Object
209
250
 
210
251
  ```typescript
@@ -242,6 +283,64 @@ try {
242
283
  }
243
284
  ```
244
285
 
286
+ ### Embeddings
287
+
288
+ Generate vector embeddings using `llm.embed()`. It always returns `number[][]` — one vector per input string.
289
+
290
+ ```typescript
291
+ // Create a dedicated embedder client (recommended)
292
+ const embedder = createLLM({
293
+ provider: "openai-compatible",
294
+ model: "text-embedding-3-small",
295
+ transport: { apiKey: process.env.OPENAI_API_KEY },
296
+ });
297
+
298
+ // Single string
299
+ const { embeddings, model, usage } = await embedder.embed("Hello world");
300
+ const vector: number[] = embeddings[0];
301
+
302
+ // Multiple strings in one request
303
+ const { embeddings } = await embedder.embed(["text one", "text two", "text three"]);
304
+ // embeddings[0], embeddings[1], embeddings[2] — one vector each
305
+
306
+ // Optional: override model or request extra options per call
307
+ const { embeddings } = await embedder.embed("Hello", {
308
+ model: "text-embedding-ada-002",
309
+ dimensions: 512, // supported by text-embedding-3-* models
310
+ body: { user: "user-id" }, // pass-through to provider
311
+ });
312
+ ```
313
+
314
+ **Result shape:**
315
+
316
+ ```typescript
317
+ {
318
+ embeddings: number[][]; // one vector per input
319
+ model: string;
320
+ usage?: { inputTokens?: number; totalTokens?: number };
321
+ raw?: unknown; // full provider response
322
+ }
323
+ ```
324
+
325
+ **Anthropic / Voyage AI**
326
+
327
+ Anthropic does not provide a native embedding API. Their recommended solution is [Voyage AI](https://api.voyageai.com), which uses the same OpenAI-compatible format:
328
+
329
+ ```typescript
330
+ const embedder = createLLM({
331
+ provider: "openai-compatible",
332
+ model: "voyage-3",
333
+ transport: {
334
+ baseURL: "https://api.voyageai.com",
335
+ apiKey: process.env.VOYAGE_API_KEY,
336
+ },
337
+ });
338
+
339
+ const { embeddings } = await embedder.embed(["query", "document"]);
340
+ ```
341
+
342
+ Calling `llm.embed()` on an `anthropic-compatible` adapter throws a descriptive error pointing to Voyage AI.
343
+
245
344
  ### MCP Tools
246
345
 
247
346
  ```typescript
@@ -329,6 +428,8 @@ Available examples:
329
428
  - `multi-step-reasoning` - Chained structured calls ([multi-step-reasoning.ts](examples/multi-step-reasoning.ts))
330
429
  - `calculator-tool` - MCP tool integration ([calculator-tool.ts](examples/calculator-tool.ts))
331
430
  - `image-analysis` - Multimodal structured extraction from an image file ([image-analysis.ts](examples/image-analysis.ts))
431
+ - `conversation` - Multi-turn conversation history and inline image messages ([conversation.ts](examples/conversation.ts))
432
+ - `embeddings` - Vector embeddings, cosine similarity, and semantic comparison ([embeddings.ts](examples/embeddings.ts))
332
433
 
333
434
  Pass arguments after the example name:
334
435
  ```bash
@@ -339,6 +440,7 @@ bun run dev timeout 5000
339
440
  bun run dev simple "Bun.js runtime"
340
441
  bun run dev sentiment-analysis "I love this product."
341
442
  bun run dev multi-step-reasoning "Why is the sky blue?"
443
+ bun run dev embeddings "the cat sat on the mat" "a feline rested on the rug"
342
444
  ```
343
445
 
344
446
  ## Environment Variables
@@ -0,0 +1,21 @@
1
+ import { type ImageInput } from "./image";
2
+ import type { LLMMessage } from "./types";
3
+ export type ConversationEntry = {
4
+ role: "user";
5
+ text: string;
6
+ images?: ImageInput[];
7
+ } | {
8
+ role: "assistant";
9
+ text: string;
10
+ images?: ImageInput[];
11
+ } | {
12
+ role: "tool_call";
13
+ id: string;
14
+ name: string;
15
+ arguments?: Record<string, unknown>;
16
+ } | {
17
+ role: "tool_result";
18
+ id: string;
19
+ output: unknown;
20
+ };
21
+ export declare function conversation(systemPrompt: string, entries: ConversationEntry[]): LLMMessage[];
package/dist/index.cjs CHANGED
@@ -64,6 +64,7 @@ __export(exports_src, {
64
64
  createLLM: () => createLLM,
65
65
  createDefaultProviderRegistry: () => createDefaultProviderRegistry,
66
66
  createAnthropicCompatibleAdapter: () => createAnthropicCompatibleAdapter,
67
+ conversation: () => conversation,
67
68
  buildSelfHealPrompt: () => buildSelfHealPrompt,
68
69
  buildDefaultStructuredPrompt: () => buildDefaultStructuredPrompt,
69
70
  StructuredParseError: () => StructuredParseError,
@@ -1605,6 +1606,7 @@ function createOpenAICompatibleAdapter(options) {
1605
1606
  const fetcher = options.fetcher ?? fetch;
1606
1607
  const path = options.path ?? "/v1/chat/completions";
1607
1608
  const responsesPath = options.responsesPath ?? "/v1/responses";
1609
+ const embeddingPath = options.embeddingPath ?? "/v1/embeddings";
1608
1610
  return {
1609
1611
  provider: "openai-compatible",
1610
1612
  model: options.model,
@@ -1677,6 +1679,36 @@ function createOpenAICompatibleAdapter(options) {
1677
1679
  const out = { text, usage, finishReason };
1678
1680
  callbacks.onComplete?.(out);
1679
1681
  return out;
1682
+ },
1683
+ async embed(request) {
1684
+ const body = cleanUndefined({
1685
+ ...options.defaultBody,
1686
+ ...request.body,
1687
+ model: request.model ?? options.model,
1688
+ input: request.input,
1689
+ dimensions: request.dimensions,
1690
+ encoding_format: "float"
1691
+ });
1692
+ const response = await fetcher(buildURL(options.baseURL, embeddingPath), {
1693
+ method: "POST",
1694
+ headers: buildHeaders(options),
1695
+ body: JSON.stringify(body)
1696
+ });
1697
+ if (!response.ok) {
1698
+ const message = await response.text();
1699
+ throw new Error(`HTTP ${response.status}: ${message}`);
1700
+ }
1701
+ const json = await response.json();
1702
+ const data = json.data;
1703
+ if (!Array.isArray(data)) {
1704
+ throw new Error("Unexpected embedding response: missing data array");
1705
+ }
1706
+ return {
1707
+ embeddings: data.map((d) => isRecord2(d) && Array.isArray(d.embedding) ? d.embedding : []),
1708
+ model: pickString(json.model) ?? body.model,
1709
+ usage: pickUsage(json),
1710
+ raw: json
1711
+ };
1680
1712
  }
1681
1713
  };
1682
1714
  }
@@ -2265,10 +2297,7 @@ function buildResponsesInput(request) {
2265
2297
  return buildMessages(request);
2266
2298
  }
2267
2299
  function toOpenAIMessage(message) {
2268
- return {
2269
- role: message.role,
2270
- content: message.content
2271
- };
2300
+ return { ...message };
2272
2301
  }
2273
2302
  function toResponsesTools(tools) {
2274
2303
  if (!Array.isArray(tools) || tools.length === 0) {
@@ -2736,6 +2765,9 @@ function createAnthropicCompatibleAdapter(options) {
2736
2765
  const out = { text, usage, finishReason };
2737
2766
  callbacks.onComplete?.(out);
2738
2767
  return out;
2768
+ },
2769
+ async embed() {
2770
+ throw new Error("Anthropic does not provide a native embedding API. " + "Use the openai-compatible provider with Voyage AI (https://api.voyageai.com) — " + "Anthropic's recommended embedding solution, which uses the same request format.");
2739
2771
  }
2740
2772
  };
2741
2773
  }
@@ -3014,6 +3046,23 @@ function toAnthropicInput(messages) {
3014
3046
  continue;
3015
3047
  }
3016
3048
  sawNonSystem = true;
3049
+ if (message.role === "assistant" && Array.isArray(message.tool_calls)) {
3050
+ const parts = [];
3051
+ if (message.content)
3052
+ parts.push({ type: "text", text: message.content });
3053
+ for (const tc of message.tool_calls) {
3054
+ parts.push({ type: "tool_use", id: tc.id, name: tc.function.name, input: JSON.parse(tc.function.arguments) });
3055
+ }
3056
+ normalizedMessages.push({ role: "assistant", content: parts });
3057
+ continue;
3058
+ }
3059
+ if (message.role === "tool") {
3060
+ normalizedMessages.push({
3061
+ role: "user",
3062
+ content: [{ type: "tool_result", tool_use_id: message.tool_call_id, content: message.content }]
3063
+ });
3064
+ continue;
3065
+ }
3017
3066
  normalizedMessages.push({
3018
3067
  role: message.role,
3019
3068
  content: message.content
@@ -4793,6 +4842,12 @@ function createLLM(config, registry = createDefaultProviderRegistry()) {
4793
4842
  async structured(schema, prompt, options) {
4794
4843
  const merged = mergeStructuredOptions(defaults, options);
4795
4844
  return structured(adapter, schema, prompt, merged);
4845
+ },
4846
+ async embed(input, options = {}) {
4847
+ if (!adapter.embed) {
4848
+ throw new Error(`Provider "${adapter.provider ?? "unknown"}" does not support embeddings.`);
4849
+ }
4850
+ return adapter.embed({ ...options, input });
4796
4851
  }
4797
4852
  };
4798
4853
  }
@@ -4950,6 +5005,38 @@ async function resizeImage(source, size, mimeType) {
4950
5005
  const buf = await img.toFormat(sharpFormat).toBuffer();
4951
5006
  return { base64: buf.toString("base64"), mimeType: outputMime };
4952
5007
  }
5008
+ // src/conversation.ts
5009
+ function conversation(systemPrompt, entries) {
5010
+ return [
5011
+ { role: "system", content: systemPrompt },
5012
+ ...entries.map((entry) => {
5013
+ if (entry.role === "tool_call") {
5014
+ return {
5015
+ role: "assistant",
5016
+ content: "",
5017
+ tool_calls: [
5018
+ {
5019
+ id: entry.id,
5020
+ type: "function",
5021
+ function: { name: entry.name, arguments: JSON.stringify(entry.arguments ?? {}) }
5022
+ }
5023
+ ]
5024
+ };
5025
+ }
5026
+ if (entry.role === "tool_result") {
5027
+ return {
5028
+ role: "tool",
5029
+ content: typeof entry.output === "string" ? entry.output : JSON.stringify(entry.output),
5030
+ tool_call_id: entry.id
5031
+ };
5032
+ }
5033
+ return {
5034
+ role: entry.role,
5035
+ content: entry.images && entry.images.length > 0 ? [{ type: "text", text: entry.text }, ...images(entry.images)] : entry.text
5036
+ };
5037
+ })
5038
+ ];
5039
+ }
4953
5040
  // src/prompt.ts
4954
5041
  function toPromptString(value) {
4955
5042
  if (value === null || value === undefined) {
@@ -5024,6 +5111,12 @@ class PromptMessageBuilderImpl {
5024
5111
  return this.pushMessage("assistant", input, values);
5025
5112
  }
5026
5113
  pushMessage(role, input, values) {
5114
+ if (Array.isArray(input) && !isTemplateStringsArray(input)) {
5115
+ if (input.length > 0) {
5116
+ this.messages.push({ role, content: input });
5117
+ }
5118
+ return this;
5119
+ }
5027
5120
  const message = toPromptMessage(input, values);
5028
5121
  if (message.length > 0) {
5029
5122
  this.messages.push({ role, content: message });
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ 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
8
  export { images, resizeImage, type ImageInput, type ImageSize } from "./image";
9
+ export { conversation, type ConversationEntry } from "./conversation";
9
10
  export { prompt, type PromptMessageBuilder } from "./prompt";
10
11
  export { s, inspectSchemaMetadata, inferSchemaExample } from "./schema-builder";
11
12
  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";
@@ -13,4 +14,4 @@ export { createOpenAICompatibleAdapter, type OpenAICompatibleAdapterOptions, } f
13
14
  export { createAnthropicCompatibleAdapter, DEFAULT_ANTHROPIC_MAX_TOKENS, DEFAULT_ANTHROPIC_VERSION, type AnthropicCompatibleAdapterOptions, } from "./providers/anthropic-compatible";
14
15
  export { DEFAULT_MAX_TOOL_ROUNDS } from "./providers/mcp-runtime";
15
16
  export { createDefaultProviderRegistry, createModelAdapter, createProviderRegistry, registerBuiltinProviders, type BuiltinProviderKind, type ModelAdapterConfig, type ProviderFactory, type ProviderRegistry, type ProviderTransportConfig, } from "./providers/registry";
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";
17
+ export type { CandidateDiagnostics, EmbeddingRequest, EmbeddingResult, LLMImageContent, LLMMessageContent, LLMTextContent, ExtractJsonCandidatesOptions, ExtractionCandidate, ExtractionHeuristicsOptions, ExtractionParseHint, HTTPHeaders, LLMAdapter, LLMMessage, LLMRequest, LLMResponse, LLMStreamCallbacks, LLMStreamChunk, LLMToolCall, LLMToolCallRef, 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
@@ -1517,6 +1517,7 @@ function createOpenAICompatibleAdapter(options) {
1517
1517
  const fetcher = options.fetcher ?? fetch;
1518
1518
  const path = options.path ?? "/v1/chat/completions";
1519
1519
  const responsesPath = options.responsesPath ?? "/v1/responses";
1520
+ const embeddingPath = options.embeddingPath ?? "/v1/embeddings";
1520
1521
  return {
1521
1522
  provider: "openai-compatible",
1522
1523
  model: options.model,
@@ -1589,6 +1590,36 @@ function createOpenAICompatibleAdapter(options) {
1589
1590
  const out = { text, usage, finishReason };
1590
1591
  callbacks.onComplete?.(out);
1591
1592
  return out;
1593
+ },
1594
+ async embed(request) {
1595
+ const body = cleanUndefined({
1596
+ ...options.defaultBody,
1597
+ ...request.body,
1598
+ model: request.model ?? options.model,
1599
+ input: request.input,
1600
+ dimensions: request.dimensions,
1601
+ encoding_format: "float"
1602
+ });
1603
+ const response = await fetcher(buildURL(options.baseURL, embeddingPath), {
1604
+ method: "POST",
1605
+ headers: buildHeaders(options),
1606
+ body: JSON.stringify(body)
1607
+ });
1608
+ if (!response.ok) {
1609
+ const message = await response.text();
1610
+ throw new Error(`HTTP ${response.status}: ${message}`);
1611
+ }
1612
+ const json = await response.json();
1613
+ const data = json.data;
1614
+ if (!Array.isArray(data)) {
1615
+ throw new Error("Unexpected embedding response: missing data array");
1616
+ }
1617
+ return {
1618
+ embeddings: data.map((d) => isRecord2(d) && Array.isArray(d.embedding) ? d.embedding : []),
1619
+ model: pickString(json.model) ?? body.model,
1620
+ usage: pickUsage(json),
1621
+ raw: json
1622
+ };
1592
1623
  }
1593
1624
  };
1594
1625
  }
@@ -2177,10 +2208,7 @@ function buildResponsesInput(request) {
2177
2208
  return buildMessages(request);
2178
2209
  }
2179
2210
  function toOpenAIMessage(message) {
2180
- return {
2181
- role: message.role,
2182
- content: message.content
2183
- };
2211
+ return { ...message };
2184
2212
  }
2185
2213
  function toResponsesTools(tools) {
2186
2214
  if (!Array.isArray(tools) || tools.length === 0) {
@@ -2648,6 +2676,9 @@ function createAnthropicCompatibleAdapter(options) {
2648
2676
  const out = { text, usage, finishReason };
2649
2677
  callbacks.onComplete?.(out);
2650
2678
  return out;
2679
+ },
2680
+ async embed() {
2681
+ throw new Error("Anthropic does not provide a native embedding API. " + "Use the openai-compatible provider with Voyage AI (https://api.voyageai.com) — " + "Anthropic's recommended embedding solution, which uses the same request format.");
2651
2682
  }
2652
2683
  };
2653
2684
  }
@@ -2926,6 +2957,23 @@ function toAnthropicInput(messages) {
2926
2957
  continue;
2927
2958
  }
2928
2959
  sawNonSystem = true;
2960
+ if (message.role === "assistant" && Array.isArray(message.tool_calls)) {
2961
+ const parts = [];
2962
+ if (message.content)
2963
+ parts.push({ type: "text", text: message.content });
2964
+ for (const tc of message.tool_calls) {
2965
+ parts.push({ type: "tool_use", id: tc.id, name: tc.function.name, input: JSON.parse(tc.function.arguments) });
2966
+ }
2967
+ normalizedMessages.push({ role: "assistant", content: parts });
2968
+ continue;
2969
+ }
2970
+ if (message.role === "tool") {
2971
+ normalizedMessages.push({
2972
+ role: "user",
2973
+ content: [{ type: "tool_result", tool_use_id: message.tool_call_id, content: message.content }]
2974
+ });
2975
+ continue;
2976
+ }
2929
2977
  normalizedMessages.push({
2930
2978
  role: message.role,
2931
2979
  content: message.content
@@ -4705,6 +4753,12 @@ function createLLM(config, registry = createDefaultProviderRegistry()) {
4705
4753
  async structured(schema, prompt, options) {
4706
4754
  const merged = mergeStructuredOptions(defaults, options);
4707
4755
  return structured(adapter, schema, prompt, merged);
4756
+ },
4757
+ async embed(input, options = {}) {
4758
+ if (!adapter.embed) {
4759
+ throw new Error(`Provider "${adapter.provider ?? "unknown"}" does not support embeddings.`);
4760
+ }
4761
+ return adapter.embed({ ...options, input });
4708
4762
  }
4709
4763
  };
4710
4764
  }
@@ -4866,6 +4920,38 @@ async function resizeImage(source, size, mimeType) {
4866
4920
  const buf = await img.toFormat(sharpFormat).toBuffer();
4867
4921
  return { base64: buf.toString("base64"), mimeType: outputMime };
4868
4922
  }
4923
+ // src/conversation.ts
4924
+ function conversation(systemPrompt, entries) {
4925
+ return [
4926
+ { role: "system", content: systemPrompt },
4927
+ ...entries.map((entry) => {
4928
+ if (entry.role === "tool_call") {
4929
+ return {
4930
+ role: "assistant",
4931
+ content: "",
4932
+ tool_calls: [
4933
+ {
4934
+ id: entry.id,
4935
+ type: "function",
4936
+ function: { name: entry.name, arguments: JSON.stringify(entry.arguments ?? {}) }
4937
+ }
4938
+ ]
4939
+ };
4940
+ }
4941
+ if (entry.role === "tool_result") {
4942
+ return {
4943
+ role: "tool",
4944
+ content: typeof entry.output === "string" ? entry.output : JSON.stringify(entry.output),
4945
+ tool_call_id: entry.id
4946
+ };
4947
+ }
4948
+ return {
4949
+ role: entry.role,
4950
+ content: entry.images && entry.images.length > 0 ? [{ type: "text", text: entry.text }, ...images(entry.images)] : entry.text
4951
+ };
4952
+ })
4953
+ ];
4954
+ }
4869
4955
  // src/prompt.ts
4870
4956
  function toPromptString(value) {
4871
4957
  if (value === null || value === undefined) {
@@ -4940,6 +5026,12 @@ class PromptMessageBuilderImpl {
4940
5026
  return this.pushMessage("assistant", input, values);
4941
5027
  }
4942
5028
  pushMessage(role, input, values) {
5029
+ if (Array.isArray(input) && !isTemplateStringsArray(input)) {
5030
+ if (input.length > 0) {
5031
+ this.messages.push({ role, content: input });
5032
+ }
5033
+ return this;
5034
+ }
4943
5035
  const message = toPromptMessage(input, values);
4944
5036
  if (message.length > 0) {
4945
5037
  this.messages.push({ role, content: message });
@@ -5170,6 +5262,7 @@ export {
5170
5262
  createLLM,
5171
5263
  createDefaultProviderRegistry,
5172
5264
  createAnthropicCompatibleAdapter,
5265
+ conversation,
5173
5266
  buildSelfHealPrompt,
5174
5267
  buildDefaultStructuredPrompt,
5175
5268
  StructuredParseError,
package/dist/llm.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { z } from "zod";
2
2
  import { type ModelAdapterConfig, type ProviderRegistry } from "./providers/registry";
3
- import type { LLMAdapter, StructuredCallOptions, StructuredPromptBuilder, StructuredResult } from "./types";
3
+ import type { EmbeddingRequest, EmbeddingResult, LLMAdapter, StructuredCallOptions, StructuredPromptBuilder, StructuredResult } from "./types";
4
4
  export interface CreateLLMOptions extends ModelAdapterConfig {
5
5
  defaults?: StructuredCallOptions<z.ZodTypeAny>;
6
6
  }
@@ -9,5 +9,6 @@ export interface LLMClient {
9
9
  provider?: string;
10
10
  model?: string;
11
11
  structured<TSchema extends z.ZodTypeAny>(schema: TSchema, prompt: StructuredPromptBuilder, options?: StructuredCallOptions<TSchema>): Promise<StructuredResult<z.infer<TSchema>>>;
12
+ embed(input: string | string[], options?: Omit<EmbeddingRequest, "input">): Promise<EmbeddingResult>;
12
13
  }
13
14
  export declare function createLLM(config: CreateLLMOptions, registry?: ProviderRegistry): LLMClient;
package/dist/prompt.d.ts CHANGED
@@ -1,11 +1,13 @@
1
- import type { StructuredPromptPayload, StructuredPromptResolver } from "./types";
1
+ import type { LLMMessageContent, StructuredPromptPayload, StructuredPromptResolver } from "./types";
2
2
  export interface PromptMessageBuilder extends StructuredPromptResolver {
3
3
  system(input: string): PromptMessageBuilder;
4
4
  system(strings: TemplateStringsArray, ...values: unknown[]): PromptMessageBuilder;
5
5
  user(input: string): PromptMessageBuilder;
6
6
  user(strings: TemplateStringsArray, ...values: unknown[]): PromptMessageBuilder;
7
+ user(content: LLMMessageContent): PromptMessageBuilder;
7
8
  assistant(input: string): PromptMessageBuilder;
8
9
  assistant(strings: TemplateStringsArray, ...values: unknown[]): PromptMessageBuilder;
10
+ assistant(content: LLMMessageContent): PromptMessageBuilder;
9
11
  build(): StructuredPromptPayload;
10
12
  }
11
13
  export declare function prompt(strings: TemplateStringsArray, ...values: unknown[]): string;
@@ -5,6 +5,7 @@ export interface OpenAICompatibleAdapterOptions {
5
5
  apiKey?: string;
6
6
  path?: string;
7
7
  responsesPath?: string;
8
+ embeddingPath?: string;
8
9
  defaultMaxToolRounds?: number;
9
10
  headers?: HTTPHeaders;
10
11
  defaultBody?: Record<string, unknown>;
package/dist/types.d.ts CHANGED
@@ -130,9 +130,18 @@ export interface LLMImageContent {
130
130
  };
131
131
  }
132
132
  export type LLMMessageContent = string | (LLMTextContent | LLMImageContent)[];
133
+ export interface LLMToolCallRef {
134
+ id: string;
135
+ type: "function";
136
+ function: {
137
+ name: string;
138
+ arguments: string;
139
+ };
140
+ }
133
141
  export interface LLMMessage {
134
142
  role: "system" | "user" | "assistant" | "tool";
135
143
  content: LLMMessageContent;
144
+ [key: string]: unknown;
136
145
  }
137
146
  export interface LLMRequest {
138
147
  prompt?: string;
@@ -179,11 +188,24 @@ export interface LLMStreamCallbacks {
179
188
  onChunk?: (chunk: LLMStreamChunk) => void;
180
189
  onComplete?: (response: LLMResponse) => void;
181
190
  }
191
+ export interface EmbeddingRequest {
192
+ input: string | string[];
193
+ model?: string;
194
+ dimensions?: number;
195
+ body?: Record<string, unknown>;
196
+ }
197
+ export interface EmbeddingResult {
198
+ embeddings: number[][];
199
+ model: string;
200
+ usage?: LLMUsage;
201
+ raw?: unknown;
202
+ }
182
203
  export interface LLMAdapter {
183
204
  provider?: string;
184
205
  model?: string;
185
206
  complete(request: LLMRequest): Promise<LLMResponse>;
186
207
  stream?(request: LLMRequest, callbacks?: LLMStreamCallbacks): Promise<LLMResponse>;
208
+ embed?(request: EmbeddingRequest): Promise<EmbeddingResult>;
187
209
  }
188
210
  export interface LLMToolCall {
189
211
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "extrait",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/tterrasson/extrait.git"