extrait 0.5.3 → 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
 
@@ -282,6 +283,64 @@ try {
282
283
  }
283
284
  ```
284
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
+
285
344
  ### MCP Tools
286
345
 
287
346
  ```typescript
@@ -370,6 +429,7 @@ Available examples:
370
429
  - `calculator-tool` - MCP tool integration ([calculator-tool.ts](examples/calculator-tool.ts))
371
430
  - `image-analysis` - Multimodal structured extraction from an image file ([image-analysis.ts](examples/image-analysis.ts))
372
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))
373
433
 
374
434
  Pass arguments after the example name:
375
435
  ```bash
@@ -380,6 +440,7 @@ bun run dev timeout 5000
380
440
  bun run dev simple "Bun.js runtime"
381
441
  bun run dev sentiment-analysis "I love this product."
382
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"
383
444
  ```
384
445
 
385
446
  ## Environment Variables
@@ -1,8 +1,21 @@
1
1
  import { type ImageInput } from "./image";
2
2
  import type { LLMMessage } from "./types";
3
- export interface ConversationEntry {
4
- role: "user" | "assistant";
3
+ export type ConversationEntry = {
4
+ role: "user";
5
5
  text: string;
6
6
  images?: ImageInput[];
7
- }
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
+ };
8
21
  export declare function conversation(systemPrompt: string, entries: ConversationEntry[]): LLMMessage[];
package/dist/index.cjs CHANGED
@@ -1606,6 +1606,7 @@ function createOpenAICompatibleAdapter(options) {
1606
1606
  const fetcher = options.fetcher ?? fetch;
1607
1607
  const path = options.path ?? "/v1/chat/completions";
1608
1608
  const responsesPath = options.responsesPath ?? "/v1/responses";
1609
+ const embeddingPath = options.embeddingPath ?? "/v1/embeddings";
1609
1610
  return {
1610
1611
  provider: "openai-compatible",
1611
1612
  model: options.model,
@@ -1678,6 +1679,36 @@ function createOpenAICompatibleAdapter(options) {
1678
1679
  const out = { text, usage, finishReason };
1679
1680
  callbacks.onComplete?.(out);
1680
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
+ };
1681
1712
  }
1682
1713
  };
1683
1714
  }
@@ -2266,10 +2297,7 @@ function buildResponsesInput(request) {
2266
2297
  return buildMessages(request);
2267
2298
  }
2268
2299
  function toOpenAIMessage(message) {
2269
- return {
2270
- role: message.role,
2271
- content: message.content
2272
- };
2300
+ return { ...message };
2273
2301
  }
2274
2302
  function toResponsesTools(tools) {
2275
2303
  if (!Array.isArray(tools) || tools.length === 0) {
@@ -2737,6 +2765,9 @@ function createAnthropicCompatibleAdapter(options) {
2737
2765
  const out = { text, usage, finishReason };
2738
2766
  callbacks.onComplete?.(out);
2739
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.");
2740
2771
  }
2741
2772
  };
2742
2773
  }
@@ -3015,6 +3046,23 @@ function toAnthropicInput(messages) {
3015
3046
  continue;
3016
3047
  }
3017
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
+ }
3018
3066
  normalizedMessages.push({
3019
3067
  role: message.role,
3020
3068
  content: message.content
@@ -4794,6 +4842,12 @@ function createLLM(config, registry = createDefaultProviderRegistry()) {
4794
4842
  async structured(schema, prompt, options) {
4795
4843
  const merged = mergeStructuredOptions(defaults, options);
4796
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 });
4797
4851
  }
4798
4852
  };
4799
4853
  }
@@ -4955,10 +5009,32 @@ async function resizeImage(source, size, mimeType) {
4955
5009
  function conversation(systemPrompt, entries) {
4956
5010
  return [
4957
5011
  { role: "system", content: systemPrompt },
4958
- ...entries.map((entry) => ({
4959
- role: entry.role,
4960
- content: entry.images && entry.images.length > 0 ? [{ type: "text", text: entry.text }, ...images(entry.images)] : entry.text
4961
- }))
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
+ })
4962
5038
  ];
4963
5039
  }
4964
5040
  // src/prompt.ts
package/dist/index.d.ts CHANGED
@@ -14,4 +14,4 @@ export { createOpenAICompatibleAdapter, type OpenAICompatibleAdapterOptions, } f
14
14
  export { createAnthropicCompatibleAdapter, DEFAULT_ANTHROPIC_MAX_TOKENS, DEFAULT_ANTHROPIC_VERSION, type AnthropicCompatibleAdapterOptions, } from "./providers/anthropic-compatible";
15
15
  export { DEFAULT_MAX_TOOL_ROUNDS } from "./providers/mcp-runtime";
16
16
  export { createDefaultProviderRegistry, createModelAdapter, createProviderRegistry, registerBuiltinProviders, type BuiltinProviderKind, type ModelAdapterConfig, type ProviderFactory, type ProviderRegistry, type ProviderTransportConfig, } from "./providers/registry";
17
- 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
  }
@@ -4870,10 +4924,32 @@ async function resizeImage(source, size, mimeType) {
4870
4924
  function conversation(systemPrompt, entries) {
4871
4925
  return [
4872
4926
  { role: "system", content: systemPrompt },
4873
- ...entries.map((entry) => ({
4874
- role: entry.role,
4875
- content: entry.images && entry.images.length > 0 ? [{ type: "text", text: entry.text }, ...images(entry.images)] : entry.text
4876
- }))
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
+ })
4877
4953
  ];
4878
4954
  }
4879
4955
  // src/prompt.ts
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;
@@ -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.3",
3
+ "version": "0.5.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/tterrasson/extrait.git"