From b057f1c24505e167523a5aa499cd80e21af66bea Mon Sep 17 00:00:00 2001 From: George Pickett Date: Fri, 4 Sep 2026 14:55:19 -0700 Subject: [PATCH] Fix fact checker service errors and update Parallel APIs --- .../parallel-fact-checker-cerebras/README.md | 31 +- .../package-lock.json | 1123 ++++++++++++++++- .../package.json | 7 +- .../worker.test.ts | 132 ++ .../parallel-fact-checker-cerebras/worker.ts | 100 +- 5 files changed, 1321 insertions(+), 72 deletions(-) create mode 100644 typescript-recipes/parallel-fact-checker-cerebras/worker.test.ts diff --git a/typescript-recipes/parallel-fact-checker-cerebras/README.md b/typescript-recipes/parallel-fact-checker-cerebras/README.md index f27251d..7f14980 100644 --- a/typescript-recipes/parallel-fact-checker-cerebras/README.md +++ b/typescript-recipes/parallel-fact-checker-cerebras/README.md @@ -96,7 +96,7 @@ RULES: }); // Stream facts as they're extracted - for await (const chunk of factsResult.textStream) { + for await (const chunk of modelText(factsResult)) { // Parse complete FACT: lines and emit them immediately // ... } @@ -123,12 +123,14 @@ async function verifyFact( ): Promise { try { // Search for evidence using Parallel - const searchResult = await parallel.beta.search({ + const searchResult = await parallel.search({ objective: `Find reliable sources to verify or refute this claim: "${fact.text}"`, search_queries: [fact.text], - processor: "base", - max_results: 5, - max_chars_per_result: 2000, + mode: "basic", + advanced_settings: { + max_results: 5, + excerpt_settings: { max_chars_per_result: 2000 }, + }, }); // Have LLM analyze the evidence @@ -148,7 +150,7 @@ EXPLANATION: [Brief 1-2 sentence explanation]`, Evidence from web search: ${JSON.stringify(searchResult.results?.slice(0, 3).map(r => ({ title: r.title, - excerpt: r.excerpts?.slice(0, 500) + excerpt: r.excerpts?.join("\n").slice(0, 2000) })), null, 2)}`, maxOutputTokens: 500, }); @@ -176,11 +178,10 @@ await Promise.all( When a user provides a URL, we extract the content using Parallel's Extract API: ```typescript -const extractResult = await parallel.beta.extract({ +const extractResult = await parallel.extract({ urls: [extractUrl], objective: "Extract the article text and key claims from this webpage", - excerpts: true, - full_content: false, + advanced_settings: { full_content: false }, }); const rawContent = extractResult.results[0].full_content || @@ -214,6 +215,8 @@ sendSSE(controller, encoder, { type: "warning", message: "Content was truncated sendSSE(controller, encoder, { type: "error", error: "Demo is experiencing high traffic. Please try again later." }); ``` +The `modelText` helper consumes the AI SDK's `fullStream` and throws on error events. Reading `textStream` alone silently filters those events out, which can make failed inference look like content with no claims. Only an actual empty response should be reported as having no verifiable claims. + The frontend handles these events to update the UI in real-time, showing facts as they're extracted and verdicts as they arrive. ## Setup @@ -297,6 +300,16 @@ Extract content from URL and fact-check it. Content is truncated to 5,000 charac | `error` | Error occurred | | `complete` | Processing finished | +## Validation and operations + +Run `npm test` for regression tests and `npm run typecheck` for TypeScript validation. Test both pasted text and URL input with working keys before deployment. + +The public demo runs as the `cerebras-fact-checker` Cloudflare Worker. Worker invocation logs are enabled in `wrangler.jsonc`. In Cloudflare, use Workers & Pages to inspect this Worker's Metrics and Observability views. Request counts include page loads and API requests; filter by `/check` and `/extract` where possible. A streamed application error can still return HTTP 200, so HTTP success counts alone do not measure successful fact checks. + +There is no browser analytics or conversion-event instrumentation in this recipe. Cloudflare traffic and logs can help diagnose usage and failures, but they do not provide a visitor-to-successful-check funnel. Parallel org usage counts API calls, which can include several searches per fact-checking session. + +If the deployed demo fails while local tests work, inspect the deployed Worker's inference errors and the credentials in the correct environment. Do not copy a personal key into the public demo. Provision the demo's own service credentials, then test both input paths again. + ## Demo Limitations This demo has several limitations: diff --git a/typescript-recipes/parallel-fact-checker-cerebras/package-lock.json b/typescript-recipes/parallel-fact-checker-cerebras/package-lock.json index 16eb994..c07680a 100644 --- a/typescript-recipes/parallel-fact-checker-cerebras/package-lock.json +++ b/typescript-recipes/parallel-fact-checker-cerebras/package-lock.json @@ -10,11 +10,12 @@ "dependencies": { "@ai-sdk/cerebras": "^1.0.32", "ai": "^5.0.108", - "parallel-web": "latest" + "parallel-web": "1.3.3" }, "devDependencies": { "@cloudflare/workers-types": "^4.20251209.0", "typescript": "^5.9.3", + "vitest": "^5.0.0", "wrangler": "^4.54.0" } }, @@ -206,8 +207,7 @@ "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20251212.0.tgz", "integrity": "sha512-RrvbKQli+zkDSLtqmsQetHoc38Zi6VWajb0E79w3lGDbJ0Qq1T4HavAm5jIQ+1R6CJxpWO6t41aefRakj+ciaw==", "dev": true, - "license": "MIT OR Apache-2.0", - "peer": true + "license": "MIT OR Apache-2.0" }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", @@ -1090,6 +1090,17 @@ "node": ">=8.0.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.148.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.148.0.tgz", + "integrity": "sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, "node_modules/@poppinss/colors": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", @@ -1119,6 +1130,302 @@ "dev": true, "license": "MIT" }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.7.tgz", + "integrity": "sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.7.tgz", + "integrity": "sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.7.tgz", + "integrity": "sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.7.tgz", + "integrity": "sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.7.tgz", + "integrity": "sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.7.tgz", + "integrity": "sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.7.tgz", + "integrity": "sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.7.tgz", + "integrity": "sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.7.tgz", + "integrity": "sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.7.tgz", + "integrity": "sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.7.tgz", + "integrity": "sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.7.tgz", + "integrity": "sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.7.tgz", + "integrity": "sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.7.tgz", + "integrity": "sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.7.tgz", + "integrity": "sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@sindresorhus/is": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.1.tgz", @@ -1143,6 +1450,31 @@ "version": "1.0.0", "license": "MIT" }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@vercel/oidc": { "version": "3.0.5", "license": "Apache-2.0", @@ -1150,6 +1482,55 @@ "node": ">= 20" } }, + "node_modules/@vitest/mocker": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.0.tgz", + "integrity": "sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.0", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.0.tgz", + "integrity": "sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -1189,11 +1570,31 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/blake3-wasm": { "version": "2.1.5", "dev": true, "license": "MIT" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -1273,6 +1674,13 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", @@ -1315,6 +1723,16 @@ "@esbuild/win32-x64": "0.27.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/eventsource-parser": { "version": "3.0.6", "license": "MIT", @@ -1335,6 +1753,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1378,6 +1824,301 @@ "node": ">=6" } }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.2.3.tgz", + "integrity": "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/mime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", @@ -1428,10 +2169,44 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/parallel-web": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/parallel-web/-/parallel-web-0.2.4.tgz", - "integrity": "sha512-684TtVFf0wvyZ83FMrhxX1Qg0+PDjsB7bPu0SXBCRVywxSeOloFIKHPKiAsAKWpDgAz3ok/bZmd2CAXImqH3Iw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parallel-web/-/parallel-web-1.3.3.tgz", + "integrity": "sha512-TX8zRvWQ+OSF9U4uuc5kyGx83t1c9AlgT/dRhpCgngYLMN2e0rMiH1Mhmnlmv7WTVn/jAdhtT7cAuJYyEwgXRQ==", "license": "MIT" }, "node_modules/path-to-regexp": { @@ -1446,6 +2221,92 @@ "dev": true, "license": "MIT" }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.7.tgz", + "integrity": "sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.148.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.7", + "@rolldown/binding-android-arm64": "1.2.7", + "@rolldown/binding-darwin-arm64": "1.2.7", + "@rolldown/binding-darwin-x64": "1.2.7", + "@rolldown/binding-freebsd-x64": "1.2.7", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.7", + "@rolldown/binding-linux-arm64-gnu": "1.2.7", + "@rolldown/binding-linux-arm64-musl": "1.2.7", + "@rolldown/binding-linux-ppc64-gnu": "1.2.7", + "@rolldown/binding-linux-s390x-gnu": "1.2.7", + "@rolldown/binding-linux-x64-gnu": "1.2.7", + "@rolldown/binding-linux-x64-musl": "1.2.7", + "@rolldown/binding-openharmony-arm64": "1.2.7", + "@rolldown/binding-win32-arm64-msvc": "1.2.7", + "@rolldown/binding-win32-x64-msvc": "1.2.7" + } + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -1499,6 +2360,13 @@ "@img/sharp-win32-x64": "0.33.5" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-swizzle": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", @@ -1509,6 +2377,31 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/stoppable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", @@ -1533,6 +2426,43 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/tinybench": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", + "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1569,11 +2499,189 @@ "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "pathe": "^2.0.3" } }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.0.tgz", + "integrity": "sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.0", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "6.1.4", + "tinyexec": "1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.0", + "@vitest/browser-preview": "5.0.0", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.0", + "@vitest/coverage-v8": "5.0.0", + "@vitest/ui": "5.0.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/workerd": { "version": "1.20251210.0", "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20251210.0.tgz", @@ -1581,7 +2689,6 @@ "dev": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "bin": { "workerd": "bin/workerd" }, diff --git a/typescript-recipes/parallel-fact-checker-cerebras/package.json b/typescript-recipes/parallel-fact-checker-cerebras/package.json index dcc6438..d81bd1a 100644 --- a/typescript-recipes/parallel-fact-checker-cerebras/package.json +++ b/typescript-recipes/parallel-fact-checker-cerebras/package.json @@ -5,16 +5,19 @@ "scripts": { "dev": "wrangler dev", "deploy": "wrangler deploy", - "types": "wrangler types" + "types": "wrangler types", + "test": "vitest run", + "typecheck": "tsc --noEmit" }, "dependencies": { "@ai-sdk/cerebras": "^1.0.32", "ai": "^5.0.108", - "parallel-web": "latest" + "parallel-web": "1.3.3" }, "devDependencies": { "@cloudflare/workers-types": "^4.20251209.0", "typescript": "^5.9.3", + "vitest": "^5.0.0", "wrangler": "^4.54.0" } } diff --git a/typescript-recipes/parallel-fact-checker-cerebras/worker.test.ts b/typescript-recipes/parallel-fact-checker-cerebras/worker.test.ts new file mode 100644 index 0000000..7d76a9a --- /dev/null +++ b/typescript-recipes/parallel-fact-checker-cerebras/worker.test.ts @@ -0,0 +1,132 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import worker from "./worker"; + +vi.mock("./index.html", () => ({ default: "

