refactor(llm): use vercel/AI instead

This commit is contained in:
Elian Doran
2026-03-29 13:03:05 +03:00
parent 261e5b59e0
commit d2d4e1cbac
3 changed files with 174 additions and 152 deletions

View File

@@ -30,7 +30,8 @@
"proxy-nginx-subdir": "docker run --name trilium-nginx-subdir --rm --network=host -v ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro nginx:latest"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@ai-sdk/anthropic": "^2.0.0",
"ai": "^5.0.0",
"better-sqlite3": "12.8.0",
"html-to-text": "9.0.5",
"node-html-parser": "7.1.0",

View File

@@ -1,4 +1,5 @@
import Anthropic from "@anthropic-ai/sdk";
import { anthropic } from "@ai-sdk/anthropic";
import { streamText, type CoreMessage } from "ai";
import type { LlmMessage, LlmStreamChunk } from "@triliumnext/commons";
import type { LlmProvider, LlmProviderConfig } from "../types.js";
@@ -6,26 +7,15 @@ import type { LlmProvider, LlmProviderConfig } from "../types.js";
const DEFAULT_MODEL = "claude-sonnet-4-20250514";
const DEFAULT_MAX_TOKENS = 8096;
/**
* Server-side web search tool type.
* Not yet in SDK types as of @anthropic-ai/sdk.
*/
interface WebSearchTool {
type: "web_search_20250305";
name: "web_search";
max_uses?: number;
}
export class AnthropicProvider implements LlmProvider {
name = "anthropic";
private client: Anthropic;
constructor() {
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
throw new Error("ANTHROPIC_API_KEY environment variable is required");
}
this.client = new Anthropic({ apiKey });
// The anthropic provider reads ANTHROPIC_API_KEY from env automatically
}
async *streamCompletion(
@@ -35,101 +25,105 @@ export class AnthropicProvider implements LlmProvider {
const systemPrompt = config.systemPrompt || messages.find(m => m.role === "system")?.content;
const chatMessages = messages.filter(m => m.role !== "system");
// Build tools array
// Using union with WebSearchTool since it's not in SDK types yet
const tools: (Anthropic.ToolUnion | WebSearchTool)[] = [];
if (config.enableWebSearch) {
tools.push({
type: "web_search_20250305",
name: "web_search",
max_uses: 5 // Limit searches per request
} satisfies WebSearchTool);
}
// Convert to AI SDK message format
const coreMessages: CoreMessage[] = chatMessages.map(m => ({
role: m.role as "user" | "assistant",
content: m.content
}));
try {
const streamParams: Anthropic.Messages.MessageStreamParams = {
model: config.model || DEFAULT_MODEL,
max_tokens: config.maxTokens || DEFAULT_MAX_TOKENS,
system: systemPrompt,
messages: chatMessages.map(m => ({
role: m.role as "user" | "assistant",
content: m.content
}))
};
const model = anthropic(config.model || DEFAULT_MODEL);
if (tools.length > 0) {
// Cast needed until SDK adds WebSearchTool type
streamParams.tools = tools as Anthropic.ToolUnion[];
}
// Build options for streamText
const streamOptions: Parameters<typeof streamText>[0] = {
model,
messages: coreMessages,
maxOutputTokens: config.maxTokens || DEFAULT_MAX_TOKENS,
system: systemPrompt
};
// Enable extended thinking for deeper reasoning
if (config.enableExtendedThinking) {
const thinkingBudget = config.thinkingBudget || 10000;
// max_tokens must be greater than thinking budget
streamParams.max_tokens = Math.max(streamParams.max_tokens, thinkingBudget + 4000);
streamParams.thinking = {
type: "enabled",
budget_tokens: thinkingBudget
// Vercel AI SDK handles thinking via providerOptions
streamOptions.providerOptions = {
anthropic: {
thinking: {
type: "enabled",
budgetTokens: thinkingBudget
}
}
};
// Ensure max tokens accommodates thinking budget
streamOptions.maxOutputTokens = Math.max(
streamOptions.maxOutputTokens || DEFAULT_MAX_TOKENS,
thinkingBudget + 4000
);
console.log(`[LLM] Extended thinking enabled with budget: ${thinkingBudget} tokens`);
}
const stream = this.client.messages.stream(streamParams);
for await (const event of stream) {
// Handle different event types
if (event.type === "content_block_start") {
const block = event.content_block;
if (block.type === "tool_use") {
yield {
type: "tool_use",
toolName: block.name,
toolInput: {} // Input comes in deltas
};
} else if (block.type === "thinking") {
console.log("[LLM] Thinking block started");
}
} else if (event.type === "content_block_delta") {
const delta = event.delta;
if (delta.type === "text_delta") {
yield { type: "text", content: delta.text };
} else if (delta.type === "thinking_delta") {
yield { type: "thinking", content: delta.thinking };
} else if (delta.type === "input_json_delta") {
// Tool input is being streamed - we could accumulate it
// For now, we already emitted tool_use at start
}
} else if (event.type === "content_block_stop") {
// Content block finished
// For server-side tools, results come in subsequent blocks
}
// Handle server-side tool results (for web_search)
// These appear as special content blocks in the response
if (event.type === "message_delta") {
// Check for citations in stop_reason or other metadata
}
// Enable web search if configured
if (config.enableWebSearch) {
const webSearchTool = anthropic.tools.webSearch_20250305({
maxUses: 5
});
streamOptions.tools = {
web_search: webSearchTool
};
}
// Get the final message to extract any citations
const finalMessage = await stream.finalMessage();
for (const block of finalMessage.content) {
if (block.type === "text" && block.citations) {
for (const citation of block.citations) {
// Extract citation info from SDK types (CitationCharLocation, etc.)
// These have: cited_text, document_index, document_title
// Web search citations may have additional properties at runtime
const citationData = citation as unknown as Record<string, unknown>;
const result = streamText(streamOptions);
// Stream the response
for await (const part of result.fullStream) {
switch (part.type) {
case "text-delta":
yield { type: "text", content: part.text };
break;
case "reasoning-delta":
// Extended thinking content
yield { type: "thinking", content: part.text };
break;
case "tool-call":
yield {
type: "citation",
citation: {
title: citation.document_title ?? undefined,
citedText: citation.cited_text,
// URL may be present for web search results (not in SDK types yet)
url: typeof citationData.url === "string" ? citationData.url : undefined
}
type: "tool_use",
toolName: part.toolName,
toolInput: part.input as Record<string, unknown>
};
}
break;
case "tool-result":
yield {
type: "tool_result",
toolName: part.toolName,
result: typeof part.output === "string"
? part.output
: JSON.stringify(part.output)
};
break;
case "source":
// Citation from web search (only URL sources have url property)
if (part.sourceType === "url") {
yield {
type: "citation",
citation: {
url: part.url,
title: part.title
}
};
}
break;
case "error":
yield { type: "error", error: String(part.error) };
break;
case "finish":
// Stream finished
break;
}
}

143
pnpm-lock.yaml generated
View File

@@ -552,9 +552,12 @@ importers:
apps/server:
dependencies:
'@anthropic-ai/sdk':
specifier: ^0.39.0
version: 0.39.0(encoding@0.1.13)
'@ai-sdk/anthropic':
specifier: ^2.0.0
version: 2.0.71(zod@4.1.12)
ai:
specifier: ^5.0.0
version: 5.0.161(zod@4.1.12)
better-sqlite3:
specifier: 12.8.0
version: 12.8.0
@@ -1529,6 +1532,28 @@ packages:
'@adobe/css-tools@4.4.4':
resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==}
'@ai-sdk/anthropic@2.0.71':
resolution: {integrity: sha512-JXTtAwlyxGzzRtpiAXk/O93aOTgdfoVX28EoUuRNVqZRgtkoniLQTtqeb8uZ4oXljNJlXzaJLNasS/U90w/wjw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/gateway@2.0.65':
resolution: {integrity: sha512-yaWzvQQWgAzV0m3eidfpRub1+PggDOr2hLnSOI+L2ZispyJ/7EoSzhjKzNCADj6PHnnPaOMH933Xhl1Z/NSxJw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/provider-utils@3.0.22':
resolution: {integrity: sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/provider@2.0.1':
resolution: {integrity: sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng==}
engines: {node: '>=18'}
'@aklinker1/rollup-plugin-visualizer@5.12.0':
resolution: {integrity: sha512-X24LvEGw6UFmy0lpGJDmXsMyBD58XmX1bbwsaMLhNoM+UMQfQ3b2RtC+nz4b/NoRK5r6QJSKJHBNVeUdwqybaQ==}
engines: {node: '>=14'}
@@ -1546,9 +1571,6 @@ packages:
'@antfu/install-pkg@1.1.0':
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
'@anthropic-ai/sdk@0.39.0':
resolution: {integrity: sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==}
'@apidevtools/json-schema-ref-parser@9.1.2':
resolution: {integrity: sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==}
@@ -6179,18 +6201,12 @@ packages:
'@types/mute-stream@0.0.4':
resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==}
'@types/node-fetch@2.6.13':
resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==}
'@types/node-forge@1.3.14':
resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==}
'@types/node@16.9.1':
resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==}
'@types/node@18.19.130':
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
'@types/node@20.19.25':
resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==}
@@ -7015,6 +7031,10 @@ packages:
'@upsetjs/venn.js@2.0.0':
resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==}
'@vercel/oidc@3.1.0':
resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
engines: {node: '>= 20'}
'@vitest/browser-webdriverio@4.1.2':
resolution: {integrity: sha512-5VKfMSq6ZoEAmvVu3sJGkDjEjGuxwk72tOgoNJfJYv+c+UQX1D4UqSdL8kXUMJcTQx1tKeWwQ9Zym0gRdMfyrA==}
peerDependencies:
@@ -7325,6 +7345,12 @@ packages:
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
engines: {node: '>=8'}
ai@5.0.161:
resolution: {integrity: sha512-CVANs7auUNEi/hRhdJDKcPYaCLWXveIfmoiekNSRel3i8WUieB6iEncDS5smcubWsx7hGtTgXxNRTg0YG0ljtA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
ajv-draft-04@1.0.0:
resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==}
peerDependencies:
@@ -9477,6 +9503,10 @@ packages:
resolution: {integrity: sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==}
engines: {node: '>=18.0.0'}
eventsource-parser@3.0.6:
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
engines: {node: '>=18.0.0'}
execa@1.0.0:
resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==}
engines: {node: '>=6'}
@@ -9741,9 +9771,6 @@ packages:
foreach@2.0.6:
resolution: {integrity: sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==}
form-data-encoder@1.7.2:
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
form-data-encoder@4.1.0:
resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==}
engines: {node: '>= 18'}
@@ -9756,10 +9783,6 @@ packages:
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
engines: {node: '>=0.4.x'}
formdata-node@4.4.1:
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
engines: {node: '>= 12.20'}
formdata-node@6.0.3:
resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==}
engines: {node: '>= 18'}
@@ -11020,6 +11043,9 @@ packages:
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
json-schema@0.4.0:
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
@@ -15044,9 +15070,6 @@ packages:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
@@ -15552,10 +15575,6 @@ packages:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
web-streams-polyfill@4.0.0-beta.3:
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
engines: {node: '>= 14'}
webdriver@9.27.0:
resolution: {integrity: sha512-w07ThZND48SIr0b4S7eFougYUyclmoUwdmju8yXvEJiXYjDjeYUpl8wZrYPEYRBylxpSx+sBHfEUBrPQkcTTRQ==}
engines: {node: '>=18.20.0'}
@@ -16003,6 +16022,30 @@ snapshots:
'@adobe/css-tools@4.4.4': {}
'@ai-sdk/anthropic@2.0.71(zod@4.1.12)':
dependencies:
'@ai-sdk/provider': 2.0.1
'@ai-sdk/provider-utils': 3.0.22(zod@4.1.12)
zod: 4.1.12
'@ai-sdk/gateway@2.0.65(zod@4.1.12)':
dependencies:
'@ai-sdk/provider': 2.0.1
'@ai-sdk/provider-utils': 3.0.22(zod@4.1.12)
'@vercel/oidc': 3.1.0
zod: 4.1.12
'@ai-sdk/provider-utils@3.0.22(zod@4.1.12)':
dependencies:
'@ai-sdk/provider': 2.0.1
'@standard-schema/spec': 1.1.0
eventsource-parser: 3.0.6
zod: 4.1.12
'@ai-sdk/provider@2.0.1':
dependencies:
json-schema: 0.4.0
'@aklinker1/rollup-plugin-visualizer@5.12.0(rollup@4.52.0)':
dependencies:
open: 8.4.2
@@ -16022,18 +16065,6 @@ snapshots:
package-manager-detector: 1.3.0
tinyexec: 1.0.4
'@anthropic-ai/sdk@0.39.0(encoding@0.1.13)':
dependencies:
'@types/node': 18.19.130
'@types/node-fetch': 2.6.13
abort-controller: 3.0.0
agentkeepalive: 4.6.0
form-data-encoder: 1.7.2
formdata-node: 4.4.1
node-fetch: 2.7.0(encoding@0.1.13)
transitivePeerDependencies:
- encoding
'@apidevtools/json-schema-ref-parser@9.1.2':
dependencies:
'@jsdevtools/ono': 7.1.3
@@ -17558,6 +17589,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.6.1
ckeditor5: 47.6.1
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-merge-fields@47.6.1':
dependencies:
@@ -22302,21 +22335,12 @@ snapshots:
dependencies:
'@types/node': 24.12.0
'@types/node-fetch@2.6.13':
dependencies:
'@types/node': 24.12.0
form-data: 4.0.5
'@types/node-forge@1.3.14':
dependencies:
'@types/node': 24.12.0
'@types/node@16.9.1': {}
'@types/node@18.19.130':
dependencies:
undici-types: 5.26.5
'@types/node@20.19.25':
dependencies:
undici-types: 6.21.0
@@ -24086,6 +24110,8 @@ snapshots:
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
'@vercel/oidc@3.1.0': {}
'@vitest/browser-webdriverio@4.1.2(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.12.0)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@8.0.3(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.1.3)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.1.2)(webdriverio@9.27.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))':
dependencies:
'@vitest/browser': 4.1.2(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.12.0)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@8.0.3(@types/node@24.12.0)(esbuild@0.27.4)(jiti@2.6.1)(less@4.1.3)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.1.2)
@@ -24498,6 +24524,14 @@ snapshots:
clean-stack: 2.2.0
indent-string: 4.0.0
ai@5.0.161(zod@4.1.12):
dependencies:
'@ai-sdk/gateway': 2.0.65(zod@4.1.12)
'@ai-sdk/provider': 2.0.1
'@ai-sdk/provider-utils': 3.0.22(zod@4.1.12)
'@opentelemetry/api': 1.9.0
zod: 4.1.12
ajv-draft-04@1.0.0(ajv@8.13.0):
optionalDependencies:
ajv: 8.13.0
@@ -27236,6 +27270,8 @@ snapshots:
eventsource-parser@3.0.2: {}
eventsource-parser@3.0.6: {}
execa@1.0.0:
dependencies:
cross-spawn: 6.0.6
@@ -27642,8 +27678,6 @@ snapshots:
foreach@2.0.6: {}
form-data-encoder@1.7.2: {}
form-data-encoder@4.1.0: {}
form-data@4.0.5:
@@ -27656,11 +27690,6 @@ snapshots:
format@0.2.2: {}
formdata-node@4.4.1:
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 4.0.0-beta.3
formdata-node@6.0.3: {}
formdata-polyfill@4.0.10:
@@ -29057,6 +29086,8 @@ snapshots:
json-schema-traverse@1.0.0: {}
json-schema@0.4.0: {}
json-stable-stringify-without-jsonify@1.0.1: {}
json-stringify-pretty-compact@4.0.0: {}
@@ -33907,8 +33938,6 @@ snapshots:
has-symbols: 1.1.0
which-boxed-primitive: 1.1.1
undici-types@5.26.5: {}
undici-types@6.21.0: {}
undici-types@7.16.0: {}
@@ -34423,8 +34452,6 @@ snapshots:
web-streams-polyfill@3.3.3: {}
web-streams-polyfill@4.0.0-beta.3: {}
webdriver@9.27.0(bufferutil@4.0.9)(utf-8-validate@6.0.5):
dependencies:
'@types/node': 20.19.25