extrait 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1553,7 +1553,15 @@ function normalizeBaseURL(baseURL) {
1553
1553
  return baseURL.endsWith("/") ? baseURL : `${baseURL}/`;
1554
1554
  }
1555
1555
  function buildURL(baseURL, path) {
1556
- return new URL(path, normalizeBaseURL(baseURL)).toString();
1556
+ try {
1557
+ return new URL(path).toString();
1558
+ } catch {}
1559
+ const base = new URL(normalizeBaseURL(baseURL));
1560
+ const resolvedPath = new URL(path, "http://provider-path.local");
1561
+ base.pathname = mergePathnames(base.pathname, resolvedPath.pathname);
1562
+ base.search = resolvedPath.search;
1563
+ base.hash = resolvedPath.hash;
1564
+ return base.toString();
1557
1565
  }
1558
1566
  function safeJSONParse(input) {
1559
1567
  try {
@@ -1628,6 +1636,36 @@ function addOptional(a, b) {
1628
1636
  }
1629
1637
  return (a ?? 0) + (b ?? 0);
1630
1638
  }
1639
+ function mergePathnames(basePathname, pathPathname) {
1640
+ const baseSegments = splitPathSegments(basePathname);
1641
+ const pathSegments = splitPathSegments(pathPathname);
1642
+ const overlap = findPathOverlap(baseSegments, pathSegments);
1643
+ const mergedSegments = [...baseSegments, ...pathSegments.slice(overlap)];
1644
+ if (mergedSegments.length === 0) {
1645
+ return "/";
1646
+ }
1647
+ const mergedPathname = `/${mergedSegments.join("/")}`;
1648
+ return pathPathname.endsWith("/") && pathPathname !== "/" ? `${mergedPathname}/` : mergedPathname;
1649
+ }
1650
+ function splitPathSegments(pathname) {
1651
+ return pathname.split("/").filter((segment) => segment.length > 0);
1652
+ }
1653
+ function findPathOverlap(baseSegments, pathSegments) {
1654
+ const maxOverlap = Math.min(baseSegments.length, pathSegments.length);
1655
+ for (let size = maxOverlap;size > 0; size -= 1) {
1656
+ let matches = true;
1657
+ for (let index = 0;index < size; index += 1) {
1658
+ if (baseSegments[baseSegments.length - size + index] !== pathSegments[index]) {
1659
+ matches = false;
1660
+ break;
1661
+ }
1662
+ }
1663
+ if (matches) {
1664
+ return size;
1665
+ }
1666
+ }
1667
+ return 0;
1668
+ }
1631
1669
 
1632
1670
  // src/providers/openai-compatible.ts
1633
1671
  function createOpenAICompatibleAdapter(options) {
@@ -1786,7 +1824,7 @@ async function completeWithChatCompletionsPassThrough(options, fetcher, path, re
1786
1824
  const message = await response.text();
1787
1825
  throw new Error(`HTTP ${response.status}: ${message}`);
1788
1826
  }
1789
- const payload = await response.json();
1827
+ const payload = await parseOpenAICompatibleJSONResponse(response, "Failed to parse OpenAI-compatible chat completion response");
1790
1828
  const assistantMessage = pickAssistantMessage(payload);
1791
1829
  if (!assistantMessage) {
1792
1830
  throw new Error("No assistant message in OpenAI-compatible response.");
@@ -1802,6 +1840,25 @@ async function completeWithChatCompletionsPassThrough(options, fetcher, path, re
1802
1840
  toolCalls: toolCalls.length > 0 ? toolCalls : undefined
1803
1841
  };
1804
1842
  }
1843
+ async function parseOpenAICompatibleJSONResponse(response, context) {
1844
+ const rawBody = await response.text();
1845
+ try {
1846
+ return JSON.parse(rawBody);
1847
+ } catch (error) {
1848
+ const message = error instanceof Error ? error.message : String(error);
1849
+ throw new Error(`${context} (HTTP ${response.status}): ${message}. Raw body: ${formatResponseBodyForError(rawBody)}`);
1850
+ }
1851
+ }
1852
+ function formatResponseBodyForError(rawBody, maxLength = 2000) {
1853
+ const normalized = rawBody.trim();
1854
+ if (normalized.length === 0) {
1855
+ return "[empty body]";
1856
+ }
1857
+ if (normalized.length <= maxLength) {
1858
+ return normalized;
1859
+ }
1860
+ return `${normalized.slice(0, maxLength)}...[truncated ${normalized.length - maxLength} chars]`;
1861
+ }
1805
1862
  async function completeWithChatCompletionsWithMCP(options, fetcher, path, request) {
1806
1863
  const maxToolRounds = normalizeMaxToolRounds(request.maxToolRounds ?? options.defaultMaxToolRounds);
1807
1864
  let messages = buildMessages(request);
package/dist/index.d.ts CHANGED
@@ -15,4 +15,4 @@ export { createOpenAICompatibleAdapter, type OpenAICompatibleAdapterOptions, } f
15
15
  export { createAnthropicCompatibleAdapter, DEFAULT_ANTHROPIC_MAX_TOKENS, DEFAULT_ANTHROPIC_VERSION, type AnthropicCompatibleAdapterOptions, } from "./providers/anthropic-compatible";
16
16
  export { DEFAULT_MAX_TOOL_ROUNDS } from "./providers/mcp-runtime";
17
17
  export { createDefaultProviderRegistry, createModelAdapter, createProviderRegistry, registerBuiltinProviders, type BuiltinProviderKind, type ModelAdapterConfig, type ProviderFactory, type ProviderRegistry, type ProviderTransportConfig, } from "./providers/registry";
18
- 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, LLMToolCallParamsTransformer, 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, StructuredStreamDelta, StructuredStreamEvent, StructuredStreamInput, StructuredStreamOptions, StructuredStreamSnapshot, StructuredSelfHealInput, StructuredTimeoutOptions, ThinkDiagnostics, ThinkBlock, StructuredTraceEvent, GenerateAttempt, GenerateCallOptions, GenerateOptions, GenerateResult, GenerateStreamDelta, GenerateStreamEvent, GenerateStreamInput, GenerateStreamOptions, GenerateStreamSnapshot, GenerateTraceEvent, } from "./types";
18
+ export type { CandidateDiagnostics, EmbeddingRequest, EmbeddingResult, LLMImageContent, LLMMessageContent, LLMReasoningEffort, LLMTextContent, ExtractJsonCandidatesOptions, ExtractionCandidate, ExtractionHeuristicsOptions, ExtractionParseHint, HTTPHeaders, LLMAdapter, LLMMessage, LLMRequest, LLMResponse, LLMStreamCallbacks, LLMStreamChunk, LLMToolCall, LLMToolCallRef, LLMToolDebugOptions, LLMToolExecution, LLMToolOutputTransformer, LLMToolArgumentsTransformer, LLMToolCallParamsTransformer, 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, StructuredStreamDelta, StructuredStreamEvent, StructuredStreamInput, StructuredStreamOptions, StructuredStreamSnapshot, StructuredSelfHealInput, StructuredTimeoutOptions, ThinkDiagnostics, ThinkBlock, StructuredTraceEvent, GenerateAttempt, GenerateCallOptions, GenerateOptions, GenerateResult, GenerateStreamDelta, GenerateStreamEvent, GenerateStreamInput, GenerateStreamOptions, GenerateStreamSnapshot, GenerateTraceEvent, } from "./types";
package/dist/index.js CHANGED
@@ -1463,7 +1463,15 @@ function normalizeBaseURL(baseURL) {
1463
1463
  return baseURL.endsWith("/") ? baseURL : `${baseURL}/`;
1464
1464
  }
1465
1465
  function buildURL(baseURL, path) {
1466
- return new URL(path, normalizeBaseURL(baseURL)).toString();
1466
+ try {
1467
+ return new URL(path).toString();
1468
+ } catch {}
1469
+ const base = new URL(normalizeBaseURL(baseURL));
1470
+ const resolvedPath = new URL(path, "http://provider-path.local");
1471
+ base.pathname = mergePathnames(base.pathname, resolvedPath.pathname);
1472
+ base.search = resolvedPath.search;
1473
+ base.hash = resolvedPath.hash;
1474
+ return base.toString();
1467
1475
  }
1468
1476
  function safeJSONParse(input) {
1469
1477
  try {
@@ -1538,6 +1546,36 @@ function addOptional(a, b) {
1538
1546
  }
1539
1547
  return (a ?? 0) + (b ?? 0);
1540
1548
  }
1549
+ function mergePathnames(basePathname, pathPathname) {
1550
+ const baseSegments = splitPathSegments(basePathname);
1551
+ const pathSegments = splitPathSegments(pathPathname);
1552
+ const overlap = findPathOverlap(baseSegments, pathSegments);
1553
+ const mergedSegments = [...baseSegments, ...pathSegments.slice(overlap)];
1554
+ if (mergedSegments.length === 0) {
1555
+ return "/";
1556
+ }
1557
+ const mergedPathname = `/${mergedSegments.join("/")}`;
1558
+ return pathPathname.endsWith("/") && pathPathname !== "/" ? `${mergedPathname}/` : mergedPathname;
1559
+ }
1560
+ function splitPathSegments(pathname) {
1561
+ return pathname.split("/").filter((segment) => segment.length > 0);
1562
+ }
1563
+ function findPathOverlap(baseSegments, pathSegments) {
1564
+ const maxOverlap = Math.min(baseSegments.length, pathSegments.length);
1565
+ for (let size = maxOverlap;size > 0; size -= 1) {
1566
+ let matches = true;
1567
+ for (let index = 0;index < size; index += 1) {
1568
+ if (baseSegments[baseSegments.length - size + index] !== pathSegments[index]) {
1569
+ matches = false;
1570
+ break;
1571
+ }
1572
+ }
1573
+ if (matches) {
1574
+ return size;
1575
+ }
1576
+ }
1577
+ return 0;
1578
+ }
1541
1579
 
1542
1580
  // src/providers/openai-compatible.ts
1543
1581
  function createOpenAICompatibleAdapter(options) {
@@ -1696,7 +1734,7 @@ async function completeWithChatCompletionsPassThrough(options, fetcher, path, re
1696
1734
  const message = await response.text();
1697
1735
  throw new Error(`HTTP ${response.status}: ${message}`);
1698
1736
  }
1699
- const payload = await response.json();
1737
+ const payload = await parseOpenAICompatibleJSONResponse(response, "Failed to parse OpenAI-compatible chat completion response");
1700
1738
  const assistantMessage = pickAssistantMessage(payload);
1701
1739
  if (!assistantMessage) {
1702
1740
  throw new Error("No assistant message in OpenAI-compatible response.");
@@ -1712,6 +1750,25 @@ async function completeWithChatCompletionsPassThrough(options, fetcher, path, re
1712
1750
  toolCalls: toolCalls.length > 0 ? toolCalls : undefined
1713
1751
  };
1714
1752
  }
1753
+ async function parseOpenAICompatibleJSONResponse(response, context) {
1754
+ const rawBody = await response.text();
1755
+ try {
1756
+ return JSON.parse(rawBody);
1757
+ } catch (error) {
1758
+ const message = error instanceof Error ? error.message : String(error);
1759
+ throw new Error(`${context} (HTTP ${response.status}): ${message}. Raw body: ${formatResponseBodyForError(rawBody)}`);
1760
+ }
1761
+ }
1762
+ function formatResponseBodyForError(rawBody, maxLength = 2000) {
1763
+ const normalized = rawBody.trim();
1764
+ if (normalized.length === 0) {
1765
+ return "[empty body]";
1766
+ }
1767
+ if (normalized.length <= maxLength) {
1768
+ return normalized;
1769
+ }
1770
+ return `${normalized.slice(0, maxLength)}...[truncated ${normalized.length - maxLength} chars]`;
1771
+ }
1715
1772
  async function completeWithChatCompletionsWithMCP(options, fetcher, path, request) {
1716
1773
  const maxToolRounds = normalizeMaxToolRounds(request.maxToolRounds ?? options.defaultMaxToolRounds);
1717
1774
  let messages = buildMessages(request);
package/dist/types.d.ts CHANGED
@@ -144,12 +144,13 @@ export interface LLMMessage {
144
144
  content: LLMMessageContent;
145
145
  [key: string]: unknown;
146
146
  }
147
+ export type LLMReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "max";
147
148
  export interface LLMRequest {
148
149
  prompt?: string;
149
150
  systemPrompt?: string;
150
151
  messages?: LLMMessage[];
151
152
  temperature?: number;
152
- reasoningEffort?: "low" | "medium" | "high" | "max";
153
+ reasoningEffort?: LLMReasoningEffort;
153
154
  maxTokens?: number;
154
155
  mcpClients?: MCPToolClient[];
155
156
  toolChoice?: LLMToolChoice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "extrait",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/tterrasson/extrait.git"
@@ -8,12 +8,12 @@
8
8
  "main": "./dist/index.cjs",
9
9
  "module": "./dist/index.js",
10
10
  "dependencies": {
11
- "@modelcontextprotocol/sdk": "^1.27.1",
12
- "jsonrepair": "^3.13.2",
11
+ "@modelcontextprotocol/sdk": "^1.29.0",
12
+ "jsonrepair": "^3.13.3",
13
13
  "zod": "^4.3.6"
14
14
  },
15
15
  "devDependencies": {
16
- "@types/bun": "^1.3.10",
16
+ "@types/bun": "^1.3.11",
17
17
  "@types/sharp": "^0.32.0",
18
18
  "typescript": "^5.9.3"
19
19
  },