Fact checker

" })); + +const claim = "Paris is the capital of France."; +const env = { PARALLEL_API_KEY: "test-key", CEREBRAS_API_KEY: "test-key" }; +const requests: Array<{ url: string; body: any }> = []; +let inference: Array<() => Response>; +let searchStatus: number; + +function textResponse(text: string): Response { + const chunks = [ + { choices: [{ index: 0, delta: { content: text }, finish_reason: null }] }, + { choices: [{ index: 0, delta: {}, finish_reason: "stop" }] }, + ]; + return new Response(chunks.map(chunk => `data: ${JSON.stringify(chunk)}\n\n`).join("") + "data: [DONE]\n\n", { + headers: { "Content-Type": "text/event-stream" }, + }); +} + +function apiError(status: number): Response { + return Response.json({ error: { message: "Private provider diagnostic", type: "api_error" } }, { status }); +} + +async function events(path: string, body: object): Promise { + const response = await worker.fetch(new Request(`https://demo.example${path}`, { + method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), + }), env, {} as ExecutionContext); + expect(response.headers.get("Content-Type")).toBe("text/event-stream"); + const text = await response.text(); + expect(text).toContain("data: [DONE]"); + expect(text).not.toContain("Private provider diagnostic"); + return text.split("\n").filter(line => line.startsWith("data: {")).map(line => JSON.parse(line.slice(6))); +} + +beforeEach(() => { + requests.length = 0; + searchStatus = 200; + inference = [ + () => textResponse(`FACT: ${claim} ||| ${claim}`), + () => textResponse("VERDICT: VERIFIED\nEXPLANATION: Sources confirm the claim."), + ]; + vi.spyOn(console, "error").mockImplementation(() => {}); + vi.spyOn(console, "log").mockImplementation(() => {}); + vi.stubGlobal("fetch", vi.fn(async (input, init) => { + const request = new Request(input, init); + const body = await request.json(); + requests.push({ url: request.url, body }); + const path = new URL(request.url).pathname; + if (request.url.startsWith("https://api.cerebras.ai/") && path === "/v1/chat/completions") { + const next = inference.shift(); + if (!next) throw new Error("Unexpected inference request"); + return next(); + } + if (request.url === "https://api.parallel.ai/v1/search") { + if (searchStatus !== 200) return apiError(searchStatus); + return Response.json({ search_id: "test-search", results: [{ + title: "Reference", url: "https://example.com/reference", excerpts: [claim], + }] }); + } + if (request.url === "https://api.parallel.ai/v1/extract") { + return Response.json({ extract_id: "test-extract", errors: [], results: [{ + title: "Article", url: body.urls[0], excerpts: [claim], + }] }); + } + throw new Error(`Unexpected network request: ${request.url}`); + })); +}); + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); +}); + +describe("fact checker with the real provider SDKs", () => { + it.each(["/check", "/agents/cerebras-fact-checker/check"])("checks pasted text through %s", async path => { + const result = await events(path, { content: claim }); + expect(result.find(event => event.type === "fact_verdict")).toMatchObject({ + status: "verified", explanation: "Sources confirm the claim.", + references: [{ title: "Reference", url: "https://example.com/reference", excerpt: claim }], + }); + expect(requests.find(request => request.url.endsWith("/search"))?.body).toMatchObject({ + mode: "basic", advanced_settings: { max_results: 5, excerpt_settings: { max_chars_per_result: 2000 } }, + }); + }); + + it("extracts a URL and completes the same verification flow", async () => { + const result = await events("/agents/cerebras-fact-checker/extract", { url: "https://example.com/article" }); + expect(result.find(event => event.type === "content_complete")).toMatchObject({ content: claim }); + expect(result.find(event => event.type === "fact_verdict")?.status).toBe("verified"); + expect(requests[0].body).toMatchObject({ advanced_settings: { full_content: false } }); + }); + + it.each(["/check", "/extract"])("surfaces inference authentication failure on %s", async path => { + inference = [() => apiError(401)]; + const result = await events(path, { content: claim, url: "https://example.com/article" }); + expect(result.find(event => event.type === "error")?.error).toContain("cannot access an upstream service"); + expect(result.some(event => event.type === "fact_verdict")).toBe(false); + expect(requests.some(request => request.url.endsWith("/search"))).toBe(false); + }); + + it("distinguishes content without claims from failed inference", async () => { + inference = [() => textResponse("No verifiable claims.")]; + const result = await events("/check", { content: "I like this color." }); + expect(result.find(event => event.type === "error")?.error).toBe("No verifiable claims were found in the content."); + }); + + it("reports a failed verdict request explicitly", async () => { + inference[1] = () => apiError(403); + const result = await events("/check", { content: claim }); + expect(result.find(event => event.type === "fact_verdict")).toMatchObject({ + status: "unsure", explanation: expect.stringContaining("cannot access an upstream service"), references: [], + }); + }); + + it("does not accept a truncated verdict as a verified claim", async () => { + inference[1] = () => textResponse("VERDICT: VERIFIED"); + const result = await events("/check", { content: claim }); + expect(result.find(event => event.type === "fact_verdict")).toMatchObject({ + status: "unsure", explanation: expect.stringContaining("incomplete verdict"), references: [], + }); + }); + + it("reports exhausted search credits without exposing provider details", async () => { + searchStatus = 402; + const result = await events("/check", { content: claim }); + expect(result.find(event => event.type === "fact_verdict")).toMatchObject({ + status: "unsure", explanation: expect.stringContaining("credits are unavailable"), references: [], + }); + }); +}); diff --git a/typescript-recipes/parallel-fact-checker-cerebras/worker.ts b/typescript-recipes/parallel-fact-checker-cerebras/worker.ts index ded312c..890f88b 100644 --- a/typescript-recipes/parallel-fact-checker-cerebras/worker.ts +++ b/typescript-recipes/parallel-fact-checker-cerebras/worker.ts @@ -38,6 +38,30 @@ function sendSSE(controller: ReadableStreamDefaultController, encoder: TextEncod controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`)); } +// Only these errors contain messages intended for the public demo UI. +class DemoError extends Error {} + +function publicErrorMessage(error: any): string { + if (error instanceof DemoError) return error.message; + const cause = error?.lastError ?? error; + const status = cause?.statusCode ?? cause?.status; + if (status === 429) return "Service is currently rate limited. Please try again in a moment."; + if (status === 401 || status === 403) { + return "The demo cannot access an upstream service. Please contact the demo owner."; + } + if (status === 402) return "The demo's service credits are unavailable. Please contact the demo owner."; + return "The fact-checking service is unavailable. Please try again later."; +} + +// AI SDK textStream filters out error events. Consume fullStream so a failed +// inference request cannot look like empty content or an unsupported claim. +async function* modelText(result: ReturnType): AsyncGenerator { + for await (const part of result.fullStream) { + if (part.type === "error") throw part.error; + if (part.type === "text-delta") yield part.text; + } +} + // Parse a fact line with format: FACT: [source span] ||| [claim] function parseFactLine(line: string): { sourceSpan: string; text: string } | null { const factContent = line.replace("FACT:", "").trim(); @@ -90,7 +114,7 @@ RULES: let currentText = ""; let totalResponse = ""; - for await (const chunk of factsResult.textStream) { + for await (const chunk of modelText(factsResult)) { currentText += chunk; totalResponse += chunk; @@ -132,7 +156,7 @@ RULES: // If no facts were found, throw so outer handler can deal with it if (extractedFacts.length === 0) { console.log("No facts extracted. Response length:", totalResponse.length); - throw new Error("No facts could be extracted from the content."); + throw new DemoError("No verifiable claims were found in the content."); } } catch (error: any) { console.error("Error extracting facts:", error); @@ -155,12 +179,14 @@ async function verifyFact( sendSSE(controller, encoder, { type: "fact_status", factId: fact.id, status: "searching" }); // Search for evidence - const searchResult = await parallel.beta.search({ + const searchResult = await parallel.search({ objective: `Find reliable sources to verify or refute this claim: "${fact.text}"`, search_queries: [fact.text], - processor: "base", - max_results: 5, - max_chars_per_result: 2000, + mode: "basic", + advanced_settings: { + max_results: 5, + excerpt_settings: { max_chars_per_result: 2000 }, + }, }); // Get verdict from LLM @@ -180,7 +206,7 @@ EXPLANATION: [Brief 1-2 sentence explanation of your reasoning]`, Evidence from web search: ${JSON.stringify(searchResult.results?.slice(0, 3).map((r: any) => ({ title: r.title, - excerpt: r.excerpts?.slice(0, 500) + excerpt: r.excerpts?.join("\n").slice(0, 2000) })), null, 2)} Analyze this evidence and provide your verdict.`, @@ -188,20 +214,17 @@ Analyze this evidence and provide your verdict.`, }); let verdictText = ""; - for await (const chunk of verdictResult.textStream) { + for await (const chunk of modelText(verdictResult)) { verdictText += chunk; } - // Parse verdict - let status: Fact["status"] = "unsure"; - if (verdictText.includes("VERDICT: VERIFIED") || verdictText.includes("VERDICT:VERIFIED")) { - status = "verified"; - } else if (verdictText.includes("VERDICT: FALSE") || verdictText.includes("VERDICT:FALSE")) { - status = "false"; + const verdictMatch = verdictText.match(/^VERDICT:\s*(VERIFIED|FALSE|UNSURE)\s*$/im); + const explanationMatch = verdictText.match(/^EXPLANATION:[ \t]*(.+)/im); + if (!verdictMatch || !explanationMatch) { + throw new DemoError("The fact-checking service returned an incomplete verdict. Please try again."); } - - const explanationMatch = verdictText.match(/EXPLANATION:\s*(.+)/is); - const explanation = explanationMatch ? explanationMatch[1].trim().split('\n')[0] : ""; + const status = verdictMatch[1].toLowerCase() as Fact["status"]; + const explanation = explanationMatch[1].trim(); const references = searchResult.results?.slice(0, 3).map((r: any) => ({ title: r.title || "Source", @@ -219,27 +242,7 @@ Analyze this evidence and provide your verdict.`, } catch (error: any) { console.error(`Error verifying fact ${fact.id}:`, error); - // Check for rate limit errors (various patterns from different APIs) - const errorStr = JSON.stringify(error) + (error?.message || '') + (error?.lastError?.message || ''); - const isRateLimit = error?.statusCode === 429 || - error?.lastError?.statusCode === 429 || - errorStr.includes("too_many_requests") || - errorStr.includes("high traffic") || - errorStr.includes("rate limit") || - errorStr.includes("limit exceeded") || - errorStr.includes("RetryError"); - - const explanation = isRateLimit - ? "Rate limited - please try again later" - : "Could not verify due to an error"; - - // Send rate limit warning to UI - if (isRateLimit) { - sendSSE(controller, encoder, { - type: "warning", - message: "Demo is experiencing high traffic. Some claims could not be verified." - }); - } + const explanation = publicErrorMessage(error); sendSSE(controller, encoder, { type: "fact_verdict", @@ -294,22 +297,21 @@ export default { // Phase 1: Extract content from URL using Parallel sendSSE(controller, encoder, { type: "phase", phase: "extracting_url" }); - const extractResult = await parallel.beta.extract({ + const extractResult = await parallel.extract({ urls: [extractUrl], objective: "Extract the article text and key claims from this webpage", - excerpts: true, - full_content: false, + advanced_settings: { full_content: false }, }); if (!extractResult.results || extractResult.results.length === 0) { - throw new Error("Could not extract content from URL"); + throw new DemoError("Could not extract content from URL"); } const rawContent = extractResult.results[0].full_content || extractResult.results[0].excerpts?.join('\n\n') || ''; if (!rawContent) { - throw new Error("No content found at URL"); + throw new DemoError("No content found at URL"); } // Truncate content if needed @@ -346,11 +348,7 @@ export default { controller.enqueue(encoder.encode("data: [DONE]\n\n")); } catch (error: any) { console.error("Stream error:", error); - // Send user-friendly error message - const errorMsg = error?.message?.includes("limit") || error?.message?.includes("rate") || error?.message?.includes("Retry") - ? "Service is currently rate limited. Please try again in a moment." - : error.message || "An error occurred"; - sendSSE(controller, encoder, { type: "error", error: errorMsg }); + sendSSE(controller, encoder, { type: "error", error: publicErrorMessage(error) }); sendSSE(controller, encoder, { type: "complete" }); controller.enqueue(encoder.encode("data: [DONE]\n\n")); } finally { @@ -426,11 +424,7 @@ export default { controller.enqueue(encoder.encode("data: [DONE]\n\n")); } catch (error: any) { console.error("Stream error:", error); - // Send user-friendly error message - const errorMsg = error?.message?.includes("limit") || error?.message?.includes("rate") || error?.message?.includes("Retry") - ? "Service is currently rate limited. Please try again in a moment." - : error.message || "An error occurred"; - sendSSE(controller, encoder, { type: "error", error: errorMsg }); + sendSSE(controller, encoder, { type: "error", error: publicErrorMessage(error) }); sendSSE(controller, encoder, { type: "complete" }); controller.enqueue(encoder.encode("data: [DONE]\n\n")); } finally {