diff --git a/.dev.vars.example b/.dev.vars.example index 47c05003d..17a6b658b 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -30,14 +30,14 @@ BETTER_AUTH_URL="http://localhost:3000" # Autumn usage analytics. When unset, usage tracking is skipped. # AUTUMN_SECRET_KEY="" -# Web search and URL/PDF extraction (Firecrawl). +# Web search and URL extraction (Firecrawl). # FIRECRAWL_API_KEY="" # Agent observability export (The Context Company). # TCC_API_KEY="" -# Document extraction (LlamaParse). -# LLAMA_CLOUD_API_KEY="" +# Hosted document extraction (FileRouter). +# FILEROUTER_API_KEY="" # Direct workspace-upload completion claims. Use a separate random 32+ byte value. # WORKSPACE_UPLOAD_TOKEN_SECRET="" diff --git a/README.md b/README.md index 2ade9d7d9..f58e79357 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ ThinkEx is a full-stack TypeScript app hosted on Cloudflare. The frontend is Rea ### AI, data, and product systems - **AI SDK**, **Cloudflare Think**, **Cloudflare Sandbox**, **Cloudflare Shell**, and **Cloudflare Codemode**. -- **Better Auth**, **Drizzle ORM**, **Zod**, **PostHog**, **The Context Company**, **Autumn**, **Firecrawl**, and **LlamaCloud** integrations. +- **Better Auth**, **Drizzle ORM**, **Zod**, **PostHog**, **The Context Company**, **Autumn**, **Firecrawl**, and **FileRouter** integrations. - **Streamdown**, **KaTeX**, **Shiki**, **PapaParse**, **dnd-kit**, **react-resizable-panels**, and **Zustand** for workspace interactions. See [`package.json`](package.json), [`wrangler.jsonc`](wrangler.jsonc), and [`docs/ENVIRONMENT.md`](docs/ENVIRONMENT.md) for deeper implementation details. diff --git a/containers/liteparse/package-lock.json b/containers/liteparse/package-lock.json deleted file mode 100644 index 2db3e29f8..000000000 --- a/containers/liteparse/package-lock.json +++ /dev/null @@ -1,147 +0,0 @@ -{ - "name": "thinkex-liteparse-container", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "thinkex-liteparse-container", - "dependencies": { - "@llamaindex/liteparse": "2.5.1" - } - }, - "node_modules/@llamaindex/liteparse": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@llamaindex/liteparse/-/liteparse-2.5.1.tgz", - "integrity": "sha512-El2QJUimxKsdNG+sqdnltc27zFAtQUBflyeITHFI9Qrz67lBOjulyBE2hUvECfegioqWzqHwAaphblbXxgL8SQ==", - "license": "Apache-2.0", - "dependencies": { - "commander": "^12.0.0" - }, - "bin": { - "lit": "dist/cli.js", - "liteparse": "dist/cli.js" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "@llamaindex/liteparse-darwin-arm64": "2.5.1", - "@llamaindex/liteparse-darwin-x64": "2.5.1", - "@llamaindex/liteparse-linux-arm64-gnu": "2.5.1", - "@llamaindex/liteparse-linux-x64-gnu": "2.5.1", - "@llamaindex/liteparse-linux-x64-musl": "2.5.1", - "@llamaindex/liteparse-win32-arm64-msvc": "2.5.1", - "@llamaindex/liteparse-win32-x64-msvc": "2.5.1" - } - }, - "node_modules/@llamaindex/liteparse-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@llamaindex/liteparse-darwin-arm64/-/liteparse-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-+DRoudhLqw0Hgl4zjS103d6CLR8T7Dn8yZeVp3cTp5fQ9DRQk9goL8mgRwmIl9UkYvWyOptP1zPTgI1xfKgd9g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@llamaindex/liteparse-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@llamaindex/liteparse-darwin-x64/-/liteparse-darwin-x64-2.5.1.tgz", - "integrity": "sha512-4XDJD8F3R4d2hbfHQLk/bAizYPOySWMBY11SlXTv+VW34rbl6zvd8d/739JlKauFvemx8Cs8kPYlyhz9MVkXSg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@llamaindex/liteparse-linux-arm64-gnu": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@llamaindex/liteparse-linux-arm64-gnu/-/liteparse-linux-arm64-gnu-2.5.1.tgz", - "integrity": "sha512-CQd++tiLV2coB1EGtv4wysJIEU8N62g76abYTA3oBf1Kpatphj312VgUn5If68E9/ziQXp2ZgZ+tf7IhLt6jwQ==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@llamaindex/liteparse-linux-x64-gnu": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@llamaindex/liteparse-linux-x64-gnu/-/liteparse-linux-x64-gnu-2.5.1.tgz", - "integrity": "sha512-oYCEhpUYznx+A7z5Ag8XeHjAGfirm/OE8HDEO4UWs/bx0bHx5mUUZyFf7DeCyYsLqaEOcKUmJ+2wtumg1dmzGA==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@llamaindex/liteparse-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@llamaindex/liteparse-linux-x64-musl/-/liteparse-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-tiOWwjuf2DmJTFBasHli+yodFxl0mx4ugoy3ttncA374hGTAtRrAOxcr9NF8icC7NJ0dh5bqd1ymYkt/Z6N3uA==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@llamaindex/liteparse-win32-arm64-msvc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@llamaindex/liteparse-win32-arm64-msvc/-/liteparse-win32-arm64-msvc-2.5.1.tgz", - "integrity": "sha512-Dq5lS8afst3T3q7OEV3ZxmLx19auRHvc9lhNpqVED8EaSK5RBmHuFQyZZSTVGvid+cGAE3cpmfbQvhbH0kcp4w==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@llamaindex/liteparse-win32-x64-msvc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@llamaindex/liteparse-win32-x64-msvc/-/liteparse-win32-x64-msvc-2.5.1.tgz", - "integrity": "sha512-2hfeM9iEUJ75GnNVk086ZYg8kji6eijKEEmy9af0L5dPK6EhDrpREcQHEoKoSQJJwhR7ySIqilu0PBofI7W62Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - } - } -} diff --git a/containers/liteparse/package.json b/containers/liteparse/package.json deleted file mode 100644 index 864deb88f..000000000 --- a/containers/liteparse/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "thinkex-liteparse-container", - "private": true, - "type": "module", - "dependencies": { - "@llamaindex/liteparse": "2.5.1" - } -} diff --git a/containers/liteparse/Dockerfile b/containers/workspace-file-processor/Dockerfile similarity index 73% rename from containers/liteparse/Dockerfile rename to containers/workspace-file-processor/Dockerfile index a2c123bc2..6c27c2366 100644 --- a/containers/liteparse/Dockerfile +++ b/containers/workspace-file-processor/Dockerfile @@ -6,9 +6,6 @@ RUN apt-get update \ WORKDIR /app -COPY package.json package-lock.json ./ -RUN npm ci --omit=dev && npm cache clean --force - COPY server.mjs ./ USER node diff --git a/containers/liteparse/server.mjs b/containers/workspace-file-processor/server.mjs similarity index 72% rename from containers/liteparse/server.mjs rename to containers/workspace-file-processor/server.mjs index d8de117fe..c400e606d 100644 --- a/containers/liteparse/server.mjs +++ b/containers/workspace-file-processor/server.mjs @@ -1,6 +1,4 @@ -import { LiteParse } from "@llamaindex/liteparse"; import { execFile } from "node:child_process"; -import { once } from "node:events"; import { createWriteStream } from "node:fs"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import { createServer } from "node:http"; @@ -11,21 +9,12 @@ import { Transform } from "node:stream"; import { promisify } from "node:util"; const port = 8080; -const parser = new LiteParse({ - extractLinks: true, - imageMode: "placeholder", - ocrEnabled: false, - outputFormat: "markdown", - quiet: true, -}); -const parseTimeoutMs = 90_000; const maxInputBytes = 100 * 1024 * 1024; const execFileAsync = promisify(execFile); createServer(async (request, response) => { const startedAt = Date.now(); let inputBytes = 0; - let pageCount = 0; let status = 500; let errorType = null; let errorMessage = null; @@ -48,27 +37,8 @@ createServer(async (request, response) => { return response.end(preview.bytes); } - if (request.method !== "POST" || request.url !== "/parse/pdf") { - status = 404; - return sendJson(response, status, { error: "Not found." }); - } - - const bytes = await readPdfRequestBytes(request); - inputBytes = bytes.byteLength; - const result = await withTimeout(parser.parse(bytes), parseTimeoutMs); - pageCount = result.pages.length; - status = 200; - response.writeHead(status, { "content-type": "application/x-ndjson; charset=utf-8" }); - for (const page of result.pages) { - if ( - !response.write( - `${JSON.stringify({ markdown: page.markdown, pageNumber: page.pageNum })}\n`, - ) - ) { - await once(response, "drain"); - } - } - return response.end(); + status = 404; + return sendJson(response, status, { error: "Not found." }); } catch (error) { errorType = error instanceof Error ? error.name : "UnknownError"; errorMessage = error instanceof Error ? error.message : String(error); @@ -76,18 +46,17 @@ createServer(async (request, response) => { status = error.status; return sendJson(response, status, { code: error.code, error: error.message }); } - return sendJson(response, status, { error: "PDF parsing failed." }); + return sendJson(response, status, { error: "File preview failed." }); } finally { console.info( JSON.stringify({ duration_ms: Date.now() - startedAt, error_type: errorType, error_message: errorMessage, - event: "liteparse_request", + event: "workspace_file_preview_request", input_bytes: inputBytes, method: request.method, outcome: status < 400 ? "success" : "error", - page_count: pageCount, path: request.url, status, }), @@ -113,14 +82,6 @@ async function validatePdfFile(filePath) { } } -async function readPdfRequestBytes(request) { - return withRequestFile( - request, - "thinkex-parse-", - async ({ filePath }) => new Uint8Array(await readFile(filePath)), - ); -} - async function generatePreviewRequest(request, kind) { return withRequestFile(request, "thinkex-preview-", async ({ filePath, sizeBytes, tempDir }) => { if (kind === "pdf") { @@ -202,16 +163,3 @@ function sendJson(response, status, body) { response.writeHead(status, { "content-type": "application/json" }); response.end(JSON.stringify(body)); } - -function withTimeout(promise, timeoutMs) { - let timeout; - const timeoutPromise = new Promise((_, reject) => { - timeout = setTimeout(() => { - reject(new Error(`LiteParse parsing timed out after ${timeoutMs}ms.`)); - }, timeoutMs); - }); - - return Promise.race([promise, timeoutPromise]).finally(() => { - clearTimeout(timeout); - }); -} diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 8544d73f6..3bd2d1614 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -38,9 +38,9 @@ The dev server boots and lets you sign in with just the two variables below. Eve | `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | Google sign-in | Google sign-in fails; use **Continue as guest** (shown in local dev) | | `AI_GATEWAY_API_KEY` | AI chat / title generation (Vercel AI Gateway) | AI calls error when invoked | | `AUTUMN_SECRET_KEY` | Autumn usage analytics for completed AI chat messages | Usage tracking is skipped | -| `FIRECRAWL_API_KEY` | Web search + URL/PDF extraction (Firecrawl) | Those calls error when invoked | +| `FIRECRAWL_API_KEY` | Web search and URL extraction (Firecrawl) | Those calls error when invoked | | `TCC_API_KEY` | Agent observability export (The Context Company) | AI chat still works; TCC export is skipped | -| `LLAMA_CLOUD_API_KEY` | Document extraction (LlamaParse) | Those calls error when invoked | +| `FILEROUTER_API_KEY` | Hosted document extraction (FileRouter) | File extraction errors when invoked | ## Signing in without Google diff --git a/docs/concepts/items.mdx b/docs/concepts/items.mdx index 8beed6c0f..a68509887 100644 --- a/docs/concepts/items.mdx +++ b/docs/concepts/items.mdx @@ -40,4 +40,4 @@ Upload limits are currently 50 files or 100 MB per selection. ## Extraction and Previews -PDFs and images are routed through extraction and preview pipelines so the AI can read them and the UI can render usable previews. The current stack uses Cloudflare Workflows, R2, Workers AI, LlamaParse, and conversion containers depending on the file type and configured secrets. +PDFs and images are routed through extraction and preview pipelines so the AI can read them and the UI can render usable previews. The current stack uses FileRouter, Cloudflare Workflows, R2, Workers AI, and conversion containers depending on the file type and configured secrets. diff --git a/docs/configuration/architecture.mdx b/docs/configuration/architecture.mdx index 85eeffe9e..915614c2d 100644 --- a/docs/configuration/architecture.mdx +++ b/docs/configuration/architecture.mdx @@ -52,8 +52,8 @@ The public landing page and product app are currently served by the same Worker. | --- | --- | | Better Auth | Sessions, Google sign-in, local guest sign-in | | AI Gateway / AI SDK | Chat models and title generation | -| Firecrawl | Public web search and URL/PDF extraction | -| LlamaCloud | Document extraction | +| Firecrawl | Public web search and URL extraction | +| FileRouter | Hosted document extraction with LiteParse and LlamaParse | | The Context Company | Agent observability export | | Autumn | AI usage analytics | | PostHog | Product analytics, feedback, and diagnostics | diff --git a/docs/configuration/environment.mdx b/docs/configuration/environment.mdx index 88f596684..ee2f2ad01 100644 --- a/docs/configuration/environment.mdx +++ b/docs/configuration/environment.mdx @@ -19,9 +19,9 @@ ThinkEx reads runtime configuration from Cloudflare bindings, environment variab | `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | Google sign-in | | `AI_GATEWAY_API_KEY` | AI chat and title generation | | `AUTUMN_SECRET_KEY` | AI usage analytics | -| `FIRECRAWL_API_KEY` | Web search and URL/PDF extraction | +| `FIRECRAWL_API_KEY` | Web search and URL extraction | | `TCC_API_KEY` | Agent observability export | -| `LLAMA_CLOUD_API_KEY` | Document extraction | +| `FILEROUTER_API_KEY` | Hosted document extraction | Declared but unset secrets do not stop local startup. The matching feature fails or stays disabled when invoked. diff --git a/package.json b/package.json index 5a1a56d92..373457633 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@embedpdf/plugin-tiling": "^2.14.4", "@embedpdf/plugin-viewport": "^2.14.4", "@embedpdf/plugin-zoom": "^2.14.4", + "@file_router/sdk": "0.5.0", "@fontsource-variable/geist": "5.3.0", "@modelcontextprotocol/sdk": "^1.29.0", "@posthog/ai": "^8.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f414d8d98..a048ceffe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,16 +19,16 @@ importers: dependencies: '@ai-sdk/react': specifier: ^4.0.40 - version: 4.0.40(react@19.2.8)(zod@4.4.3) + version: 4.0.43(react@19.2.8)(zod@4.4.3) '@base-ui/react': specifier: ^1.6.0 version: 1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.3.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@better-auth/oauth-provider': specifier: 1.6.25 - version: 1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.25(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@tanstack/react-start@1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.12)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vitest@4.1.10)(vue@3.5.38(typescript@6.0.3)))(better-call@1.3.7(zod@4.4.3)) + version: 1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.25(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@tanstack/react-start@1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.12)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vitest@4.1.10)(vue@3.5.38(typescript@6.0.3)))(better-call@1.3.7(zod@4.4.3)) '@cloudflare/codemode': specifier: ^0.5.0 - version: 0.5.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.37(zod@4.4.3))(zod@4.4.3) + version: 0.5.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.40(zod@4.4.3))(zod@4.4.3) '@cloudflare/containers': specifier: ^0.3.7 version: 0.3.7 @@ -37,10 +37,10 @@ importers: version: 0.12.3 '@cloudflare/shell': specifier: ^0.4.3 - version: 0.4.3(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.37(zod@4.4.3))(zod@4.4.3) + version: 0.4.3(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.40(zod@4.4.3))(zod@4.4.3) '@cloudflare/think': specifier: ^0.15.0 - version: 0.15.0(patch_hash=443a9229f3de7d304667c7596e6efc61d24da59a94931cfd2306371cad259c26)(@ai-sdk/react@4.0.40(react@19.2.8)(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(agents@0.19.0(@ai-sdk/react@4.0.40(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260727.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(ai@7.0.37(zod@4.4.3))(chat@4.31.0(ai@7.0.37(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3))(ai@7.0.37(zod@4.4.3))(react@19.2.8)(zod@4.4.3) + version: 0.15.0(patch_hash=443a9229f3de7d304667c7596e6efc61d24da59a94931cfd2306371cad259c26)(@ai-sdk/react@4.0.43(react@19.2.8)(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(agents@0.19.0(@ai-sdk/react@4.0.43(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260728.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(ai@7.0.40(zod@4.4.3))(chat@4.31.0(ai@7.0.40(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3))(ai@7.0.40(zod@4.4.3))(react@19.2.8)(zod@4.4.3) '@content-collections/core': specifier: ^0.15.2 version: 0.15.2(typescript@6.0.3) @@ -107,6 +107,9 @@ importers: '@embedpdf/plugin-zoom': specifier: ^2.14.4 version: 2.14.4(patch_hash=0510267c484dfeaac9d1f5321c6991b3d8103d39e26182e94e2838e2e9867a39)(73bed704a65db3f4645324cb8ef357b2) + '@file_router/sdk': + specifier: 0.5.0 + version: 0.5.0(@llamaindex/llama-cloud@2.13.0) '@fontsource-variable/geist': specifier: 5.3.0 version: 5.3.0 @@ -208,19 +211,19 @@ importers: version: 3.29.1 agents: specifier: ^0.19.0 - version: 0.19.0(@ai-sdk/react@4.0.40(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260727.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(ai@7.0.37(zod@4.4.3))(chat@4.31.0(ai@7.0.37(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3) + version: 0.19.0(@ai-sdk/react@4.0.43(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260728.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(ai@7.0.40(zod@4.4.3))(chat@4.31.0(ai@7.0.40(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3) ai: specifier: ^7.0.37 - version: 7.0.37(zod@4.4.3) + version: 7.0.40(zod@4.4.3) autumn-js: specifier: ^1.2.44 - version: 1.2.44(better-auth@1.6.25(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@tanstack/react-start@1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.12)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vitest@4.1.10)(vue@3.5.38(typescript@6.0.3)))(better-call@1.3.7(zod@4.4.3))(express@5.2.1)(hono@4.12.27)(react@19.2.8) + version: 1.2.44(better-auth@1.6.25(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@tanstack/react-start@1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.12)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vitest@4.1.10)(vue@3.5.38(typescript@6.0.3)))(better-call@1.3.7(zod@4.4.3))(express@5.2.1)(hono@4.12.27)(react@19.2.8) aws4fetch: specifier: ^1.0.20 version: 1.0.20 better-auth: specifier: ^1.6.25 - version: 1.6.25(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@tanstack/react-start@1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.12)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vitest@4.1.10)(vue@3.5.38(typescript@6.0.3)) + version: 1.6.25(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@tanstack/react-start@1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.12)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vitest@4.1.10)(vue@3.5.38(typescript@6.0.3)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -232,7 +235,7 @@ importers: version: 0.31.10 drizzle-orm: specifier: ^0.45.1 - version: 0.45.2(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1) + version: 0.45.2(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1) hast-util-to-string: specifier: ^3.0.1 version: 3.0.1 @@ -259,7 +262,7 @@ importers: version: 5.5.4 partyserver: specifier: ^0.5.8 - version: 0.5.8(@cloudflare/workers-types@5.20260727.1) + version: 0.5.8(@cloudflare/workers-types@5.20260728.1) posthog-js: specifier: ^1.407.3 version: 1.407.3 @@ -295,7 +298,7 @@ importers: version: 11.1.2 shadcn: specifier: ^4.15.0 - version: 4.15.0(@cfworker/json-schema@4.1.1)(typescript@6.0.3) + version: 4.16.0(@cfworker/json-schema@4.1.1)(typescript@6.0.3) shiki: specifier: ^4.3.1 version: 4.3.1 @@ -322,7 +325,7 @@ importers: version: 9.0.12(yjs@13.6.31) y-partyserver: specifier: ^2.2.0 - version: 2.2.0(@cloudflare/workers-types@5.20260727.1)(partyserver@0.5.8(@cloudflare/workers-types@5.20260727.1))(yjs@13.6.31) + version: 2.2.0(@cloudflare/workers-types@5.20260728.1)(partyserver@0.5.8(@cloudflare/workers-types@5.20260728.1))(yjs@13.6.31) y-protocols: specifier: ^1.0.7 version: 1.0.7(yjs@13.6.31) @@ -341,13 +344,13 @@ importers: version: 8.0.2(@babel/core@7.29.7) '@cloudflare/vite-plugin': specifier: ^1.47.0 - version: 1.47.0(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(wrangler@4.114.0(@cloudflare/workers-types@5.20260727.1)) + version: 1.47.0(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(wrangler@4.114.0(@cloudflare/workers-types@5.20260728.1)) '@cloudflare/vitest-pool-workers': specifier: ^0.18.8 - version: 0.18.8(@cloudflare/workers-types@5.20260727.1)(@vitest/runner@4.1.10)(@vitest/snapshot@4.1.10)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(jsdom@28.1.0(@noble/hashes@2.2.0))(msw@2.14.6(@types/node@26.1.1)(typescript@6.0.3))) + version: 0.18.8(@cloudflare/workers-types@5.20260728.1)(@vitest/runner@4.1.10)(@vitest/snapshot@4.1.10)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(jsdom@28.1.0(@noble/hashes@2.2.0))(msw@2.14.6(@types/node@26.1.1)(typescript@6.0.3))) '@cloudflare/workers-types': specifier: ^5.20260727.1 - version: 5.20260727.1 + version: 5.20260728.1 '@commitlint/cli': specifier: ^21.2.1 version: 21.2.1(@types/node@26.1.1)(conventional-commits-parser@7.0.1)(typescript@6.0.3) @@ -434,7 +437,7 @@ importers: version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(jsdom@28.1.0(@noble/hashes@2.2.0))(msw@2.14.6(@types/node@26.1.1)(typescript@6.0.3)) wrangler: specifier: ^4.114.0 - version: 4.114.0(@cloudflare/workers-types@5.20260727.1) + version: 4.114.0(@cloudflare/workers-types@5.20260728.1) packages: @@ -444,42 +447,42 @@ packages: '@ag-ui/core@0.0.52': resolution: {integrity: sha512-Xo0bUaNV56EqylzcrAuhUkQX7et7+SZIrqZZtEByGwEq/I1EHny6ZMkWHLkKR7UNi0FJZwJyhKYmKJS3B2SEgA==} - '@ai-sdk/anthropic@4.0.21': - resolution: {integrity: sha512-rvWzefSJiDLBswGFn0oWM7D8p8eo2tKey2anSM2IxaFd/4tLfolWAWP6Zp+82YUURZUoX6f01NI/txF6ekbC4Q==} + '@ai-sdk/anthropic@4.0.23': + resolution: {integrity: sha512-9sky++sOcQ3V38XWqPpiLIe4knjWREOUEv3ZOZZ9mTQepb2Ho60NUFGR/0NxQlTHEW9G0zxciEecySNE/iHdVQ==} engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/gateway@4.0.28': - resolution: {integrity: sha512-ee9TsNO3mkgHDWmTmJ8Fvltr6PFh52zhrV2+FaKJY3F3iu7kWZi5tCRJCR1HVhhlpj6lHniUb28nLQdPHkA/1Q==} + '@ai-sdk/gateway@4.0.30': + resolution: {integrity: sha512-nhgR5U6N0wRg+32Mnd8YrOknWQhWfn3ZAKhEOEf6u3dWzhNxTAAPIB7nnz5gRf8Us8K45yIsTrk0OtH897z7Mw==} engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/mcp@2.0.16': - resolution: {integrity: sha512-jC2r+StEuk8uIldfP4j8nBwOEBJ2TRwy/bz71SHMZ3NG6PSbvRPXGfyoHHR6JTm1O5GcIww0f6B8LznzkrseDA==} + '@ai-sdk/mcp@2.0.18': + resolution: {integrity: sha512-0NEiqoS3PzkITQFldRuuPyO+CckgUPb9wWkp2hQlcaGxKu9ebFJDyjxGYEySE7TQN7mYc8PUuaNEODvky51NaA==} engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/openai@4.0.20': - resolution: {integrity: sha512-/Hu+btLIO2zuYqCp//vBBAGdM/BeN4gds+NWrlv7Y9WrcTXwnEEwh6P3QZIWh2Tt1I1lswuDIpAJ4t/c/rws3Q==} + '@ai-sdk/openai@4.0.22': + resolution: {integrity: sha512-SjElnPn11HsOgkMJzM216NR3BQ3nyvkpJfAVe1C1+ZRpYNmYyLkFRMEs1JR/CUV4BSrxNb3YsPDPtBEfIQE7yQ==} engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@5.0.12': - resolution: {integrity: sha512-bbhlOgHeYwrIGheLkM6fhS8hVger8uFPmcOLg+kxc9EFh7y30XYorWhthlYAgpadO3SJhFZrIcEknN7qEqEVvA==} + '@ai-sdk/provider-utils@5.0.14': + resolution: {integrity: sha512-HW+Ys98cr2BRQzP6jTlp2bRKIqt+oAjYNLm9gqUUQkLz7QERtaPgDZOoXHz9IXSXoOtz175p4Q17qKQdEX0Hag==} engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider@4.0.3': - resolution: {integrity: sha512-e0CpNWJUY7OxAFAnCZkw+ri9QOHWwTs1tXP42782KFGCU07qt8NiXCrCVowyCB5dP2r5/Uls+g2oPd8kOJn9dw==} + '@ai-sdk/provider@4.0.4': + resolution: {integrity: sha512-tbHKNLirllUNF3ZlkCsXnwab2ZV1Sl4b1H/Cp9ruCce15IBmskE8Gwkk0yo9xDWY+jho2of7lVXtwSsyrq7cwQ==} engines: {node: '>=22'} - '@ai-sdk/react@4.0.40': - resolution: {integrity: sha512-z3Ht1ERCEYmt7vo6IZYXlPPNEH/SYz8CAdRWuxA+MgA5p8AXFnor/U6WhZmsAmyzhQk1Oe138PtlW7ee4Ap4qg==} + '@ai-sdk/react@4.0.43': + resolution: {integrity: sha512-GiHV/LxFyDaNCaj8JBz3LJoFnvZz0GZjlEK5lfPrOconxqkLtM4MfzdSnWyRl9q1S6+8orxcLSLhVlH79F5zJQ==} engines: {node: '>=22'} peerDependencies: react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1 @@ -875,8 +878,8 @@ packages: '@chevrotain/types@11.1.2': resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} - '@cloudflare/codemode@0.5.0': - resolution: {integrity: sha512-DS3S/azE4XOCZRJz/slpBo4kFAkdoz303FYDgDSRCJXpnLyDEq+qj9WDceZPBWI7HK0olgOEyLeo/jQy6IKkvQ==} + '@cloudflare/codemode@0.5.1': + resolution: {integrity: sha512-PcX5+qAvupi8p1bMLKhqvPHziZpDubbrxDIvVH+iuuNUaFyOxxWNS9HplfFqIULqUzDPdFf1w7IiSCKHp7GDgg==} peerDependencies: '@modelcontextprotocol/sdk': ^1.25.0 '@tanstack/ai': '>=0.8.0 <1.0.0' @@ -990,8 +993,8 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workers-types@5.20260727.1': - resolution: {integrity: sha512-b/wT+LMZz0oELzxibww0ujFz5BD8NRz9WJ+xd+JNZJUMXgh8IHjpibKdGDvtkbotmihWUknP5tBPUU8KluLxxA==} + '@cloudflare/workers-types@5.20260728.1': + resolution: {integrity: sha512-rZqmesLEH40Xt67PpQSj+iJqwkYBzVr7U/UOa5twkToMsxO5pYsPw2QAaTKDdiCSiki9fWAgsWTj9OmFnbwLRQ==} '@commitlint/cli@21.2.1': resolution: {integrity: sha512-blsZGe29hJ72VGEFVl72IVYX+1vsfINpjA9yWQA6i7OKD/McGEOXg08sKIRKjFk4JvzhV/9n0l3i6NooPLTNfg==} @@ -1873,6 +1876,18 @@ packages: '@noble/hashes': optional: true + '@file_router/sdk@0.5.0': + resolution: {integrity: sha512-8oe3K+yQbNoD4RY014Y1H7hZVtn2CNmNtvLcd8yvVDID+MdmnTuNob2flcngR0brl2ZjWgrhHj5Cl8m9maiApA==} + engines: {node: '>=22.14.0'} + peerDependencies: + '@llamaindex/llama-cloud': ^2.10.0 + '@mistralai/mistralai': ^2.5.0 + peerDependenciesMeta: + '@llamaindex/llama-cloud': + optional: true + '@mistralai/mistralai': + optional: true + '@floating-ui/core@1.7.5': resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} @@ -2166,6 +2181,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@llamaindex/llama-cloud@2.13.0': + resolution: {integrity: sha512-6FXtNuMc+pE5GlWzEdUhyBZZt4bL764zaU2SnHswAi+VfPC92kc9BCjfpKPJVHwfXv7H83iLV3s71EbBi2fIFQ==} + '@mermaid-js/parser@1.1.1': resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} @@ -3252,8 +3270,8 @@ packages: openai: optional: true - '@posthog/browser-common@0.2.2': - resolution: {integrity: sha512-4NHumu2lx7pMeucDLfXnDmeP5CV2oVWY8jBpXBwBUjoYwkQxB7Z3A6q9oDydw+/iSAGl6MaPqW9gsnRK8AvqLA==} + '@posthog/browser-common@0.2.3': + resolution: {integrity: sha512-LJOUcBLLjFOqw6ZZsjrCuojliJHFU/GYeOV88T+lXyePXJQnxFzuAouDtwwHFHtoaaOjfQ0OvDMR7J468Nkomw==} '@posthog/cli@0.7.30': resolution: {integrity: sha512-Qo665wxrTOUJv7AwHbh1zXdb1UnSX8EaeCRSVBj+VqjR43/461fxoBt30a+dGw5jWqk/5Y0k1FOz0RaV2d6yIg==} @@ -4947,8 +4965,8 @@ packages: vite: optional: true - ai@7.0.37: - resolution: {integrity: sha512-stF+SEQJgKY3Qfe3FwNzqUrehHviOp2l7LemoI8YMOa0Zk5PxKsRNgUk3cHXz0RAue9RRyCAis2fz6LSCP6EKw==} + ai@7.0.40: + resolution: {integrity: sha512-Q113Nory1PNgWzo2X7x/bUnJhd95UPNEtWlv+nnM5bSuEkA9iy+VZ3WZFcOvGMZjNDQzD5sZnJgIKQjGh8lv9Q==} engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -6177,6 +6195,10 @@ packages: resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} engines: {node: '>=20'} + file-type@22.0.1: + resolution: {integrity: sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA==} + engines: {node: '>=22'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -8103,8 +8125,8 @@ packages: engines: {node: '>= 0.10'} hasBin: true - shadcn@4.15.0: - resolution: {integrity: sha512-fFTpfOuRwqjpXGp/sKpAxJkjdgv1jf8bDrW1xi0cVn2k7WJ5ijV/gjkAlkAidGDjhEkgcUuxVdm4oRB9r8BivA==} + shadcn@4.16.0: + resolution: {integrity: sha512-kPr4RrQmbbZeAjwBYeBSpFvAHV8rkTlNPUluIxkRriq5TpevfFelVO5xvcF6oguHPVn0R6wPuVer4HudfcLy/A==} engines: {node: '>=20.18.1'} hasBin: true @@ -8966,50 +8988,50 @@ snapshots: zod: 3.25.76 optional: true - '@ai-sdk/anthropic@4.0.21(zod@4.4.3)': + '@ai-sdk/anthropic@4.0.23(zod@4.4.3)': dependencies: - '@ai-sdk/provider': 4.0.3 - '@ai-sdk/provider-utils': 5.0.12(zod@4.4.3) + '@ai-sdk/provider': 4.0.4 + '@ai-sdk/provider-utils': 5.0.14(zod@4.4.3) zod: 4.4.3 - '@ai-sdk/gateway@4.0.28(zod@4.4.3)': + '@ai-sdk/gateway@4.0.30(zod@4.4.3)': dependencies: - '@ai-sdk/provider': 4.0.3 - '@ai-sdk/provider-utils': 5.0.12(zod@4.4.3) + '@ai-sdk/provider': 4.0.4 + '@ai-sdk/provider-utils': 5.0.14(zod@4.4.3) '@vercel/oidc': 3.2.0 zod: 4.4.3 - '@ai-sdk/mcp@2.0.16(zod@4.4.3)': + '@ai-sdk/mcp@2.0.18(zod@4.4.3)': dependencies: - '@ai-sdk/provider': 4.0.3 - '@ai-sdk/provider-utils': 5.0.12(zod@4.4.3) + '@ai-sdk/provider': 4.0.4 + '@ai-sdk/provider-utils': 5.0.14(zod@4.4.3) pkce-challenge: 5.0.1 zod: 4.4.3 - '@ai-sdk/openai@4.0.20(zod@4.4.3)': + '@ai-sdk/openai@4.0.22(zod@4.4.3)': dependencies: - '@ai-sdk/provider': 4.0.3 - '@ai-sdk/provider-utils': 5.0.12(zod@4.4.3) + '@ai-sdk/provider': 4.0.4 + '@ai-sdk/provider-utils': 5.0.14(zod@4.4.3) zod: 4.4.3 - '@ai-sdk/provider-utils@5.0.12(zod@4.4.3)': + '@ai-sdk/provider-utils@5.0.14(zod@4.4.3)': dependencies: - '@ai-sdk/provider': 4.0.3 + '@ai-sdk/provider': 4.0.4 '@standard-schema/spec': 1.1.0 '@workflow/serde': 4.1.0 eventsource-parser: 3.1.0 zod: 4.4.3 - '@ai-sdk/provider@4.0.3': + '@ai-sdk/provider@4.0.4': dependencies: json-schema: 0.4.0 - '@ai-sdk/react@4.0.40(react@19.2.8)(zod@4.4.3)': + '@ai-sdk/react@4.0.43(react@19.2.8)(zod@4.4.3)': dependencies: - '@ai-sdk/mcp': 2.0.16(zod@4.4.3) - '@ai-sdk/provider': 4.0.3 - '@ai-sdk/provider-utils': 5.0.12(zod@4.4.3) - ai: 7.0.37(zod@4.4.3) + '@ai-sdk/mcp': 2.0.18(zod@4.4.3) + '@ai-sdk/provider': 4.0.4 + '@ai-sdk/provider-utils': 5.0.14(zod@4.4.3) + ai: 7.0.40(zod@4.4.3) react: 19.2.8 swr: 2.4.1(react@19.2.8) throttleit: 2.1.0 @@ -9376,7 +9398,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - '@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0)': + '@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0)': dependencies: '@better-auth/utils': 0.4.2 '@better-fetch/fetch': 1.3.1 @@ -9388,51 +9410,51 @@ snapshots: nanostores: 1.3.0 zod: 4.4.3 optionalDependencies: - '@cloudflare/workers-types': 5.20260727.1 + '@cloudflare/workers-types': 5.20260728.1 '@opentelemetry/api': 1.9.1 - '@better-auth/drizzle-adapter@1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))': + '@better-auth/drizzle-adapter@1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))': dependencies: - '@better-auth/core': 1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/core': 1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.2 optionalDependencies: - drizzle-orm: 0.45.2(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1) + drizzle-orm: 0.45.2(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1) - '@better-auth/kysely-adapter@1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(kysely@0.28.17)': + '@better-auth/kysely-adapter@1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(kysely@0.28.17)': dependencies: - '@better-auth/core': 1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/core': 1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.2 optionalDependencies: kysely: 0.28.17 - '@better-auth/memory-adapter@1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': + '@better-auth/memory-adapter@1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/core': 1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.2 - '@better-auth/mongo-adapter@1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': + '@better-auth/mongo-adapter@1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/core': 1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.2 - '@better-auth/oauth-provider@1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.25(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@tanstack/react-start@1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.12)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vitest@4.1.10)(vue@3.5.38(typescript@6.0.3)))(better-call@1.3.7(zod@4.4.3))': + '@better-auth/oauth-provider@1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.25(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@tanstack/react-start@1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.12)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vitest@4.1.10)(vue@3.5.38(typescript@6.0.3)))(better-call@1.3.7(zod@4.4.3))': dependencies: - '@better-auth/core': 1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/core': 1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.2 '@better-fetch/fetch': 1.3.1 - better-auth: 1.6.25(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@tanstack/react-start@1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.12)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vitest@4.1.10)(vue@3.5.38(typescript@6.0.3)) + better-auth: 1.6.25(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@tanstack/react-start@1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.12)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vitest@4.1.10)(vue@3.5.38(typescript@6.0.3)) better-call: 1.3.7(zod@4.4.3) jose: 6.2.3 zod: 4.4.3 - '@better-auth/prisma-adapter@1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': + '@better-auth/prisma-adapter@1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/core': 1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.2 - '@better-auth/telemetry@1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)': + '@better-auth/telemetry@1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)': dependencies: - '@better-auth/core': 1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/core': 1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) '@better-auth/utils': 0.4.2 '@better-fetch/fetch': 1.3.1 @@ -9457,14 +9479,14 @@ snapshots: '@chevrotain/types@11.1.2': {} - '@cloudflare/codemode@0.5.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.37(zod@4.4.3))(zod@4.4.3)': + '@cloudflare/codemode@0.5.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.40(zod@4.4.3))(zod@4.4.3)': dependencies: '@types/json-schema': 7.0.15 acorn: 8.17.0 optionalDependencies: '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3) '@tanstack/ai': 0.22.0(@opentelemetry/api@1.9.1) - ai: 7.0.37(zod@4.4.3) + ai: 7.0.40(zod@4.4.3) zod: 4.4.3 '@cloudflare/containers@0.3.7': {} @@ -9478,9 +9500,9 @@ snapshots: capnweb: 0.8.0 hono: 4.12.27 - '@cloudflare/shell@0.4.3(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.37(zod@4.4.3))(zod@4.4.3)': + '@cloudflare/shell@0.4.3(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.40(zod@4.4.3))(zod@4.4.3)': dependencies: - '@cloudflare/codemode': 0.5.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.37(zod@4.4.3))(zod@4.4.3) + '@cloudflare/codemode': 0.5.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.40(zod@4.4.3))(zod@4.4.3) isomorphic-git: 1.38.5 transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -9488,24 +9510,24 @@ snapshots: - ai - zod - '@cloudflare/think@0.15.0(patch_hash=443a9229f3de7d304667c7596e6efc61d24da59a94931cfd2306371cad259c26)(@ai-sdk/react@4.0.40(react@19.2.8)(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(agents@0.19.0(@ai-sdk/react@4.0.40(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260727.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(ai@7.0.37(zod@4.4.3))(chat@4.31.0(ai@7.0.37(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3))(ai@7.0.37(zod@4.4.3))(react@19.2.8)(zod@4.4.3)': + '@cloudflare/think@0.15.0(patch_hash=443a9229f3de7d304667c7596e6efc61d24da59a94931cfd2306371cad259c26)(@ai-sdk/react@4.0.43(react@19.2.8)(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(agents@0.19.0(@ai-sdk/react@4.0.43(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260728.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(ai@7.0.40(zod@4.4.3))(chat@4.31.0(ai@7.0.40(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3))(ai@7.0.40(zod@4.4.3))(react@19.2.8)(zod@4.4.3)': dependencies: - '@ai-sdk/anthropic': 4.0.21(zod@4.4.3) - '@ai-sdk/openai': 4.0.20(zod@4.4.3) - '@cloudflare/codemode': 0.5.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.37(zod@4.4.3))(zod@4.4.3) - '@cloudflare/shell': 0.4.3(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.37(zod@4.4.3))(zod@4.4.3) - agents: 0.19.0(@ai-sdk/react@4.0.40(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260727.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(ai@7.0.37(zod@4.4.3))(chat@4.31.0(ai@7.0.37(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3) - ai: 7.0.37(zod@4.4.3) + '@ai-sdk/anthropic': 4.0.23(zod@4.4.3) + '@ai-sdk/openai': 4.0.22(zod@4.4.3) + '@cloudflare/codemode': 0.5.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.40(zod@4.4.3))(zod@4.4.3) + '@cloudflare/shell': 0.4.3(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.40(zod@4.4.3))(zod@4.4.3) + agents: 0.19.0(@ai-sdk/react@4.0.43(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260728.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(ai@7.0.40(zod@4.4.3))(chat@4.31.0(ai@7.0.40(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3) + ai: 7.0.40(zod@4.4.3) aywson: 0.0.16 - chat: 4.31.0(ai@7.0.37(zod@4.4.3))(zod@4.4.3) + chat: 4.31.0(ai@7.0.40(zod@4.4.3))(zod@4.4.3) create-think: 0.1.1 just-bash: 3.0.2 smol-toml: 1.7.0 - workers-ai-provider: 4.0.0(@ai-sdk/anthropic@4.0.21(zod@4.4.3))(@ai-sdk/openai@4.0.20(zod@4.4.3))(ai@7.0.37(zod@4.4.3)) + workers-ai-provider: 4.0.0(@ai-sdk/anthropic@4.0.23(zod@4.4.3))(@ai-sdk/openai@4.0.22(zod@4.4.3))(ai@7.0.40(zod@4.4.3)) yargs: 18.0.0 zod: 4.4.3 optionalDependencies: - '@ai-sdk/react': 4.0.40(react@19.2.8)(zod@4.4.3) + '@ai-sdk/react': 4.0.43(react@19.2.8)(zod@4.4.3) react: 19.2.8 vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0)' transitivePeerDependencies: @@ -9521,20 +9543,20 @@ snapshots: optionalDependencies: workerd: 1.20260722.1 - '@cloudflare/vite-plugin@1.47.0(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(wrangler@4.114.0(@cloudflare/workers-types@5.20260727.1))': + '@cloudflare/vite-plugin@1.47.0(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(wrangler@4.114.0(@cloudflare/workers-types@5.20260728.1))': dependencies: '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260722.1) miniflare: 4.20260722.0 unenv: 2.0.0-rc.24 vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0)' workerd: 1.20260722.1 - wrangler: 4.114.0(@cloudflare/workers-types@5.20260727.1) + wrangler: 4.114.0(@cloudflare/workers-types@5.20260728.1) ws: 8.21.0 transitivePeerDependencies: - bufferutil - utf-8-validate - '@cloudflare/vitest-pool-workers@0.18.8(@cloudflare/workers-types@5.20260727.1)(@vitest/runner@4.1.10)(@vitest/snapshot@4.1.10)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(jsdom@28.1.0(@noble/hashes@2.2.0))(msw@2.14.6(@types/node@26.1.1)(typescript@6.0.3)))': + '@cloudflare/vitest-pool-workers@0.18.8(@cloudflare/workers-types@5.20260728.1)(@vitest/runner@4.1.10)(@vitest/snapshot@4.1.10)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(jsdom@28.1.0(@noble/hashes@2.2.0))(msw@2.14.6(@types/node@26.1.1)(typescript@6.0.3)))': dependencies: '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -9542,7 +9564,7 @@ snapshots: esbuild: 0.28.1 miniflare: 4.20260722.0 vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-preview@4.1.10)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(jsdom@28.1.0(@noble/hashes@2.2.0))(msw@2.14.6(@types/node@26.1.1)(typescript@6.0.3)) - wrangler: 4.114.0(@cloudflare/workers-types@5.20260727.1) + wrangler: 4.114.0(@cloudflare/workers-types@5.20260728.1) zod: 3.25.76 transitivePeerDependencies: - '@cloudflare/workers-types' @@ -9564,7 +9586,7 @@ snapshots: '@cloudflare/workerd-windows-64@1.20260722.1': optional: true - '@cloudflare/workers-types@5.20260727.1': {} + '@cloudflare/workers-types@5.20260728.1': {} '@commitlint/cli@21.2.1(@types/node@26.1.1)(conventional-commits-parser@7.0.1)(typescript@6.0.3)': dependencies: @@ -10318,9 +10340,17 @@ snapshots: '@noble/hashes': 2.2.0 optional: true + '@file_router/sdk@0.5.0(@llamaindex/llama-cloud@2.13.0)': + dependencies: + file-type: 22.0.1 + optionalDependencies: + '@llamaindex/llama-cloud': 2.13.0 + transitivePeerDependencies: + - supports-color + '@floating-ui/core@1.7.5': dependencies: - '@floating-ui/utils': 0.2.11 + '@floating-ui/utils': 0.2.12 '@floating-ui/core@1.8.0': dependencies: @@ -10346,8 +10376,7 @@ snapshots: '@floating-ui/utils@0.2.11': {} - '@floating-ui/utils@0.2.12': - optional: true + '@floating-ui/utils@0.2.12': {} '@fontsource-variable/geist@5.3.0': {} @@ -10561,6 +10590,9 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@llamaindex/llama-cloud@2.13.0': + optional: true + '@mermaid-js/parser@1.1.1': dependencies: '@chevrotain/types': 11.1.2 @@ -11235,7 +11267,7 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.1) - '@posthog/browser-common@0.2.2': + '@posthog/browser-common@0.2.3': dependencies: '@posthog/core': 1.45.1 '@posthog/types': 1.398.0 @@ -12778,28 +12810,28 @@ snapshots: prompts: 2.4.2 yaml: 2.9.0 - agents@0.19.0(@ai-sdk/react@4.0.40(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260727.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(ai@7.0.37(zod@4.4.3))(chat@4.31.0(ai@7.0.37(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3): + agents@0.19.0(@ai-sdk/react@4.0.43(react@19.2.8)(zod@4.4.3))(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@cloudflare/workers-types@5.20260728.1)(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(ai@7.0.40(zod@4.4.3))(chat@4.31.0(ai@7.0.40(zod@4.4.3))(zod@4.4.3))(just-bash@3.0.2)(react@19.2.8)(rolldown@1.1.2)(zod@4.4.3): dependencies: '@babel/plugin-proposal-decorators': 8.0.2(@babel/core@7.29.7) '@cfworker/json-schema': 4.1.1 - '@cloudflare/codemode': 0.5.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.37(zod@4.4.3))(zod@4.4.3) + '@cloudflare/codemode': 0.5.1(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@tanstack/ai@0.22.0(@opentelemetry/api@1.9.1))(ai@7.0.40(zod@4.4.3))(zod@4.4.3) '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3) '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(rolldown@1.1.2) cron-schedule: 6.0.0 esbuild: 0.28.1 mimetext: 3.0.28 nanoid: 5.1.16 - partyserver: 0.5.8(@cloudflare/workers-types@5.20260727.1) + partyserver: 0.5.8(@cloudflare/workers-types@5.20260728.1) partysocket: 1.3.0(react@19.2.8) react: 19.2.8 yaml: 2.9.0 yargs: 18.0.0 zod: 4.4.3 optionalDependencies: - '@ai-sdk/react': 4.0.40(react@19.2.8)(zod@4.4.3) + '@ai-sdk/react': 4.0.43(react@19.2.8)(zod@4.4.3) '@tanstack/ai': 0.22.0(@opentelemetry/api@1.9.1) - ai: 7.0.37(zod@4.4.3) - chat: 4.31.0(ai@7.0.37(zod@4.4.3))(zod@4.4.3) + ai: 7.0.40(zod@4.4.3) + chat: 4.31.0(ai@7.0.40(zod@4.4.3))(zod@4.4.3) just-bash: 3.0.2 vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0)' transitivePeerDependencies: @@ -12810,11 +12842,11 @@ snapshots: - rolldown - supports-color - ai@7.0.37(zod@4.4.3): + ai@7.0.40(zod@4.4.3): dependencies: - '@ai-sdk/gateway': 4.0.28(zod@4.4.3) - '@ai-sdk/provider': 4.0.3 - '@ai-sdk/provider-utils': 5.0.12(zod@4.4.3) + '@ai-sdk/gateway': 4.0.30(zod@4.4.3) + '@ai-sdk/provider': 4.0.4 + '@ai-sdk/provider-utils': 5.0.14(zod@4.4.3) zod: 4.4.3 ajv-formats@2.1.1(ajv@8.20.0): @@ -12884,13 +12916,13 @@ snapshots: stubborn-fs: 2.0.0 when-exit: 2.1.5 - autumn-js@1.2.44(better-auth@1.6.25(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@tanstack/react-start@1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.12)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vitest@4.1.10)(vue@3.5.38(typescript@6.0.3)))(better-call@1.3.7(zod@4.4.3))(express@5.2.1)(hono@4.12.27)(react@19.2.8): + autumn-js@1.2.44(better-auth@1.6.25(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@tanstack/react-start@1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.12)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vitest@4.1.10)(vue@3.5.38(typescript@6.0.3)))(better-call@1.3.7(zod@4.4.3))(express@5.2.1)(hono@4.12.27)(react@19.2.8): dependencies: query-string: 9.4.1 rou3: 0.6.3 zod: 4.4.3 optionalDependencies: - better-auth: 1.6.25(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@tanstack/react-start@1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.12)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vitest@4.1.10)(vue@3.5.38(typescript@6.0.3)) + better-auth: 1.6.25(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@tanstack/react-start@1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.12)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vitest@4.1.10)(vue@3.5.38(typescript@6.0.3)) better-call: 1.3.7(zod@4.4.3) express: 5.2.1 hono: 4.12.27 @@ -12932,15 +12964,15 @@ snapshots: baseline-browser-mapping@2.10.40: {} - better-auth@1.6.25(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@tanstack/react-start@1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.12)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vitest@4.1.10)(vue@3.5.38(typescript@6.0.3)): + better-auth@1.6.25(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@tanstack/react-start@1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1))(pg@8.22.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(solid-js@1.9.12)(svelte@5.56.3(@typescript-eslint/types@8.61.1))(vitest@4.1.10)(vue@3.5.38(typescript@6.0.3)): dependencies: - '@better-auth/core': 1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) - '@better-auth/drizzle-adapter': 1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1)) - '@better-auth/kysely-adapter': 1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(kysely@0.28.17) - '@better-auth/memory-adapter': 1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2) - '@better-auth/mongo-adapter': 1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2) - '@better-auth/prisma-adapter': 1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2) - '@better-auth/telemetry': 1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1) + '@better-auth/core': 1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0) + '@better-auth/drizzle-adapter': 1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1)) + '@better-auth/kysely-adapter': 1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(kysely@0.28.17) + '@better-auth/memory-adapter': 1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2) + '@better-auth/mongo-adapter': 1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2) + '@better-auth/prisma-adapter': 1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2) + '@better-auth/telemetry': 1.6.25(@better-auth/core@1.6.25(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(better-call@1.3.7(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1) '@better-auth/utils': 0.4.2 '@better-fetch/fetch': 1.3.1 '@noble/ciphers': 2.2.0 @@ -12954,7 +12986,7 @@ snapshots: optionalDependencies: '@tanstack/react-start': 1.168.32(@voidzero-dev/vite-plus-core@0.2.2(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(typescript@6.0.3)(yaml@2.9.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) drizzle-kit: 0.31.10 - drizzle-orm: 0.45.2(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1) + drizzle-orm: 0.45.2(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1) pg: 8.22.0 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -13087,7 +13119,7 @@ snapshots: character-reference-invalid@2.0.1: {} - chat@4.31.0(ai@7.0.37(zod@4.4.3))(zod@4.4.3): + chat@4.31.0(ai@7.0.40(zod@4.4.3))(zod@4.4.3): dependencies: '@workflow/serde': 4.1.0-beta.2 mdast-util-to-string: 4.0.0 @@ -13097,7 +13129,7 @@ snapshots: remend: 1.3.0 unified: 11.0.5 optionalDependencies: - ai: 7.0.37(zod@4.4.3) + ai: 7.0.40(zod@4.4.3) zod: 4.4.3 transitivePeerDependencies: - supports-color @@ -13586,9 +13618,9 @@ snapshots: esbuild: 0.25.12 tsx: 4.23.0 - drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260727.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1): + drizzle-orm@0.45.2(@cloudflare/workers-types@5.20260728.1)(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.22.0)(sql.js@1.14.1): optionalDependencies: - '@cloudflare/workers-types': 5.20260727.1 + '@cloudflare/workers-types': 5.20260728.1 '@opentelemetry/api': 1.9.1 '@types/pg': 8.20.0 kysely: 0.28.17 @@ -14023,6 +14055,15 @@ snapshots: transitivePeerDependencies: - supports-color + file-type@22.0.1: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -15787,9 +15828,9 @@ snapshots: partial-json@0.1.7: optional: true - partyserver@0.5.8(@cloudflare/workers-types@5.20260727.1): + partyserver@0.5.8(@cloudflare/workers-types@5.20260728.1): dependencies: - '@cloudflare/workers-types': 5.20260727.1 + '@cloudflare/workers-types': 5.20260728.1 nanoid: 5.1.16 partysocket@1.3.0(react@19.2.8): @@ -15925,7 +15966,7 @@ snapshots: posthog-js@1.407.3: dependencies: - '@posthog/browser-common': 0.2.2 + '@posthog/browser-common': 0.2.3 '@posthog/core': 1.45.1 '@posthog/types': 1.398.0 core-js: 3.49.0 @@ -16497,7 +16538,7 @@ snapshots: safe-buffer: 5.2.1 to-buffer: 1.2.2 - shadcn@4.15.0(@cfworker/json-schema@4.1.1)(typescript@6.0.3): + shadcn@4.16.0(@cfworker/json-schema@4.1.1)(typescript@6.0.3): dependencies: '@babel/core': 7.29.7 '@babel/parser': 7.29.7 @@ -17274,14 +17315,14 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260722.1 '@cloudflare/workerd-windows-64': 1.20260722.1 - workers-ai-provider@4.0.0(@ai-sdk/anthropic@4.0.21(zod@4.4.3))(@ai-sdk/openai@4.0.20(zod@4.4.3))(ai@7.0.37(zod@4.4.3)): + workers-ai-provider@4.0.0(@ai-sdk/anthropic@4.0.23(zod@4.4.3))(@ai-sdk/openai@4.0.22(zod@4.4.3))(ai@7.0.40(zod@4.4.3)): dependencies: - ai: 7.0.37(zod@4.4.3) + ai: 7.0.40(zod@4.4.3) optionalDependencies: - '@ai-sdk/anthropic': 4.0.21(zod@4.4.3) - '@ai-sdk/openai': 4.0.20(zod@4.4.3) + '@ai-sdk/anthropic': 4.0.23(zod@4.4.3) + '@ai-sdk/openai': 4.0.22(zod@4.4.3) - wrangler@4.114.0(@cloudflare/workers-types@5.20260727.1): + wrangler@4.114.0(@cloudflare/workers-types@5.20260728.1): dependencies: '@cloudflare/kv-asset-handler': 0.5.0 '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260722.1) @@ -17292,7 +17333,7 @@ snapshots: unenv: 2.0.0-rc.24 workerd: 1.20260722.1 optionalDependencies: - '@cloudflare/workers-types': 5.20260727.1 + '@cloudflare/workers-types': 5.20260728.1 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil @@ -17343,13 +17384,13 @@ snapshots: lib0: 0.2.117 yjs: 13.6.31 - y-partyserver@2.2.0(@cloudflare/workers-types@5.20260727.1)(partyserver@0.5.8(@cloudflare/workers-types@5.20260727.1))(yjs@13.6.31): + y-partyserver@2.2.0(@cloudflare/workers-types@5.20260728.1)(partyserver@0.5.8(@cloudflare/workers-types@5.20260728.1))(yjs@13.6.31): dependencies: - '@cloudflare/workers-types': 5.20260727.1 + '@cloudflare/workers-types': 5.20260728.1 lib0: 0.2.117 lodash.debounce: 4.0.8 nanoid: 5.1.16 - partyserver: 0.5.8(@cloudflare/workers-types@5.20260727.1) + partyserver: 0.5.8(@cloudflare/workers-types@5.20260728.1) y-protocols: 1.0.7(yjs@13.6.31) yjs: 13.6.31 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 79b9334e3..ba44bd7c4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -52,6 +52,7 @@ minimumReleaseAgeExclude: - "@cloudflare/codemode@0.5.0" - "@cloudflare/shell@0.4.3" - "@cloudflare/think@0.15.0" + - "@file_router/sdk@0.5.0" - agents@0.19.0 - "@voidzero-dev/vite-plus-core@0.2.6" - "@voidzero-dev/vite-plus-darwin-arm64@0.2.6" diff --git a/src/features/workspaces/extraction/filerouter-extraction.test.ts b/src/features/workspaces/extraction/filerouter-extraction.test.ts new file mode 100644 index 000000000..a1beb4288 --- /dev/null +++ b/src/features/workspaces/extraction/filerouter-extraction.test.ts @@ -0,0 +1,267 @@ +import type { HostedJobAccepted } from "@file_router/sdk"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const fileRouter = vi.hoisted(() => ({ + createDocument: vi.fn(), + createJob: vi.fn(), + getResult: vi.fn(), + options: vi.fn(), + releaseDocument: vi.fn(), + waitForExecution: vi.fn(), +})); +const workspace = vi.hoisted(() => ({ + getKernel: vi.fn(), + getSource: vi.fn(), + writeProjection: vi.fn(), +})); + +vi.mock("@file_router/sdk", () => ({ + FileRouter: class { + readonly documents = { + create: fileRouter.createDocument, + release: fileRouter.releaseDocument, + }; + readonly executions = { result: fileRouter.getResult }; + readonly jobs = { + create: fileRouter.createJob, + waitForExecution: fileRouter.waitForExecution, + }; + + constructor(options: unknown) { + fileRouter.options(options); + } + }, +})); +vi.mock("#/features/workspaces/extraction/workspace-file-source", () => ({ + getWorkspaceFileSourceObject: workspace.getSource, +})); +vi.mock("#/features/workspaces/extraction/workspace-page-projection", () => ({ + writeWorkspacePageProjection: workspace.writeProjection, +})); +vi.mock("#/features/workspaces/kernel/workspace-kernel-access", () => ({ + getWorkspaceKernelFromEnv: workspace.getKernel, +})); + +import { + createFileRouterExtractionJob, + releaseFileRouterDocument, + stageFileRouterProjection, + uploadWorkspaceFileToFileRouter, +} from "#/features/workspaces/extraction/filerouter-extraction"; + +const acceptedJob: HostedJobAccepted = { + executions: [ + { id: "execution_fast", key: "fast", provider: "liteparse" }, + { id: "execution_enhanced", key: "enhanced", provider: "llamaparse" }, + ], + id: "job_123", + status: "queued" as const, +}; + +beforeEach(() => { + for (const mock of Object.values(fileRouter)) { + mock.mockReset(); + } + for (const mock of Object.values(workspace)) { + mock.mockReset(); + } + fileRouter.createJob.mockResolvedValue(acceptedJob); +}); + +describe("FileRouter extraction", () => { + it("starts one keyed LiteParse and LlamaParse job", async () => { + const result = await createFileRouterExtractionJob( + { FILEROUTER_API_KEY: "fr_test" } as Cloudflare.Env, + { + documentId: "document_123", + idempotencyKey: "workflow_123:job", + }, + ); + + expect(result).toEqual(acceptedJob); + expect(fileRouter.options).toHaveBeenCalledWith({ + apiKey: "fr_test", + }); + expect(fileRouter.createJob).toHaveBeenCalledWith( + { + documentId: "document_123", + providers: [ + { + key: "fast", + outputs: ["pages"], + pageFields: ["markdown"], + provider: "liteparse", + providerOptions: { + imageMode: "off", + includeComplexity: false, + ocr: "off", + screenshots: false, + }, + }, + { + key: "enhanced", + outputs: ["pages"], + pageFields: ["markdown"], + provider: "llamaparse", + providerOptions: { + output_options: { + markdown: { + tables: { + output_tables_as_markdown: true, + }, + }, + }, + processing_options: { + cost_optimizer: { + enable: true, + }, + }, + tier: "agentic", + version: "latest", + }, + }, + ], + }, + { idempotencyKey: "workflow_123:job" }, + ); + }); + + it("streams the stored workspace source into FileRouter", async () => { + const body = new ReadableStream(); + workspace.getKernel.mockResolvedValue({ id: "kernel" }); + workspace.getSource.mockResolvedValue({ + object: { body, etag: "source-etag" }, + source: { contentType: "application/pdf", fileName: "source.pdf" }, + }); + fileRouter.createDocument.mockResolvedValue({ id: "document_123" }); + + const result = await uploadWorkspaceFileToFileRouter( + { FILEROUTER_API_KEY: "fr_test" } as Cloudflare.Env, + { + actorUserId: "user_123", + assetKind: "pdf", + itemId: "item_123", + requestId: "request_123", + workspaceId: "workspace_123", + }, + "workflow_123:document", + ); + + expect(result).toEqual({ + documentId: "document_123", + sourceHash: "source-etag", + }); + expect(fileRouter.createDocument).toHaveBeenCalledWith( + { + data: body, + kind: "stream", + mimeType: "application/pdf", + name: "source.pdf", + }, + { idempotencyKey: "workflow_123:document" }, + ); + }); + + it("waits on the exact keyed execution and stages normalized pages", async () => { + fileRouter.waitForExecution.mockResolvedValue({ + id: "execution_fast", + resultAvailable: true, + status: "complete", + }); + fileRouter.getResult.mockResolvedValue({ + id: "result_123", + outputs: { + pages: [ + { markdown: "# First", pageNumber: 1 }, + { markdown: "# Second", pageNumber: 2 }, + ], + }, + pageCount: 2, + provider: "liteparse", + timing: { durationMs: 42 }, + usage: { credits: 1 }, + warnings: [], + }); + workspace.writeProjection.mockResolvedValue({ + manifest: { markdownLength: 16, pageCount: 2 }, + manifestObjectKey: "projection/manifest.json", + }); + + const result = await stageFileRouterProjection( + { + FILEROUTER_API_KEY: "fr_test", + WORKSPACE_KERNEL_FILES: {} as R2Bucket, + } as Cloudflare.Env, + { + documentId: "document_123", + executionKey: "fast", + itemId: "item_123", + job: acceptedJob, + runId: "run_123", + sourceHash: "source-etag", + tier: "fast", + timeoutMs: 300_000, + workspaceId: "workspace_123", + }, + ); + + expect(fileRouter.waitForExecution).toHaveBeenCalledWith( + acceptedJob, + acceptedJob.executions[0], + { timeoutMs: 300_000 }, + ); + expect(fileRouter.getResult).toHaveBeenCalledWith("execution_fast"); + expect(workspace.writeProjection).toHaveBeenCalledWith( + expect.objectContaining({ + pages: [ + { markdown: "# First", pageNumber: 1 }, + { markdown: "# Second", pageNumber: 2 }, + ], + provider: "liteparse", + providerMode: "fast", + }), + ); + expect(result).toEqual( + expect.objectContaining({ + manifestObjectKey: "projection/manifest.json", + pageCount: 2, + provider: "liteparse", + providerMode: "fast", + routeReason: "filerouter_liteparse", + }), + ); + }); + + it("does not request an unavailable execution result", async () => { + fileRouter.waitForExecution.mockResolvedValue({ + id: "execution_enhanced", + resultAvailable: false, + status: "complete", + }); + + await expect( + stageFileRouterProjection({ FILEROUTER_API_KEY: "fr_test" } as Cloudflare.Env, { + documentId: "document_123", + executionKey: "enhanced", + itemId: "item_123", + job: acceptedJob, + runId: "run_123", + sourceHash: "source-etag", + tier: "enhanced", + timeoutMs: 600_000, + workspaceId: "workspace_123", + }), + ).rejects.toThrow("completed without an available result"); + expect(fileRouter.getResult).not.toHaveBeenCalled(); + expect(workspace.writeProjection).not.toHaveBeenCalled(); + }); + + it("releases document artifacts after extraction", async () => { + await releaseFileRouterDocument( + { FILEROUTER_API_KEY: "fr_test" } as Cloudflare.Env, + "document_123", + ); + + expect(fileRouter.releaseDocument).toHaveBeenCalledWith("document_123"); + }); +}); diff --git a/src/features/workspaces/extraction/filerouter-extraction.ts b/src/features/workspaces/extraction/filerouter-extraction.ts new file mode 100644 index 000000000..710739b7d --- /dev/null +++ b/src/features/workspaces/extraction/filerouter-extraction.ts @@ -0,0 +1,245 @@ +import { + FileRouter, + type HostedExecutionReference, + type HostedJobAccepted, + type HostedProviderTarget, + type ParseResult, +} from "@file_router/sdk"; + +import type { MarkdownProjectionPage } from "#/features/workspaces/extraction/page-markdown-projection"; +import type { + MarkdownExtractionProviderMode, + StagedPageProjection, + WorkspaceFileExtractionWorkflowParams, +} from "#/features/workspaces/extraction/types"; +import { getWorkspaceFileSourceObject } from "#/features/workspaces/extraction/workspace-file-source"; +import { writeWorkspacePageProjection } from "#/features/workspaces/extraction/workspace-page-projection"; +import { getWorkspaceKernelFromEnv } from "#/features/workspaces/kernel/workspace-kernel-access"; + +type FileRouterPdfExecutionKey = "enhanced" | "fast"; + +const pdfProviderByExecutionKey = { + enhanced: "llamaparse", + fast: "liteparse", +} as const satisfies Record; + +type FileRouterPdfProvider = (typeof pdfProviderByExecutionKey)[FileRouterPdfExecutionKey]; + +const pdfTargets = [ + { + key: "fast", + outputs: ["pages"], + pageFields: ["markdown"], + provider: "liteparse", + providerOptions: { + imageMode: "off", + includeComplexity: false, + ocr: "off", + screenshots: false, + }, + }, + { + key: "enhanced", + outputs: ["pages"], + pageFields: ["markdown"], + provider: "llamaparse", + providerOptions: { + output_options: { + markdown: { + tables: { + output_tables_as_markdown: true, + }, + }, + }, + processing_options: { + cost_optimizer: { + enable: true, + }, + }, + tier: "agentic" as const, + version: "latest" as const, + }, + }, +] satisfies HostedProviderTarget[]; + +export interface FileRouterDocumentReference { + documentId: string; + sourceHash: string; +} + +export async function uploadWorkspaceFileToFileRouter( + env: Cloudflare.Env, + params: WorkspaceFileExtractionWorkflowParams, + idempotencyKey: string, +): Promise { + const kernel = await getWorkspaceKernelFromEnv(env, params.workspaceId); + const { object, source } = await getWorkspaceFileSourceObject({ + env, + itemId: params.itemId, + kernel, + }); + const document = await createFileRouterClient(env).documents.create( + { + data: object.body, + kind: "stream", + mimeType: source.contentType, + name: source.fileName, + }, + { idempotencyKey }, + ); + + return { + documentId: document.id, + sourceHash: object.etag, + }; +} + +export async function createFileRouterExtractionJob( + env: Cloudflare.Env, + input: { + documentId: string; + idempotencyKey: string; + }, +) { + const job = await createFileRouterClient(env).jobs.create( + { + documentId: input.documentId, + providers: pdfTargets, + }, + { idempotencyKey: input.idempotencyKey }, + ); + + return job; +} + +export async function stageFileRouterProjection( + env: Cloudflare.Env, + input: { + documentId: string; + executionKey: FileRouterPdfExecutionKey; + itemId: string; + job: HostedJobAccepted; + runId: string; + sourceHash: string; + tier: "enhanced" | "fast"; + timeoutMs: number; + workspaceId: string; + }, +): Promise { + const client = createFileRouterClient(env); + const provider = pdfProviderByExecutionKey[input.executionKey]; + const executionReference = getExecutionReference(input.job, input.executionKey, provider); + const execution = await client.jobs.waitForExecution(input.job, executionReference, { + timeoutMs: input.timeoutMs, + }); + + if (execution.status !== "complete") { + throw new Error( + execution.error?.message ?? + `FileRouter ${provider} execution failed without an error message.`, + ); + } + if (!execution.resultAvailable) { + throw new Error(`FileRouter ${provider} execution completed without an available result.`); + } + + const result = await client.executions.result(execution.id); + const providerMode = getProviderMode(provider); + const metadata = getFileRouterResultMetadata(result, { + documentId: input.documentId, + executionId: execution.id, + jobId: input.job.id, + }); + const projection = await writeWorkspacePageProjection({ + bucket: env.WORKSPACE_KERNEL_FILES, + itemId: input.itemId, + metadata, + pages: getProjectionPages(result), + provider, + providerMode, + runId: input.runId, + sourceHash: input.sourceHash, + tier: input.tier, + workspaceId: input.workspaceId, + }); + + return { + manifestObjectKey: projection.manifestObjectKey, + markdownLength: projection.manifest.markdownLength, + metadata, + pageCount: projection.manifest.pageCount, + provider, + providerMode, + routeReason: `filerouter_${provider}`, + sourceHash: input.sourceHash, + }; +} + +export function releaseFileRouterDocument(env: Cloudflare.Env, documentId: string) { + return createFileRouterClient(env).documents.release(documentId); +} + +function createFileRouterClient(env: Cloudflare.Env) { + return new FileRouter({ + apiKey: env.FILEROUTER_API_KEY, + }); +} + +function getProjectionPages(result: ParseResult): MarkdownProjectionPage[] { + const pages = result.outputs.pages; + if (!pages) { + throw new Error(`FileRouter ${result.provider} result did not include pages.`); + } + + return pages.map((page) => ({ + markdown: page.markdown ?? "", + pageNumber: page.pageNumber, + })); +} + +function getProviderMode(provider: FileRouterPdfProvider): MarkdownExtractionProviderMode { + switch (provider) { + case "liteparse": + return "fast"; + case "llamaparse": + return "agentic"; + } +} + +function getExecutionReference( + job: HostedJobAccepted, + key: FileRouterPdfExecutionKey, + expectedProvider: FileRouterPdfProvider, +): HostedExecutionReference { + const execution = job.executions.find((candidate) => candidate.key === key); + if (!execution) { + throw new Error(`FileRouter job ${job.id} did not include the ${key} execution.`); + } + if (execution.provider !== expectedProvider) { + throw new Error( + `FileRouter job ${job.id} assigned ${execution.provider} to ${key}; expected ${expectedProvider}.`, + ); + } + + return execution; +} + +function getFileRouterResultMetadata( + result: ParseResult, + resources: { + documentId: string; + executionId: string; + jobId: string; + }, +) { + return { + credits: result.usage?.credits ?? null, + documentId: resources.documentId, + durationMs: result.timing.durationMs, + executionId: resources.executionId, + jobId: resources.jobId, + pageCount: result.pageCount, + resultId: result.id, + warningCount: result.warnings.length, + }; +} diff --git a/src/features/workspaces/extraction/liteparse-projection.ts b/src/features/workspaces/extraction/liteparse-projection.ts deleted file mode 100644 index 4766e7877..000000000 --- a/src/features/workspaces/extraction/liteparse-projection.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { WorkflowStep } from "cloudflare:workers"; - -import { extractPdfWithLiteParse } from "#/features/workspaces/extraction/providers/liteparse"; -import type { - LiteParseStageOutcome, - WorkspaceFileExtractionWorkflowParams, -} from "#/features/workspaces/extraction/types"; -import { getWorkspaceFileSourceObject } from "#/features/workspaces/extraction/workspace-file-source"; -import { writeWorkspacePageProjection } from "#/features/workspaces/extraction/workspace-page-projection"; -import { getWorkspaceKernelFromEnv } from "#/features/workspaces/kernel/workspace-kernel-access"; -import { recordOperationalFailure } from "#/integrations/observability/operational-events"; - -export async function publishLiteParseProjection( - env: Cloudflare.Env, - step: WorkflowStep, - params: WorkspaceFileExtractionWorkflowParams, - runId: string, -): Promise { - if (params.assetKind !== "pdf") { - return { durationMs: 0, outcome: "skipped" }; - } - - const startedAt = Date.now(); - - try { - return await step.do( - "publish fast LiteParse projection", - { - retries: { limit: 1, delay: "5 seconds", backoff: "constant" }, - timeout: "2 minutes", - }, - async () => { - const kernel = await getWorkspaceKernelFromEnv(env, params.workspaceId); - const { object, source } = await getWorkspaceFileSourceObject({ - env, - itemId: params.itemId, - kernel, - }); - const projection = await writeWorkspacePageProjection({ - bucket: env.WORKSPACE_KERNEL_FILES, - itemId: params.itemId, - pages: extractPdfWithLiteParse(env, { - body: object.body, - fileName: source.fileName, - sizeBytes: source.sizeBytes, - }), - provider: "liteparse", - providerMode: "fast", - runId, - sourceHash: object.etag, - tier: "fast", - workspaceId: params.workspaceId, - }); - - await kernel.upsertFileProjection({ - itemId: params.itemId, - format: "pages", - status: "ready", - objectKey: projection.manifestObjectKey, - provider: "liteparse", - providerMode: "fast", - sourceHash: object.etag, - metadataJson: { - markdownLength: projection.manifest.markdownLength, - pageCount: projection.manifest.pageCount, - provisional: true, - }, - actorUserId: params.actorUserId, - clientMutationId: `${runId}:projection:liteparse-ready`, - }); - - return { - durationMs: Date.now() - startedAt, - markdownLength: projection.manifest.markdownLength, - outcome: "success" as const, - pageCount: projection.manifest.pageCount, - }; - }, - ); - } catch (error) { - recordOperationalFailure({ - distinctId: params.actorUserId ?? undefined, - error, - event: "workspace_liteparse_projection", - fields: { - item_id: params.itemId, - request_id: params.requestId, - workflow_id: runId, - workspace_id: params.workspaceId, - }, - }); - return { - durationMs: Date.now() - startedAt, - errorType: error instanceof Error ? error.name : "UnknownError", - outcome: "error", - }; - } -} diff --git a/src/features/workspaces/extraction/providers/firecrawl.ts b/src/features/workspaces/extraction/providers/firecrawl.ts deleted file mode 100644 index e58d4de29..000000000 --- a/src/features/workspaces/extraction/providers/firecrawl.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type { - FirecrawlPdfMode, - MarkdownExtractionInput, - MarkdownExtractionProvider, - MarkdownExtractionResult, -} from "#/features/workspaces/extraction/types"; -import { createSingleMarkdownProjectionPage } from "#/features/workspaces/extraction/page-markdown-projection"; -import { - firecrawlJsonRequest, - getFirstArrayRecord, - getNumberValue, - getRecordValue, - getStringValue, -} from "#/integrations/firecrawl/client"; -import { createStreamingMultipartFile } from "#/lib/http/streaming-multipart"; - -const firecrawlParseTimeoutMs = 300_000; - -export function createFirecrawlPdfExtractionProvider( - env: Cloudflare.Env, -): MarkdownExtractionProvider { - return { - id: "firecrawl", - async extract(input) { - const mode = normalizeFirecrawlMode(input.mode); - const multipart = createStreamingMultipartFile({ - body: input.body, - contentType: input.contentType || "application/pdf", - fields: { - options: JSON.stringify({ - formats: ["markdown"], - parsers: [{ type: "pdf", mode }], - timeout: firecrawlParseTimeoutMs, - }), - }, - fileName: input.fileName, - formFieldName: "file", - sizeBytes: input.sizeBytes, - }); - - const [responseJson] = await Promise.all([ - firecrawlJsonRequest({ - env, - path: "/v2/parse", - operation: "Firecrawl PDF parsing", - method: "POST", - headers: { "content-type": multipart.contentType }, - body: multipart.body, - }), - multipart.done, - ]); - - const markdown = getFirecrawlMarkdown(responseJson); - - if (!markdown) { - throw new Error("Firecrawl PDF parsing completed without markdown output."); - } - - return { - pages: createSingleMarkdownProjectionPage(markdown), - provider: "firecrawl", - providerMode: mode, - metadata: getFirecrawlMetadata(responseJson), - } satisfies MarkdownExtractionResult; - }, - }; -} - -function normalizeFirecrawlMode(mode: MarkdownExtractionInput["mode"]): FirecrawlPdfMode { - if (mode === "fast" || mode === "ocr") { - return mode; - } - - return "auto"; -} - -function getFirecrawlMarkdown(value: unknown): string | null { - const candidates = [ - value, - getRecordValue(value, "data"), - getRecordValue(value, "document"), - getRecordValue(value, "result"), - getFirstArrayRecord(getRecordValue(value, "data")), - getFirstArrayRecord(getRecordValue(value, "documents")), - getFirstArrayRecord(getRecordValue(value, "results")), - ]; - - for (const candidate of candidates) { - const markdown = getRecordValue(candidate, "markdown"); - - if (typeof markdown === "string" && markdown.trim().length > 0) { - return markdown; - } - } - - return null; -} - -function getFirecrawlMetadata(value: unknown) { - const data = getRecordValue(value, "data"); - const usage = getRecordValue(value, "usage") ?? getRecordValue(data, "usage"); - const metadata = getRecordValue(data, "metadata") ?? getRecordValue(value, "metadata"); - const creditsUsed = - getNumberValue(metadata, "creditsUsed") ?? - getNumberValue(usage, "credits") ?? - getNumberValue(data, "creditsUsed") ?? - getNumberValue(value, "creditsUsed"); - const title = getStringValue(metadata, "title") ?? getStringValue(data, "title"); - const sourceFile = getStringValue(metadata, "sourceFile") ?? getStringValue(data, "sourceFile"); - const pageCount = - getNumberValue(metadata, "numPages") ?? - getNumberValue(metadata, "pageCount") ?? - getNumberValue(data, "numPages") ?? - getNumberValue(data, "pageCount") ?? - getNumberValue(value, "numPages") ?? - getNumberValue(value, "pageCount"); - const result: Record = {}; - - if (creditsUsed !== null) { - result.creditsUsed = creditsUsed; - } - - if (pageCount !== null) { - result.numPages = pageCount; - result.pageCount = pageCount; - } - - if (title !== null) { - result.title = title; - } - - if (sourceFile !== null) { - result.sourceFile = sourceFile; - } - - return result; -} diff --git a/src/features/workspaces/extraction/providers/index.ts b/src/features/workspaces/extraction/providers/index.ts deleted file mode 100644 index 452f0c29a..000000000 --- a/src/features/workspaces/extraction/providers/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createFirecrawlPdfExtractionProvider } from "#/features/workspaces/extraction/providers/firecrawl"; -import { createLlamaParseExtractionProvider } from "#/features/workspaces/extraction/providers/llama-parse"; -import { createStubMarkdownExtractionProvider } from "#/features/workspaces/extraction/providers/stubs"; -import { createWorkersAiToMarkdownProvider } from "#/features/workspaces/extraction/providers/workers-ai-to-markdown"; -import type { - MarkdownExtractionProvider, - MarkdownExtractionProviderId, -} from "#/features/workspaces/extraction/types"; - -export function createMarkdownExtractionProvider( - providerId: MarkdownExtractionProviderId, - env: Env, -): MarkdownExtractionProvider { - switch (providerId) { - case "firecrawl": - return createFirecrawlPdfExtractionProvider(env); - case "workers_ai_to_markdown": - return createWorkersAiToMarkdownProvider(env); - case "llama_parse": - return createLlamaParseExtractionProvider(env); - case "mistral_ocr": - return createStubMarkdownExtractionProvider(providerId); - } -} diff --git a/src/features/workspaces/extraction/providers/liteparse-response.ts b/src/features/workspaces/extraction/providers/liteparse-response.ts deleted file mode 100644 index aa5e0f959..000000000 --- a/src/features/workspaces/extraction/providers/liteparse-response.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { MarkdownProjectionPage } from "#/features/workspaces/extraction/page-markdown-projection"; - -export function parseLiteParsePage(payload: unknown): MarkdownProjectionPage { - if ( - !isRecord(payload) || - typeof payload.pageNumber !== "number" || - !Number.isInteger(payload.pageNumber) || - payload.pageNumber < 1 || - typeof payload.markdown !== "string" - ) { - throw new Error("LiteParse returned an invalid page."); - } - - return { pageNumber: payload.pageNumber, markdown: payload.markdown.trim() }; -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} diff --git a/src/features/workspaces/extraction/providers/liteparse.test.ts b/src/features/workspaces/extraction/providers/liteparse.test.ts deleted file mode 100644 index 7e4ee1e0c..000000000 --- a/src/features/workspaces/extraction/providers/liteparse.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import { extractPdfWithLiteParse } from "#/features/workspaces/extraction/providers/liteparse"; -import { parseLiteParsePage } from "#/features/workspaces/extraction/providers/liteparse-response"; -import { requestWorkspaceFileProcessor } from "#/features/workspaces/files/workspace-file-processor"; - -vi.mock("#/features/workspaces/files/workspace-file-processor", () => ({ - requestWorkspaceFileProcessor: vi.fn(), -})); - -describe("LiteParse response parsing", () => { - it("accepts one canonical page record", () => { - expect(parseLiteParsePage({ markdown: " # First ", pageNumber: 1 })).toEqual({ - markdown: "# First", - pageNumber: 1, - }); - }); - - it("keeps blank pages so PDF page numbering remains stable", () => { - expect(parseLiteParsePage({ markdown: " \n\t", pageNumber: 1 })).toEqual({ - markdown: "", - pageNumber: 1, - }); - }); - - it.each([ - undefined, - {}, - { markdown: "document markdown without page data" }, - { markdown: "Text", pageNumber: 0 }, - { markdown: 123, pageNumber: 1 }, - ])("rejects malformed container responses", (payload) => { - expect(() => parseLiteParsePage(payload)).toThrow("LiteParse returned an invalid"); - }); - - it("rejects an oversized NDJSON record and cancels the processor response", async () => { - const cancel = vi.fn(); - const body = new ReadableStream({ - cancel, - start(controller) { - controller.enqueue(new Uint8Array(8 * 1024 * 1024 + 1)); - }, - }); - vi.mocked(requestWorkspaceFileProcessor).mockResolvedValue({ - body, - ok: true, - } as Response); - const pages = extractPdfWithLiteParse({} as Cloudflare.Env, { - body: new ReadableStream(), - fileName: "document.pdf", - sizeBytes: 1, - }); - - await expect(pages.next()).rejects.toThrow("oversized NDJSON page"); - expect(cancel).toHaveBeenCalledOnce(); - }); -}); diff --git a/src/features/workspaces/extraction/providers/liteparse.ts b/src/features/workspaces/extraction/providers/liteparse.ts deleted file mode 100644 index 88b1e91f1..000000000 --- a/src/features/workspaces/extraction/providers/liteparse.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { MarkdownProjectionPage } from "#/features/workspaces/extraction/page-markdown-projection"; -import { parseLiteParsePage } from "#/features/workspaces/extraction/providers/liteparse-response"; -import { requestWorkspaceFileProcessor } from "#/features/workspaces/files/workspace-file-processor"; - -const maxNdjsonLineBytes = 8 * 1024 * 1024; - -export async function* extractPdfWithLiteParse( - env: Cloudflare.Env, - input: { - body: ReadableStream; - fileName: string; - sizeBytes: number; - }, -): AsyncGenerator { - const response = await requestWorkspaceFileProcessor(env, { - body: input.body, - contentType: "application/pdf", - fileName: input.fileName, - path: "/parse/pdf", - sizeBytes: input.sizeBytes, - }); - - if (!response.ok) { - throw new Error(`LiteParse failed with status ${response.status}.`); - } - - if (!response.body) { - throw new Error("LiteParse completed without a response body."); - } - - for await (const line of readNdjsonLines(response.body)) { - let payload: unknown; - try { - payload = JSON.parse(line); - } catch { - throw new Error("LiteParse returned invalid NDJSON."); - } - yield parseLiteParsePage(payload); - } -} - -async function* readNdjsonLines(body: ReadableStream): AsyncGenerator { - const reader = body.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - let pendingLineBytes = 0; - - try { - while (true) { - const { done, value } = await reader.read(); - - if (value) { - let offset = 0; - while (offset < value.byteLength) { - const newlineOffset = value.indexOf(0x0a, offset); - const segmentEnd = newlineOffset === -1 ? value.byteLength : newlineOffset; - pendingLineBytes += segmentEnd - offset; - if (pendingLineBytes > maxNdjsonLineBytes) { - throw new Error("LiteParse returned an oversized NDJSON page."); - } - if (newlineOffset === -1) { - break; - } - pendingLineBytes = 0; - offset = newlineOffset + 1; - } - } - - buffer += decoder.decode(value, { stream: !done }); - const lines = buffer.split("\n"); - buffer = lines.pop() ?? ""; - - for (const line of lines) { - if (line.trim()) { - yield line; - } - } - - if (done) { - break; - } - } - - if (buffer.trim()) { - yield buffer; - } - } finally { - await reader.cancel().catch(() => undefined); - reader.releaseLock(); - } -} diff --git a/src/features/workspaces/extraction/providers/llama-parse.ts b/src/features/workspaces/extraction/providers/llama-parse.ts deleted file mode 100644 index 0771a7179..000000000 --- a/src/features/workspaces/extraction/providers/llama-parse.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { - createSingleMarkdownProjectionPage, - type MarkdownProjectionPage, -} from "#/features/workspaces/extraction/page-markdown-projection"; -import type { - LlamaParseTier, - MarkdownExtractionInput, - MarkdownExtractionProvider, - MarkdownExtractionResult, -} from "#/features/workspaces/extraction/types"; -import { - getNumberValue, - getRecordArrayValue, - getRecordValue, - getStringValue, - llamaCloudJsonRequest, -} from "#/integrations/llamaparse/client"; -import { createStreamingMultipartFile } from "#/lib/http/streaming-multipart"; - -const llamaParsePollIntervalMs = 2_000; -const llamaParseMaxPollMs = 300_000; -const llamaParseVersion = "latest"; - -export function createLlamaParseExtractionProvider(env: Env): MarkdownExtractionProvider { - return { - id: "llama_parse", - async extract(input) { - const tier = normalizeLlamaParseTier(input.mode); - const fileId = await uploadLlamaParseFile(env, input); - const jobId = await startLlamaParseJob(env, { fileId, tier }); - const result = await pollLlamaParseJob(env, jobId); - const pages = getLlamaParseMarkdownPages(result); - const projectionPages = - pages.length > 0 - ? pages - : createSingleMarkdownProjectionPage(getLlamaParseMarkdown(result) ?? ""); - - if (projectionPages.length === 0) { - throw new Error("LlamaParse completed without markdown output."); - } - - return { - pages: projectionPages, - provider: "llama_parse", - providerMode: tier, - metadata: getLlamaParseMetadata(result, { - fileId, - jobId, - pageCount: projectionPages.length, - tier, - }), - } satisfies MarkdownExtractionResult; - }, - }; -} - -function normalizeLlamaParseTier(mode: MarkdownExtractionInput["mode"]): LlamaParseTier { - if (mode === "cost_effective" || mode === "agentic_plus") { - return mode; - } - - return "agentic"; -} - -async function uploadLlamaParseFile(env: Env, input: MarkdownExtractionInput) { - const multipart = createStreamingMultipartFile({ - body: input.body, - contentType: input.contentType || "application/pdf", - fields: { purpose: "parse" }, - fileName: input.fileName, - formFieldName: "file", - sizeBytes: input.sizeBytes, - }); - const [responseJson] = await Promise.all([ - llamaCloudJsonRequest({ - env, - path: "/api/v1/beta/files", - operation: "LlamaParse file upload", - method: "POST", - headers: { "content-type": multipart.contentType }, - body: multipart.body, - }), - multipart.done, - ]); - const fileId = getStringValue(responseJson, "id"); - - if (!fileId) { - throw new Error("LlamaParse file upload completed without a file id."); - } - - return fileId; -} - -async function startLlamaParseJob( - env: Env, - input: { - fileId: string; - tier: LlamaParseTier; - }, -) { - const responseJson = await llamaCloudJsonRequest({ - env, - path: "/api/v2/parse", - operation: "LlamaParse job start", - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - file_id: input.fileId, - tier: input.tier, - version: llamaParseVersion, - output_options: { - markdown: { - tables: { - output_tables_as_markdown: true, - }, - }, - }, - processing_options: { - cost_optimizer: { - enable: true, - }, - }, - }), - }); - const jobId = getStringValue(responseJson, "id"); - - if (!jobId) { - throw new Error("LlamaParse job start completed without a job id."); - } - - return jobId; -} - -async function pollLlamaParseJob(env: Env, jobId: string) { - const startedAt = Date.now(); - - while (Date.now() - startedAt < llamaParseMaxPollMs) { - const responseJson = await llamaCloudJsonRequest({ - env, - path: `/api/v2/parse/${jobId}`, - operation: "LlamaParse job status", - }); - const job = getRecordValue(responseJson, "job") ?? responseJson; - const status = getStringValue(job, "status"); - - if (status === "COMPLETED") { - return await llamaCloudJsonRequest({ - env, - path: `/api/v2/parse/${jobId}?expand=markdown,metadata,job_metadata`, - operation: "LlamaParse job result", - }); - } - - if (status === "FAILED" || status === "CANCELLED") { - throw new Error(`LlamaParse job ${status.toLowerCase()}.`); - } - - await wait(llamaParsePollIntervalMs); - } - - throw new Error("LlamaParse job timed out."); -} - -function getLlamaParseMarkdownPages(value: unknown): MarkdownProjectionPage[] { - const markdown = getRecordValue(value, "markdown"); - const pages = getRecordArrayValue(markdown, "pages"); - - return pages - .map((page, index) => { - const pageMarkdown = getStringValue(page, "markdown")?.trim(); - - if (!pageMarkdown) { - return null; - } - - return { - pageNumber: - getNumberValue(page, "page") ?? - getNumberValue(page, "page_number") ?? - getNumberValue(page, "pageNumber") ?? - index + 1, - markdown: pageMarkdown, - } satisfies MarkdownProjectionPage; - }) - .filter((page): page is MarkdownProjectionPage => page !== null); -} - -function getLlamaParseMarkdown(value: unknown) { - const markdown = getRecordValue(value, "markdown"); - const candidates = [ - getStringValue(markdown, "markdown"), - getStringValue(markdown, "text"), - getStringValue(value, "markdown"), - getStringValue(value, "text"), - ]; - - return candidates.find((candidate) => candidate && candidate.trim().length > 0)?.trim() ?? null; -} - -function getLlamaParseMetadata( - value: unknown, - input: { - fileId: string; - jobId: string; - pageCount: number; - tier: LlamaParseTier; - }, -) { - const metadata = getRecordValue(value, "metadata"); - const usage = getRecordValue(value, "usage"); - const job = getRecordValue(value, "job"); - const result: Record = { - fileId: input.fileId, - jobId: input.jobId, - tier: input.tier, - version: getStringValue(metadata, "version") ?? llamaParseVersion, - }; - const pageCount = - input.pageCount || - getNumberValue(metadata, "page_count") || - getNumberValue(metadata, "pageCount") || - getNumberValue(value, "page_count") || - getNumberValue(value, "pageCount"); - const creditsUsed = - getNumberValue(usage, "credits") ?? - getNumberValue(usage, "credits_used") ?? - getNumberValue(metadata, "credits") ?? - getNumberValue(metadata, "credits_used"); - const status = getStringValue(job, "status") ?? getStringValue(value, "status"); - - if (pageCount !== null) { - result.numPages = pageCount; - result.pageCount = pageCount; - } - - if (creditsUsed !== null) { - result.creditsUsed = creditsUsed; - } - - if (status !== null) { - result.status = status; - } - - return result; -} - -function wait(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/src/features/workspaces/extraction/providers/stubs.ts b/src/features/workspaces/extraction/providers/stubs.ts deleted file mode 100644 index ca52b556e..000000000 --- a/src/features/workspaces/extraction/providers/stubs.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { - MarkdownExtractionProvider, - MarkdownExtractionProviderId, -} from "#/features/workspaces/extraction/types"; - -const implementedMarkdownExtractionProviders = [ - "firecrawl", - "llama_parse", - "workers_ai_to_markdown", -] as const satisfies readonly MarkdownExtractionProviderId[]; - -type StubMarkdownExtractionProviderId = Exclude< - MarkdownExtractionProviderId, - (typeof implementedMarkdownExtractionProviders)[number] ->; - -export function createStubMarkdownExtractionProvider( - id: StubMarkdownExtractionProviderId, -): MarkdownExtractionProvider { - return { - id, - async extract() { - throw new Error( - `${id} markdown extraction is intentionally stubbed. Add credentials, pricing limits, and routing rules before routing uploads here.`, - ); - }, - }; -} diff --git a/src/features/workspaces/extraction/providers/workers-ai-to-markdown.ts b/src/features/workspaces/extraction/providers/workers-ai-to-markdown.ts deleted file mode 100644 index 3aedc7cf8..000000000 --- a/src/features/workspaces/extraction/providers/workers-ai-to-markdown.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { normalizeChatImageToJpeg } from "#/features/workspaces/conversion/image-normalizer"; -import { WORKSPACE_AI_CHAT_ATTACHMENT_POLICY } from "#/features/workspaces/ai/chat-attachment-policy"; -import type { - MarkdownExtractionProvider, - MarkdownExtractionResult, -} from "#/features/workspaces/extraction/types"; -import { createSingleMarkdownProjectionPage } from "#/features/workspaces/extraction/page-markdown-projection"; - -export function createWorkersAiToMarkdownProvider(env: Env): MarkdownExtractionProvider { - return { - id: "workers_ai_to_markdown", - async extract(input) { - const conversion = await normalizeChatImageToJpeg( - env, - input.body, - WORKSPACE_AI_CHAT_ATTACHMENT_POLICY.maxNormalizedFileSize, - ); - const result = await env.AI.toMarkdown( - { - name: input.fileName, - blob: new Blob([conversion.bytes], { - type: "image/jpeg", - }), - }, - { - conversionOptions: { - image: { - descriptionLanguage: "en", - }, - }, - }, - ); - - if (result.format === "error") { - throw new Error(`Workers AI toMarkdown failed: ${result.error}`); - } - - const markdown = result.data.trim(); - - if (!markdown) { - throw new Error("Workers AI toMarkdown completed without markdown output."); - } - - return { - pages: createSingleMarkdownProjectionPage(markdown), - provider: "workers_ai_to_markdown", - providerMode: "default", - metadata: getWorkersAiToMarkdownMetadata(result), - } satisfies MarkdownExtractionResult; - }, - }; -} - -function getWorkersAiToMarkdownMetadata(result: { mimeType: string; tokens: number }) { - return { - mimeType: result.mimeType, - tokens: result.tokens, - }; -} diff --git a/src/features/workspaces/extraction/types.ts b/src/features/workspaces/extraction/types.ts index 5dbc9b86c..d2df23859 100644 --- a/src/features/workspaces/extraction/types.ts +++ b/src/features/workspaces/extraction/types.ts @@ -1,19 +1,8 @@ -import type { - WorkspaceFileAssetKind, - WorkspaceFileExtractionMode, - WorkspaceFileExtractionProviderId, -} from "#/features/workspaces/model/workspace-file/types"; -import { workspaceFileExtractionProviders } from "#/features/workspaces/model/workspace-file/types"; -import type { MarkdownProjectionPage } from "#/features/workspaces/extraction/page-markdown-projection"; +import type { WorkspaceFileAssetKind } from "#/features/workspaces/model/workspace-file/types"; -export type MarkdownExtractionProviderId = WorkspaceFileExtractionProviderId; +export type MarkdownExtractionProviderId = "liteparse" | "llamaparse" | "workers_ai_to_markdown"; -export type MarkdownExtractionProviderMode = WorkspaceFileExtractionMode; - -export { workspaceFileExtractionProviders as markdownExtractionProviders }; - -export type FirecrawlPdfMode = "fast" | "auto" | "ocr"; -export type LlamaParseTier = "cost_effective" | "agentic" | "agentic_plus"; +export type MarkdownExtractionProviderMode = "agentic" | "default" | "fast"; export interface WorkspaceFileExtractionWorkflowParams { workspaceId: string; @@ -23,6 +12,17 @@ export interface WorkspaceFileExtractionWorkflowParams { requestId: string | null; } +export interface StagedPageProjection { + manifestObjectKey: string; + markdownLength: number; + metadata: Record; + pageCount: number; + provider: MarkdownExtractionProviderId; + providerMode: MarkdownExtractionProviderMode; + routeReason: string; + sourceHash: string; +} + export type LiteParseStageOutcome = | { durationMs: number; outcome: "skipped" } | { durationMs: number; errorType: string; outcome: "error" } @@ -32,26 +32,3 @@ export type LiteParseStageOutcome = outcome: "success"; pageCount: number; }; - -export interface MarkdownExtractionInput { - workspaceId: string; - itemId: string; - body: ReadableStream; - fileName: string; - contentType: string; - sizeBytes: number; - sourceHash: string; - mode: MarkdownExtractionProviderMode; -} - -export interface MarkdownExtractionResult { - pages: MarkdownProjectionPage[]; - provider: MarkdownExtractionProviderId; - providerMode: MarkdownExtractionProviderMode; - metadata: Record; -} - -export interface MarkdownExtractionProvider { - id: MarkdownExtractionProviderId; - extract(input: MarkdownExtractionInput): Promise; -} diff --git a/src/features/workspaces/extraction/workers-ai-image-extraction.ts b/src/features/workspaces/extraction/workers-ai-image-extraction.ts new file mode 100644 index 000000000..91897f72c --- /dev/null +++ b/src/features/workspaces/extraction/workers-ai-image-extraction.ts @@ -0,0 +1,49 @@ +import { WORKSPACE_AI_CHAT_ATTACHMENT_POLICY } from "#/features/workspaces/ai/chat-attachment-policy"; +import { normalizeChatImageToJpeg } from "#/features/workspaces/conversion/image-normalizer"; +import { createSingleMarkdownProjectionPage } from "#/features/workspaces/extraction/page-markdown-projection"; + +export async function extractImageWithWorkersAi( + env: Cloudflare.Env, + input: { + body: ReadableStream; + fileName: string; + }, +) { + const conversion = await normalizeChatImageToJpeg( + env, + input.body, + WORKSPACE_AI_CHAT_ATTACHMENT_POLICY.maxNormalizedFileSize, + ); + const result = await env.AI.toMarkdown( + { + name: input.fileName, + blob: new Blob([conversion.bytes], { type: "image/jpeg" }), + }, + { + conversionOptions: { + image: { + descriptionLanguage: "en", + }, + }, + }, + ); + + if (result.format === "error") { + throw new Error(`Workers AI toMarkdown failed: ${result.error}`); + } + + const pages = createSingleMarkdownProjectionPage(result.data); + if (pages.length === 0) { + throw new Error("Workers AI toMarkdown completed without markdown output."); + } + + return { + metadata: { + mimeType: result.mimeType, + tokens: result.tokens, + }, + pages, + provider: "workers_ai_to_markdown" as const, + providerMode: "default" as const, + }; +} diff --git a/src/features/workspaces/extraction/workspace-file-extraction-observability.ts b/src/features/workspaces/extraction/workspace-file-extraction-observability.ts index 1d3d1364f..b24c7a48c 100644 --- a/src/features/workspaces/extraction/workspace-file-extraction-observability.ts +++ b/src/features/workspaces/extraction/workspace-file-extraction-observability.ts @@ -1,3 +1,5 @@ +import type { WorkflowStep } from "cloudflare:workers"; + import type { LiteParseStageOutcome, MarkdownExtractionProviderId, @@ -32,7 +34,7 @@ type WorkspaceFileExtractionOutcome = WorkspaceFileExtractionOutcomeBase & | { outcome: "partial" | "success"; pageCount: number; - provider: MarkdownExtractionProviderId | "liteparse"; + provider: MarkdownExtractionProviderId; providerMode: MarkdownExtractionProviderMode; routeReason: string; } @@ -111,6 +113,32 @@ export function recordWorkspaceFileExtractionOutcome(input: WorkspaceFileExtract }); } +export async function recordWorkspaceFileExtractionOutcomeStep( + step: WorkflowStep, + name: string, + input: WorkspaceFileExtractionOutcome, +) { + try { + await step.do(name, async () => { + recordWorkspaceFileExtractionOutcome(input); + return { outcome: input.outcome }; + }); + } catch (error) { + recordOperationalFailure({ + distinctId: input.params.actorUserId ?? undefined, + error, + event: "workspace_file_extraction_observability", + fields: { + item_id: input.params.itemId, + recorded_outcome: input.outcome, + workflow_id: input.instanceId, + workspace_id: input.params.workspaceId, + }, + schedule: input.schedule, + }); + } +} + function getErrorType(error: unknown) { return error instanceof Error ? error.name : "UnknownError"; } diff --git a/src/features/workspaces/extraction/workspace-file-extraction-state.ts b/src/features/workspaces/extraction/workspace-file-extraction-state.ts new file mode 100644 index 000000000..f5fa3a4a7 --- /dev/null +++ b/src/features/workspaces/extraction/workspace-file-extraction-state.ts @@ -0,0 +1,95 @@ +import type { WorkflowStep } from "cloudflare:workers"; + +import type { + StagedPageProjection, + WorkspaceFileExtractionWorkflowParams, +} from "#/features/workspaces/extraction/types"; +import { getWorkspaceKernelFromEnv } from "#/features/workspaces/kernel/workspace-kernel-access"; + +export async function markWorkspaceFileExtractionProcessing( + env: Cloudflare.Env, + step: WorkflowStep, + params: WorkspaceFileExtractionWorkflowParams, + runId: string, +) { + await step.do("mark extraction processing", async () => { + const kernel = await getWorkspaceKernelFromEnv(env, params.workspaceId); + await kernel.upsertFileProjection({ + itemId: params.itemId, + format: "pages", + status: "processing", + actorUserId: params.actorUserId, + clientMutationId: `${runId}:projection:processing`, + }); + + return { status: "processing" }; + }); +} + +export function publishWorkspaceFileProjection( + env: Cloudflare.Env, + step: WorkflowStep, + params: WorkspaceFileExtractionWorkflowParams, + runId: string, + extraction: StagedPageProjection, + provisional: boolean, +) { + return step.do( + provisional ? "publish fast FileRouter projection" : "publish enhanced projection", + { + retries: { limit: 3, delay: "10 seconds", backoff: "exponential" }, + timeout: "5 minutes", + }, + async () => { + const kernel = await getWorkspaceKernelFromEnv(env, params.workspaceId); + await kernel.upsertFileProjection({ + itemId: params.itemId, + format: "pages", + status: "ready", + objectKey: extraction.manifestObjectKey, + provider: extraction.provider, + providerMode: extraction.providerMode, + sourceHash: extraction.sourceHash, + metadataJson: { + ...extraction.metadata, + markdownLength: extraction.markdownLength, + pageCount: extraction.pageCount, + provisional, + routeReason: extraction.routeReason, + }, + actorUserId: params.actorUserId, + clientMutationId: `${runId}:projection:${provisional ? "fast" : "enhanced"}-ready`, + }); + + return { + pageCount: extraction.pageCount, + provider: extraction.provider, + providerMode: extraction.providerMode, + status: "ready" as const, + }; + }, + ); +} + +export async function markWorkspaceFileExtractionFailed( + env: Cloudflare.Env, + step: WorkflowStep, + params: WorkspaceFileExtractionWorkflowParams, + runId: string, + error: unknown, +) { + await step.do("mark extraction failed", async () => { + const kernel = await getWorkspaceKernelFromEnv(env, params.workspaceId); + const errorMessage = error instanceof Error ? error.message : String(error); + await kernel.upsertFileProjection({ + itemId: params.itemId, + format: "pages", + status: "failed", + errorMessage, + actorUserId: params.actorUserId, + clientMutationId: `${runId}:projection:failed`, + }); + + return { status: "failed", errorMessage }; + }); +} diff --git a/src/features/workspaces/extraction/workspace-file-extraction-workflow.ts b/src/features/workspaces/extraction/workspace-file-extraction-workflow.ts index 446226008..d5eeeaee7 100644 --- a/src/features/workspaces/extraction/workspace-file-extraction-workflow.ts +++ b/src/features/workspaces/extraction/workspace-file-extraction-workflow.ts @@ -1,17 +1,9 @@ import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from "cloudflare:workers"; -import { publishLiteParseProjection } from "#/features/workspaces/extraction/liteparse-projection"; -import { recordWorkspaceFileExtractionOutcome } from "#/features/workspaces/extraction/workspace-file-extraction-observability"; -import { createMarkdownExtractionProvider } from "#/features/workspaces/extraction/providers/index"; -import type { - MarkdownExtractionProviderId, - MarkdownExtractionProviderMode, - WorkspaceFileExtractionWorkflowParams, -} from "#/features/workspaces/extraction/types"; -import { getWorkspaceFileSourceObject } from "#/features/workspaces/extraction/workspace-file-source"; -import { writeWorkspacePageProjection } from "#/features/workspaces/extraction/workspace-page-projection"; -import { getWorkspaceKernelFromEnv } from "#/features/workspaces/kernel/workspace-kernel-access"; -import { getWorkspaceUploadFamily } from "#/features/workspaces/model/workspace-file"; +import type { WorkspaceFileExtractionWorkflowParams } from "#/features/workspaces/extraction/types"; +import { markWorkspaceFileExtractionProcessing } from "#/features/workspaces/extraction/workspace-file-extraction-state"; +import { runWorkspaceImageExtraction } from "#/features/workspaces/extraction/workspace-image-extraction"; +import { runWorkspacePdfExtraction } from "#/features/workspaces/extraction/workspace-pdf-extraction"; export class WorkspaceFileExtractionWorkflow extends WorkflowEntrypoint< Cloudflare.Env, @@ -24,231 +16,22 @@ export class WorkspaceFileExtractionWorkflow extends WorkflowEntrypoint< const params = assertWorkflowParams(event.payload); const schedule = (task: Promise) => this.ctx.waitUntil(task); - await step.do("mark extraction processing", async () => { - const kernel = await getWorkspaceKernelFromEnv(this.env, params.workspaceId); - await kernel.upsertFileProjection({ - itemId: params.itemId, - format: "pages", - status: "processing", - actorUserId: params.actorUserId, - clientMutationId: `${event.instanceId}:projection:processing`, - }); + await markWorkspaceFileExtractionProcessing(this.env, step, params, event.instanceId); - return { status: "processing" }; - }); - - const liteParse = await publishLiteParseProjection(this.env, step, params, event.instanceId); - const enhancementStartedAt = Date.now(); - let extraction: StagedPageExtractionResult; - let result: { - pageCount: number; - provider: MarkdownExtractionProviderId; - providerMode: MarkdownExtractionProviderMode; - status: "ready"; + const input = { + env: this.env, + event, + params, + schedule, + step, }; - try { - extraction = await step.do( - "extract page markdown with provider", - { - retries: { - limit: 2, - delay: "30 seconds", - backoff: "exponential", - }, - timeout: "10 minutes", - }, - async (): Promise => { - const kernel = await getWorkspaceKernelFromEnv(this.env, params.workspaceId); - const { object, source } = await getWorkspaceFileSourceObject({ - env: this.env, - itemId: params.itemId, - kernel, - }); - const route = getWorkspaceUploadFamily(params.assetKind).extractionRoute; - const provider = createMarkdownExtractionProvider(route.provider, this.env); - const extraction = await provider.extract({ - workspaceId: params.workspaceId, - itemId: params.itemId, - body: object.body, - fileName: source.fileName, - contentType: source.contentType, - sizeBytes: source.sizeBytes, - sourceHash: object.etag, - mode: route.mode, - }); - - const projection = await writeWorkspacePageProjection({ - bucket: this.env.WORKSPACE_KERNEL_FILES, - itemId: params.itemId, - metadata: extraction.metadata, - pages: extraction.pages, - provider: extraction.provider, - providerMode: extraction.providerMode, - runId: event.instanceId, - sourceHash: object.etag, - tier: "enhanced", - workspaceId: params.workspaceId, - }); - - return { - manifestObjectKey: projection.manifestObjectKey, - markdownLength: projection.manifest.markdownLength, - provider: extraction.provider, - providerMode: extraction.providerMode, - metadata: extraction.metadata, - pageCount: projection.manifest.pageCount, - routeReason: route.reason, - sourceHash: object.etag, - }; - }, - ); - - result = await step.do( - "write extracted projections", - { - retries: { - limit: 3, - delay: "10 seconds", - backoff: "exponential", - }, - timeout: "5 minutes", - }, - async () => { - const kernel = await getWorkspaceKernelFromEnv(this.env, params.workspaceId); - const metadataJson = { - ...extraction.metadata, - routeReason: extraction.routeReason, - pageCount: extraction.pageCount, - markdownLength: extraction.markdownLength, - }; - - await kernel.upsertFileProjection({ - itemId: params.itemId, - format: "pages", - status: "ready", - objectKey: extraction.manifestObjectKey, - provider: extraction.provider, - providerMode: extraction.providerMode, - sourceHash: extraction.sourceHash, - metadataJson, - actorUserId: params.actorUserId, - clientMutationId: `${event.instanceId}:projection:enhanced-ready`, - }); - - return { - status: "ready" as const, - provider: extraction.provider, - providerMode: extraction.providerMode, - pageCount: extraction.pageCount, - }; - }, - ); - } catch (error) { - if (liteParse.outcome === "success") { - await step.do("record partial extraction outcome", async () => { - recordWorkspaceFileExtractionOutcome({ - durationMs: Date.now() - event.timestamp.getTime(), - enhancement: { - durationMs: Date.now() - enhancementStartedAt, - error, - outcome: "error", - }, - instanceId: event.instanceId, - liteParse, - outcome: "partial", - pageCount: liteParse.pageCount, - params, - provider: "liteparse", - providerMode: "fast", - routeReason: "LiteParse projection retained after enhancement failed.", - schedule, - }); - - return { outcome: "partial" }; - }); - - return { - pageCount: liteParse.pageCount, - provider: "liteparse", - providerMode: "fast", - status: "ready", - }; - } - - await step.do("mark extraction failed", async () => { - const kernel = await getWorkspaceKernelFromEnv(this.env, params.workspaceId); - const errorMessage = getErrorMessage(error); - await kernel.upsertFileProjection({ - itemId: params.itemId, - format: "pages", - status: "failed", - errorMessage, - actorUserId: params.actorUserId, - clientMutationId: `${event.instanceId}:projection:failed`, - }); - - return { status: "failed", errorMessage }; - }); - - await step.do("record extraction failure", async () => { - recordWorkspaceFileExtractionOutcome({ - durationMs: Date.now() - event.timestamp.getTime(), - enhancement: { - durationMs: Date.now() - enhancementStartedAt, - error, - outcome: "error", - }, - error, - instanceId: event.instanceId, - liteParse, - outcome: "error", - params, - schedule, - }); - - return { outcome: "error" }; - }); - - throw error; - } - - await step.do("record extraction outcome", async () => { - recordWorkspaceFileExtractionOutcome({ - durationMs: Date.now() - event.timestamp.getTime(), - enhancement: { - durationMs: Date.now() - enhancementStartedAt, - outcome: "success", - }, - instanceId: event.instanceId, - liteParse, - outcome: "success", - pageCount: extraction.pageCount, - params, - provider: extraction.provider, - providerMode: extraction.providerMode, - routeReason: extraction.routeReason, - schedule, - }); - - return { outcome: "success" }; - }); - - return result; + return params.assetKind === "pdf" + ? runWorkspacePdfExtraction(input) + : runWorkspaceImageExtraction(input); } } -interface StagedPageExtractionResult { - manifestObjectKey: string; - markdownLength: number; - provider: MarkdownExtractionProviderId; - providerMode: MarkdownExtractionProviderMode; - metadata: Record; - pageCount: number; - routeReason: string; - sourceHash: string; -} - function assertWorkflowParams( value: Readonly, ): WorkspaceFileExtractionWorkflowParams { @@ -264,7 +47,3 @@ function assertWorkflowParams( requestId: value.requestId ?? null, }; } - -function getErrorMessage(error: unknown) { - return error instanceof Error ? error.message : String(error); -} diff --git a/src/features/workspaces/extraction/workspace-image-extraction.ts b/src/features/workspaces/extraction/workspace-image-extraction.ts new file mode 100644 index 000000000..8bbdfb915 --- /dev/null +++ b/src/features/workspaces/extraction/workspace-image-extraction.ts @@ -0,0 +1,127 @@ +import type { WorkflowEvent, WorkflowStep } from "cloudflare:workers"; + +import type { + StagedPageProjection, + WorkspaceFileExtractionWorkflowParams, +} from "#/features/workspaces/extraction/types"; +import { recordWorkspaceFileExtractionOutcomeStep } from "#/features/workspaces/extraction/workspace-file-extraction-observability"; +import { + markWorkspaceFileExtractionFailed, + publishWorkspaceFileProjection, +} from "#/features/workspaces/extraction/workspace-file-extraction-state"; +import { getWorkspaceFileSourceObject } from "#/features/workspaces/extraction/workspace-file-source"; +import { writeWorkspacePageProjection } from "#/features/workspaces/extraction/workspace-page-projection"; +import { extractImageWithWorkersAi } from "#/features/workspaces/extraction/workers-ai-image-extraction"; +import { getWorkspaceKernelFromEnv } from "#/features/workspaces/kernel/workspace-kernel-access"; +import type { PostHogTelemetryScheduler } from "#/integrations/posthog/scheduler"; + +export async function runWorkspaceImageExtraction(input: { + env: Cloudflare.Env; + event: Readonly>; + params: WorkspaceFileExtractionWorkflowParams; + schedule: PostHogTelemetryScheduler; + step: WorkflowStep; +}) { + const startedAt = Date.now(); + const liteParse = { durationMs: 0, outcome: "skipped" as const }; + + try { + const extraction = await input.step.do( + "extract image with Workers AI", + { + retries: { limit: 2, delay: "30 seconds", backoff: "exponential" }, + timeout: "10 minutes", + }, + () => stageImageProjection(input), + ); + const result = await publishWorkspaceFileProjection( + input.env, + input.step, + input.params, + input.event.instanceId, + extraction, + false, + ); + + await recordWorkspaceFileExtractionOutcomeStep(input.step, "record extraction outcome", { + durationMs: Date.now() - input.event.timestamp.getTime(), + enhancement: { + durationMs: Date.now() - startedAt, + outcome: "success", + }, + instanceId: input.event.instanceId, + liteParse, + outcome: "success", + pageCount: extraction.pageCount, + params: input.params, + provider: extraction.provider, + providerMode: extraction.providerMode, + routeReason: extraction.routeReason, + schedule: input.schedule, + }); + + return result; + } catch (error) { + await markWorkspaceFileExtractionFailed( + input.env, + input.step, + input.params, + input.event.instanceId, + error, + ); + await recordWorkspaceFileExtractionOutcomeStep(input.step, "record extraction failure", { + durationMs: Date.now() - input.event.timestamp.getTime(), + enhancement: { + durationMs: Date.now() - startedAt, + error, + outcome: "error", + }, + error, + instanceId: input.event.instanceId, + liteParse, + outcome: "error", + params: input.params, + schedule: input.schedule, + }); + + throw error; + } +} + +async function stageImageProjection( + input: Parameters[0], +): Promise { + const kernel = await getWorkspaceKernelFromEnv(input.env, input.params.workspaceId); + const { object, source } = await getWorkspaceFileSourceObject({ + env: input.env, + itemId: input.params.itemId, + kernel, + }); + const result = await extractImageWithWorkersAi(input.env, { + body: object.body, + fileName: source.fileName, + }); + const projection = await writeWorkspacePageProjection({ + bucket: input.env.WORKSPACE_KERNEL_FILES, + itemId: input.params.itemId, + metadata: result.metadata, + pages: result.pages, + provider: result.provider, + providerMode: result.providerMode, + runId: input.event.instanceId, + sourceHash: object.etag, + tier: "enhanced", + workspaceId: input.params.workspaceId, + }); + + return { + manifestObjectKey: projection.manifestObjectKey, + markdownLength: projection.manifest.markdownLength, + metadata: result.metadata, + pageCount: projection.manifest.pageCount, + provider: result.provider, + providerMode: result.providerMode, + routeReason: "workers_ai_image_to_markdown", + sourceHash: object.etag, + }; +} diff --git a/src/features/workspaces/extraction/workspace-pdf-extraction.ts b/src/features/workspaces/extraction/workspace-pdf-extraction.ts new file mode 100644 index 000000000..fe19169bf --- /dev/null +++ b/src/features/workspaces/extraction/workspace-pdf-extraction.ts @@ -0,0 +1,294 @@ +import type { WorkflowEvent, WorkflowStep } from "cloudflare:workers"; +import type { HostedJobAccepted } from "@file_router/sdk"; + +import { + createFileRouterExtractionJob, + type FileRouterDocumentReference, + releaseFileRouterDocument, + stageFileRouterProjection, + uploadWorkspaceFileToFileRouter, +} from "#/features/workspaces/extraction/filerouter-extraction"; +import type { + LiteParseStageOutcome, + WorkspaceFileExtractionWorkflowParams, +} from "#/features/workspaces/extraction/types"; +import { recordWorkspaceFileExtractionOutcomeStep } from "#/features/workspaces/extraction/workspace-file-extraction-observability"; +import { + markWorkspaceFileExtractionFailed, + publishWorkspaceFileProjection, +} from "#/features/workspaces/extraction/workspace-file-extraction-state"; +import { recordOperationalFailure } from "#/integrations/observability/operational-events"; +import type { PostHogTelemetryScheduler } from "#/integrations/posthog/scheduler"; + +const fastExtractionTimeoutMs = 5 * 60_000; +const enhancedExtractionTimeoutMs = 10 * 60_000; + +interface PdfExtractionInput { + env: Cloudflare.Env; + event: Readonly>; + params: WorkspaceFileExtractionWorkflowParams; + schedule: PostHogTelemetryScheduler; + step: WorkflowStep; +} + +export async function runWorkspacePdfExtraction(input: PdfExtractionInput) { + const document = await uploadSourceDocument(input); + const result = await extractPdf(input, document); + await releaseSourceDocument(input, document.documentId); + return result; +} + +async function extractPdf(input: PdfExtractionInput, document: FileRouterDocumentReference) { + let failureStartedAt = Date.now(); + let liteParse: LiteParseStageOutcome = { durationMs: 0, outcome: "skipped" }; + + try { + const job = await input.step.do( + "start FileRouter extraction", + { + retries: { limit: 2, delay: "10 seconds", backoff: "exponential" }, + timeout: "2 minutes", + }, + () => + createFileRouterExtractionJob(input.env, { + documentId: document.documentId, + idempotencyKey: `${input.event.instanceId}:job`, + }), + ); + liteParse = await publishFastProjection(input, document, job); + failureStartedAt = Date.now(); + const extraction = await input.step.do( + "stage enhanced FileRouter projection", + { + retries: { limit: 2, delay: "30 seconds", backoff: "exponential" }, + timeout: "12 minutes", + }, + () => + stageFileRouterProjection(input.env, { + documentId: document.documentId, + executionKey: "enhanced", + itemId: input.params.itemId, + job, + runId: input.event.instanceId, + sourceHash: document.sourceHash, + tier: "enhanced", + timeoutMs: enhancedExtractionTimeoutMs, + workspaceId: input.params.workspaceId, + }), + ); + const result = await publishWorkspaceFileProjection( + input.env, + input.step, + input.params, + input.event.instanceId, + extraction, + false, + ); + + await recordWorkspaceFileExtractionOutcomeStep(input.step, "record extraction outcome", { + durationMs: Date.now() - input.event.timestamp.getTime(), + enhancement: { + durationMs: Date.now() - failureStartedAt, + outcome: "success", + }, + instanceId: input.event.instanceId, + liteParse, + outcome: "success", + pageCount: extraction.pageCount, + params: input.params, + provider: extraction.provider, + providerMode: extraction.providerMode, + routeReason: extraction.routeReason, + schedule: input.schedule, + }); + + return result; + } catch (error) { + return handlePdfExtractionFailure(input, liteParse, failureStartedAt, error); + } +} + +async function publishFastProjection( + input: PdfExtractionInput, + document: FileRouterDocumentReference, + job: HostedJobAccepted, +): Promise { + const startedAt = Date.now(); + try { + const extraction = await input.step.do( + "stage fast FileRouter projection", + { + retries: { limit: 1, delay: "10 seconds", backoff: "constant" }, + timeout: "7 minutes", + }, + () => + stageFileRouterProjection(input.env, { + documentId: document.documentId, + executionKey: "fast", + itemId: input.params.itemId, + job, + runId: input.event.instanceId, + sourceHash: document.sourceHash, + tier: "fast", + timeoutMs: fastExtractionTimeoutMs, + workspaceId: input.params.workspaceId, + }), + ); + await publishWorkspaceFileProjection( + input.env, + input.step, + input.params, + input.event.instanceId, + extraction, + true, + ); + + return { + durationMs: Date.now() - startedAt, + markdownLength: extraction.markdownLength, + outcome: "success", + pageCount: extraction.pageCount, + }; + } catch (error) { + recordOperationalFailure({ + distinctId: input.params.actorUserId ?? undefined, + error, + event: "workspace_liteparse_projection", + fields: { + item_id: input.params.itemId, + request_id: input.params.requestId, + workflow_id: input.event.instanceId, + workspace_id: input.params.workspaceId, + }, + schedule: input.schedule, + }); + + return { + durationMs: Date.now() - startedAt, + errorType: error instanceof Error ? error.name : "UnknownError", + outcome: "error", + }; + } +} + +async function handlePdfExtractionFailure( + input: PdfExtractionInput, + liteParse: LiteParseStageOutcome, + failureStartedAt: number, + error: unknown, +) { + if (liteParse.outcome === "success") { + await recordWorkspaceFileExtractionOutcomeStep( + input.step, + "record partial extraction outcome", + { + durationMs: Date.now() - input.event.timestamp.getTime(), + enhancement: { + durationMs: Date.now() - failureStartedAt, + error, + outcome: "error", + }, + instanceId: input.event.instanceId, + liteParse, + outcome: "partial", + pageCount: liteParse.pageCount, + params: input.params, + provider: "liteparse", + providerMode: "fast", + routeReason: "filerouter_liteparse_fallback", + schedule: input.schedule, + }, + ); + + return { + pageCount: liteParse.pageCount, + provider: "liteparse" as const, + providerMode: "fast" as const, + status: "ready" as const, + }; + } + + return failPdfExtraction(input, liteParse, failureStartedAt, error); +} + +async function uploadSourceDocument( + input: PdfExtractionInput, +): Promise { + const startedAt = Date.now(); + + try { + return await input.step.do( + "upload source to FileRouter", + { + retries: { limit: 2, delay: "10 seconds", backoff: "exponential" }, + timeout: "5 minutes", + }, + () => + uploadWorkspaceFileToFileRouter( + input.env, + input.params, + `${input.event.instanceId}:document`, + ), + ); + } catch (error) { + return failPdfExtraction(input, { durationMs: 0, outcome: "skipped" }, startedAt, error); + } +} + +async function failPdfExtraction( + input: PdfExtractionInput, + liteParse: LiteParseStageOutcome, + failureStartedAt: number, + error: unknown, +): Promise { + await markWorkspaceFileExtractionFailed( + input.env, + input.step, + input.params, + input.event.instanceId, + error, + ); + await recordWorkspaceFileExtractionOutcomeStep(input.step, "record extraction failure", { + durationMs: Date.now() - input.event.timestamp.getTime(), + enhancement: { + durationMs: Date.now() - failureStartedAt, + error, + outcome: "error", + }, + error, + instanceId: input.event.instanceId, + liteParse, + outcome: "error", + params: input.params, + schedule: input.schedule, + }); + + throw error; +} + +async function releaseSourceDocument(input: PdfExtractionInput, documentId: string) { + try { + await input.step.do( + "release FileRouter document artifacts", + { + retries: { limit: 3, delay: "10 seconds", backoff: "exponential" }, + timeout: "2 minutes", + }, + () => releaseFileRouterDocument(input.env, documentId), + ); + } catch (error) { + recordOperationalFailure({ + distinctId: input.params.actorUserId ?? undefined, + error, + event: "workspace_file_extraction_cleanup", + fields: { + document_id: documentId, + item_id: input.params.itemId, + request_id: input.params.requestId, + workflow_id: input.event.instanceId, + workspace_id: input.params.workspaceId, + }, + schedule: input.schedule, + }); + } +} diff --git a/src/features/workspaces/files/workspace-file-processor.ts b/src/features/workspaces/files/workspace-file-processor.ts index 00d90ed2d..b522a4f33 100644 --- a/src/features/workspaces/files/workspace-file-processor.ts +++ b/src/features/workspaces/files/workspace-file-processor.ts @@ -3,7 +3,6 @@ import { Container, getRandom } from "@cloudflare/containers"; const workspaceFileProcessorPort = 8080; const workspaceFileProcessorPoolSize = 2; const processorRequestTimeoutMs = { - "/parse/pdf": 10 * 60_000, "/prepare/pdf": 2 * 60_000, "/preview/image": 2 * 60_000, } as const; @@ -21,7 +20,7 @@ export async function requestWorkspaceFileProcessor( body: ReadableStream; contentType: string; fileName?: string; - path: "/parse/pdf" | "/prepare/pdf" | "/preview/image"; + path: "/prepare/pdf" | "/preview/image"; sizeBytes: number; }, ) { diff --git a/src/features/workspaces/model/workspace-file/index.ts b/src/features/workspaces/model/workspace-file/index.ts index aefa7e057..3d80775ac 100644 --- a/src/features/workspaces/model/workspace-file/index.ts +++ b/src/features/workspaces/model/workspace-file/index.ts @@ -30,12 +30,8 @@ export { } from "#/features/workspaces/model/workspace-file/resolve"; export { type WorkspaceFileAssetKind, - type WorkspaceFileExtractionMode, - type WorkspaceFileExtractionProviderId, - type WorkspaceFileExtractionRoute, workspaceFileAssetKindSchema, workspaceFileAssetKinds, - workspaceFileExtractionProviders, } from "#/features/workspaces/model/workspace-file/types"; export { getWorkspaceFileContentUrl, diff --git a/src/features/workspaces/model/workspace-file/limits.ts b/src/features/workspaces/model/workspace-file/limits.ts index deb5b0a1c..2aa90f17e 100644 --- a/src/features/workspaces/model/workspace-file/limits.ts +++ b/src/features/workspaces/model/workspace-file/limits.ts @@ -1,8 +1,8 @@ export const workspaceFileUploadLimits = { maxFilesPerSelection: 50, - maxFileBytes: 100 * 1024 * 1024, + maxFileBytes: 100_000_000, maxImageFileBytes: 20 * 1024 * 1024, - maxSelectionBytes: 100 * 1024 * 1024, + maxSelectionBytes: 100_000_000, maxDocumentImportBytes: 10 * 1024 * 1024, concurrency: 3, } as const; diff --git a/src/features/workspaces/model/workspace-file/policy.ts b/src/features/workspaces/model/workspace-file/policy.ts index 67bb9e322..17785aff6 100644 --- a/src/features/workspaces/model/workspace-file/policy.ts +++ b/src/features/workspaces/model/workspace-file/policy.ts @@ -1,10 +1,7 @@ import { FileText, Image, type LucideIcon } from "lucide-react"; import { normalizeWorkspaceItemName } from "#/features/workspaces/defaults"; -import type { - WorkspaceFileAssetKind, - WorkspaceFileExtractionRoute, -} from "#/features/workspaces/model/workspace-file/types"; +import type { WorkspaceFileAssetKind } from "#/features/workspaces/model/workspace-file/types"; import { getDeniedWorkspaceUploadMessage, normalizeUploadContentType, @@ -30,7 +27,6 @@ export interface WorkspaceUploadFamily { icon: LucideIcon; defaultFileName: string; requiresHeavyViewerRuntime: boolean; - extractionRoute: WorkspaceFileExtractionRoute; } export interface WorkspaceFileTypeDescriptor extends WorkspaceUploadFamily { @@ -105,11 +101,6 @@ const WORKSPACE_UPLOAD_FAMILIES = [ icon: FileText, defaultFileName: "Uploaded file.pdf", requiresHeavyViewerRuntime: true, - extractionRoute: { - provider: "llama_parse", - mode: "agentic", - reason: "default_pdf_upload_route", - }, }, { assetKind: "image", @@ -118,11 +109,6 @@ const WORKSPACE_UPLOAD_FAMILIES = [ icon: Image, defaultFileName: "Uploaded image.png", requiresHeavyViewerRuntime: false, - extractionRoute: { - provider: "workers_ai_to_markdown", - mode: "default", - reason: "default_image_upload_route", - }, }, ] as const satisfies readonly WorkspaceUploadFamily[]; diff --git a/src/features/workspaces/model/workspace-file/types.ts b/src/features/workspaces/model/workspace-file/types.ts index f3bab7de1..6238f2b8d 100644 --- a/src/features/workspaces/model/workspace-file/types.ts +++ b/src/features/workspaces/model/workspace-file/types.ts @@ -4,28 +4,3 @@ export const workspaceFileAssetKinds = ["pdf", "image"] as const; export const workspaceFileAssetKindSchema = z.enum(workspaceFileAssetKinds); export type WorkspaceFileAssetKind = (typeof workspaceFileAssetKinds)[number]; - -export const workspaceFileExtractionProviders = [ - "firecrawl", - "workers_ai_to_markdown", - "mistral_ocr", - "llama_parse", -] as const; - -export type WorkspaceFileExtractionProviderId = (typeof workspaceFileExtractionProviders)[number]; - -export type WorkspaceFileExtractionMode = - | "fast" - | "auto" - | "ocr" - | "default" - | "stub" - | "cost_effective" - | "agentic" - | "agentic_plus"; - -export interface WorkspaceFileExtractionRoute { - provider: WorkspaceFileExtractionProviderId; - mode: WorkspaceFileExtractionMode; - reason: string; -} diff --git a/src/integrations/firecrawl/client.ts b/src/integrations/firecrawl/client.ts index ace362902..1a2d2e7c3 100644 --- a/src/integrations/firecrawl/client.ts +++ b/src/integrations/firecrawl/client.ts @@ -68,10 +68,6 @@ export function getRecordArrayValue(value: unknown, key: string) { return Array.isArray(field) ? field : []; } -export function getFirstArrayRecord(value: unknown) { - return Array.isArray(value) ? (value[0] ?? null) : null; -} - export function getStringValue(value: unknown, key: string) { const field = getRecordValue(value, key); return typeof field === "string" && field.trim().length > 0 ? field : null; diff --git a/src/integrations/llamaparse/client.ts b/src/integrations/llamaparse/client.ts deleted file mode 100644 index b2f385ab3..000000000 --- a/src/integrations/llamaparse/client.ts +++ /dev/null @@ -1,85 +0,0 @@ -const defaultLlamaCloudApiUrl = "https://api.cloud.llamaindex.ai"; - -export async function llamaCloudJsonRequest(input: { - env: Env; - path: string; - operation: string; - method?: string; - body?: BodyInit | null; - headers?: HeadersInit; -}) { - const headers = new Headers({ - Accept: "application/json", - Authorization: `Bearer ${input.env.LLAMA_CLOUD_API_KEY}`, - }); - new Headers(input.headers).forEach((value, key) => { - headers.set(key, value); - }); - - const response = await fetch(getLlamaCloudUrl(input.path), { - method: input.method ?? "GET", - headers, - body: input.body, - }); - const responseJson = (await response.json().catch(() => null)) as unknown; - - if (!response.ok) { - throw new Error( - `${input.operation} failed (${response.status}): ${getLlamaCloudErrorMessage(responseJson)}`, - ); - } - - return responseJson; -} - -export function getLlamaCloudApiUrl() { - return defaultLlamaCloudApiUrl; -} - -export function getLlamaCloudUrl(path: string) { - const baseUrl = `${getLlamaCloudApiUrl()}/`; - return new URL(path.replace(/^\/+/, ""), baseUrl); -} - -export function getLlamaCloudErrorMessage(value: unknown) { - const error = getRecordValue(value, "error"); - const detail = getRecordValue(value, "detail"); - const message = getRecordValue(value, "message"); - - if (typeof error === "string") { - return error; - } - - if (typeof detail === "string") { - return detail; - } - - if (typeof message === "string") { - return message; - } - - return "unknown error"; -} - -export function getRecordValue(value: unknown, key: string) { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return null; - } - - return (value as Record)[key] ?? null; -} - -export function getRecordArrayValue(value: unknown, key: string) { - const field = getRecordValue(value, key); - return Array.isArray(field) ? field : []; -} - -export function getStringValue(value: unknown, key: string) { - const field = getRecordValue(value, key); - return typeof field === "string" && field.trim().length > 0 ? field : null; -} - -export function getNumberValue(value: unknown, key: string) { - const field = getRecordValue(value, key); - return typeof field === "number" ? field : null; -} diff --git a/src/routes/privacy.tsx b/src/routes/privacy.tsx index e7e4ef5e3..d6bcd50ca 100644 --- a/src/routes/privacy.tsx +++ b/src/routes/privacy.tsx @@ -35,7 +35,7 @@ const privacyDocument = { }, { title: "Service providers", - body: "We use service providers to operate ThinkEx. Depending on the feature and environment, these may include Cloudflare for hosting, storage, databases, Durable Objects, Workers AI, Browser Rendering, email delivery, and sandboxed compute; Google for sign-in; Vercel AI Gateway and AI model providers such as OpenAI, Anthropic, and Google for AI responses; LlamaParse, Firecrawl, and Cloudflare Workers AI for file, web, and research extraction; PostHog for analytics, feedback, error reporting, session replay in deployed builds, and AI observability; and Autumn for AI usage tracking. These providers process data only as needed to provide their services to ThinkEx Inc.", + body: "We use service providers to operate ThinkEx. Depending on the feature and environment, these may include Cloudflare for hosting, storage, databases, Durable Objects, Workers AI, Browser Rendering, email delivery, and sandboxed compute; Google for sign-in; Vercel AI Gateway and AI model providers such as OpenAI, Anthropic, and Google for AI responses; FileRouter and parsing providers such as LlamaParse for document extraction; Firecrawl for web and research extraction; PostHog for analytics, feedback, error reporting, session replay in deployed builds, and AI observability; and Autumn for AI usage tracking. These providers process data only as needed to provide their services to ThinkEx Inc.", }, { title: "Workspace content and AI", diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 5ff33da39..3e12576be 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 5348c83093b741e804d43d2ed057d3fa) +// Generated by Wrangler by running `wrangler types` (hash: 54afe551d3e992cd59e1c907fdde1fb0) // Runtime types generated with workerd@1.20260722.1 2026-03-24 nodejs_compat interface __BaseEnv_Env { WORKSPACE_KERNEL_FILES: R2Bucket; @@ -18,7 +18,7 @@ interface __BaseEnv_Env { GOOGLE_CLIENT_ID: string; GOOGLE_CLIENT_SECRET: string; FIRECRAWL_API_KEY: string; - LLAMA_CLOUD_API_KEY: string; + FILEROUTER_API_KEY: string; AI_GATEWAY_API_KEY: string; AUTUMN_SECRET_KEY: string; TCC_API_KEY: string; @@ -56,7 +56,7 @@ declare namespace Cloudflare { GOOGLE_CLIENT_ID: string; GOOGLE_CLIENT_SECRET: string; FIRECRAWL_API_KEY: string; - LLAMA_CLOUD_API_KEY: string; + FILEROUTER_API_KEY: string; AI_GATEWAY_API_KEY: string; AUTUMN_SECRET_KEY: string; TCC_API_KEY: string; @@ -89,7 +89,7 @@ declare namespace Cloudflare { GOOGLE_CLIENT_ID: string; GOOGLE_CLIENT_SECRET: string; FIRECRAWL_API_KEY: string; - LLAMA_CLOUD_API_KEY: string; + FILEROUTER_API_KEY: string; AI_GATEWAY_API_KEY: string; AUTUMN_SECRET_KEY: string; TCC_API_KEY: string; @@ -112,7 +112,7 @@ type StringifyValues> = { [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; }; declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} + interface ProcessEnv extends StringifyValues> {} } // Begin runtime types diff --git a/wrangler.jsonc b/wrangler.jsonc index 1eed3b57b..4e515e77e 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -71,7 +71,7 @@ "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "FIRECRAWL_API_KEY", - "LLAMA_CLOUD_API_KEY", + "FILEROUTER_API_KEY", "AI_GATEWAY_API_KEY", "AUTUMN_SECRET_KEY", "TCC_API_KEY", @@ -102,7 +102,7 @@ }, { "class_name": "WorkspaceFileProcessor", - "image": "./containers/liteparse/Dockerfile", + "image": "./containers/workspace-file-processor/Dockerfile", "instance_type": "standard-2", "max_instances": 2, }, @@ -212,7 +212,7 @@ "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "FIRECRAWL_API_KEY", - "LLAMA_CLOUD_API_KEY", + "FILEROUTER_API_KEY", "AI_GATEWAY_API_KEY", "AUTUMN_SECRET_KEY", "TCC_API_KEY", @@ -271,7 +271,7 @@ }, { "class_name": "WorkspaceFileProcessor", - "image": "./containers/liteparse/Dockerfile", + "image": "./containers/workspace-file-processor/Dockerfile", "instance_type": "standard-2", "max_instances": 2, }, @@ -336,7 +336,7 @@ "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "FIRECRAWL_API_KEY", - "LLAMA_CLOUD_API_KEY", + "FILEROUTER_API_KEY", "AI_GATEWAY_API_KEY", "AUTUMN_SECRET_KEY", "TCC_API_KEY", @@ -395,7 +395,7 @@ }, { "class_name": "WorkspaceFileProcessor", - "image": "./containers/liteparse/Dockerfile", + "image": "./containers/workspace-file-processor/Dockerfile", "instance_type": "standard-2", "max_instances": 2, "name": "thinkex-liteparsepdfextractor-production",