diff --git a/.dev.vars.example b/.dev.vars.example index 47c05003..17a6b658 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 2ade9d7d..f58e7935 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 2db3e29f..00000000 --- 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 864deb88..00000000 --- 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 a2c123bc..6c27c236 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 d8de117f..c400e606 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 8544d73f..3bd2d161 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 8beed6c0..a6850988 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 85eeffe9..915614c2 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 88f59668..ee2f2ad0 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 5a1a56d9..37345763 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 f414d8d9..a048ceff 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 79b9334e..ba44bd7c 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 00000000..a1beb428 --- /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 00000000..710739b7 --- /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 4766e787..00000000 --- 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 e58d4de2..00000000 --- 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 452f0c29..00000000 --- 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 aa5e0f95..00000000 --- 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 7e4ee1e0..00000000 --- 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 88b1e91f..00000000 --- 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 0771a717..00000000 --- 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 ca52b556..00000000 --- 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 3aedc7cf..00000000 --- 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 5dbc9b86..d2df2385 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 00000000..91897f72 --- /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 1d3d1364..b24c7a48 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 00000000..f5fa3a4a --- /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 44622600..d5eeeaee 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 00000000..8bbdfb91 --- /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 00000000..fe19169b --- /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 00d90ed2..b522a4f3 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 aefa7e05..3d80775a 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 deb5b0a1..2aa90f17 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 67bb9e32..17785aff 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 f3bab7de..6238f2b8 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 ace36290..1a2d2e7c 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 b2f385ab..00000000 --- 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 e7e4ef5e..d6bcd50c 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 5ff33da3..3e12576b 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 1eed3b57..4e515e77 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",