From 8ccaa824dd0b04ad2953f32ac01bf77335bf6f16 Mon Sep 17 00:00:00 2001 From: Zoe <62722391+juls0730@users.noreply.github.com> Date: Sat, 6 Jun 2026 00:16:39 -0500 Subject: [PATCH] feat: file upload retry, secure file tokens, UI polish - Add HMAC-based file token auth for secure AI model file access - Add file upload retry with exponential backoff (max 3 retries) - File endpoint now requires session auth or signed token - Support assistant role messages in chat input - Optimistic UI for attachments on message send - Verify topic ownership before allowing messages - Switch web scraping to Firecrawl API - Agent profile page layout fixes (proper flex overflow) - Add quick switcher (Ctrl+K) to sidenav - Clean up longcat.ts and stale comments --- app/components/Attachment/Display.vue | 19 +- app/components/Attachment/Preview.vue | 10 +- app/components/ChatInput.vue | 102 ++++++++++- app/components/FileSelector.vue | 165 ++++++++++++------ app/components/Sidenav/NavAgent.vue | 13 ++ app/composables/useChat.ts | 114 +++++++++--- app/pages/agent/[id]/index.vue | 43 ++++- app/pages/agent/[id]/profile.vue | 48 ++--- app/pages/agent/[id]/topic/[topicId].vue | 25 ++- bun.lock | 79 +++------ package.json | 8 +- server/api/files/[...key].get.ts | 34 +++- .../topic/[topicId]/auto-rename/index.post.ts | 17 +- server/api/topic/[topicId]/chat/index.post.ts | 70 ++++++-- server/api/topic/[topicId]/message.post.ts | 161 ++++++++++++----- server/api/upload/presigned.post.ts | 17 +- server/utils/file-token.ts | 37 ++++ server/utils/longcat.ts | 116 ------------ server/utils/providers/closedrouter.ts | 6 +- utils/message.ts | 149 ++++++++++------ 20 files changed, 827 insertions(+), 406 deletions(-) create mode 100644 server/utils/file-token.ts delete mode 100644 server/utils/longcat.ts diff --git a/app/components/Attachment/Display.vue b/app/components/Attachment/Display.vue index e464026..6ca2938 100644 --- a/app/components/Attachment/Display.vue +++ b/app/components/Attachment/Display.vue @@ -10,9 +10,14 @@ const props = defineProps<{ status?: 'uploading' | 'uploaded' | 'error'; url: string; progress?: number; + retryCount?: number; }; }>(); +const emit = defineEmits<{ + retry: []; +}>(); + const isImage = computed(() => props.file.mimeType.startsWith('image/')); const isVideo = computed(() => props.file.mimeType.startsWith('video/')); const isAudio = computed(() => props.file.mimeType.startsWith('audio/')); @@ -71,8 +76,20 @@ function openImageViewer() { class="absolute inset-0 bg-black/50 rounded-lg flex items-center justify-center">
- {{ file.progress || 0 }}% + + {{ file.retryCount ? `Retry ${file.retryCount}/3` : `${file.progress || 0}%` }} +
+
+ + Upload failed + +
diff --git a/app/components/Attachment/Preview.vue b/app/components/Attachment/Preview.vue index b2902fb..aa01ba6 100644 --- a/app/components/Attachment/Preview.vue +++ b/app/components/Attachment/Preview.vue @@ -8,11 +8,13 @@ const props = defineProps<{ mimeType: string; status: 'uploading' | 'uploaded' | 'error'; url: string; + retryCount?: number; } }>(); const emit = defineEmits<{ delete: []; + retry: []; }>(); const handleDelete = async () => { @@ -21,12 +23,16 @@ const handleDelete = async () => { \ No newline at end of file diff --git a/app/components/ChatInput.vue b/app/components/ChatInput.vue index cad4ce2..6d466b9 100644 --- a/app/components/ChatInput.vue +++ b/app/components/ChatInput.vue @@ -1,6 +1,6 @@ @@ -108,7 +146,8 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
+ :allow-manual-role="true" :agent="agent" :providers="providers?.filter(p => p.enabled)" + @submit="handleSubmit" @add-message="handleAddMessage" />
diff --git a/app/pages/agent/[id]/profile.vue b/app/pages/agent/[id]/profile.vue index ab2c48a..7aa6e54 100644 --- a/app/pages/agent/[id]/profile.vue +++ b/app/pages/agent/[id]/profile.vue @@ -55,30 +55,32 @@ const changeSystemPrompt = async (e: Event) => { \ No newline at end of file + diff --git a/app/pages/agent/[id]/topic/[topicId].vue b/app/pages/agent/[id]/topic/[topicId].vue index 26f1556..e47b0de 100644 --- a/app/pages/agent/[id]/topic/[topicId].vue +++ b/app/pages/agent/[id]/topic/[topicId].vue @@ -54,6 +54,27 @@ const submitMessage = async (message: BaseMessage, model: ModelWithProvider | nu startGeneration(model); }; +const handleAddMessage = async (message: BaseMessage, role: 'user' | 'assistant', _model: ModelWithProvider | null) => { + inputValue.value = { content: '', fileIds: [] }; + + const res = await sendMessage(message, async () => { + await nextTick(); + setTimeout(() => { + scrollToBottom('instant'); + }); + }, role); + if (!res.ok) { + console.error('Failed to add message:', res.error); + const chatInput = document.getElementById('chat') as HTMLInputElement | null; + if (chatInput) { + inputValue.value = message; + nextTick(() => { + chatInput.focus(); + }); + } + } +}; + const handleRegenerate = async (message: Message) => { if (!agent.value!.defaultModelId) { console.error('No model selected'); @@ -292,8 +313,8 @@ console.log("full page render took", Date.now() - rootStart);
diff --git a/bun.lock b/bun.lock index 1035329..d345fb9 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "veridian", @@ -14,17 +15,17 @@ "@ai-sdk/openai-compatible": "^2.0.41", "@aws-sdk/client-s3": "^3.1028.0", "@aws-sdk/s3-request-presigner": "^3.1028.0", - "@better-auth/drizzle-adapter": "https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@6913", + "@better-auth/drizzle-adapter": "https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@9489", "@floating-ui/vue": "^1.1.11", "@iconify-json/mynaui": "^1.2.17", "@nuxt/fonts": "0.14.0", "@nuxt/hints": "1.0.0-alpha.5", "@nuxt/icon": "2.2.0", - "@openrouter/ai-sdk-provider": "^2.5.1", + "@openrouter/ai-sdk-provider": "^2.9.0", "@pydantic/monty": "^0.0.17", "@sentry/nuxt": "^10.48.0", "@tanstack/vue-virtual": "^3.13.23", - "ai": "^6.0.156", + "ai": "^6.0.182", "ai-sdk-ollama": "^3.8.3", "better-auth": "^1.6.2", "comlink": "^4.4.2", @@ -76,7 +77,7 @@ "@ai-sdk/cohere": ["@ai-sdk/cohere@3.0.30", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-j3fe/6lUUkHPD/51OgMXN9UD7p1QSQEAlroIinmb3MhJ1s+O0MnqdRa30IM7dRHafNp0FQ9X4YpobY85iMknUQ=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.114", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MqkZ5sd+qiq6RgIxELkoFQXg2/JwK+WCMaot7U+rtrZpWJl3fSyYvc28SC03b256o4F7OXjQtdjTqs81B2w+dA=="], "@ai-sdk/google": ["@ai-sdk/google@3.0.61", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jEKU1Mjcy5CoicejdJQIzM0ntYwyXR8vtYgAZYriKaOuLAiAhiiU538++fGU3CC9HJH/mL1OfsCwMM3gFiCNsw=="], @@ -264,9 +265,9 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], - "@better-auth/core": ["@better-auth/core@1.4.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg=="], + "@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="], - "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@6913", { "peerDependencies": { "@better-auth/core": "1.5.0-beta.13", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0", "prettier": "^3.7.4" } }], + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@9489", { "peerDependencies": { "@better-auth/core": "^1.7.0-beta.3", "@better-auth/utils": "0.4.0", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0" }, "optionalPeers": ["drizzle-orm"] }], "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "kysely": "^0.27.0 || ^0.28.0" }, "optionalPeers": ["kysely"] }, "sha512-YMMm75jek/MNCAFWTAaq/U3VPmFnrwZW4NhBjjAwruHQJEIrSZZaOaUEXuUpFRRBhWqg7OOltQcHMwU/45CkuA=="], @@ -278,7 +279,7 @@ "@better-auth/telemetry": ["@better-auth/telemetry@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-o4gHKXqizUxVUUYChZZTowLEzdsz3ViBE/fKFzfHqNFUnF+aVt8QsbLSfipq1WpTIXyJVT/SnH0hgSdWxdssbQ=="], - "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], + "@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], @@ -456,9 +457,9 @@ "@nuxt/vite-builder": ["@nuxt/vite-builder@4.4.2", "", { "dependencies": { "@nuxt/kit": "4.4.2", "@rollup/plugin-replace": "^6.0.3", "@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue-jsx": "^5.1.4", "autoprefixer": "^10.4.27", "consola": "^3.4.2", "cssnano": "^7.1.3", "defu": "^6.1.4", "escape-string-regexp": "^5.0.0", "exsolve": "^1.0.8", "get-port-please": "^3.2.0", "jiti": "^2.6.1", "knitwork": "^1.3.0", "magic-string": "^0.30.21", "mlly": "^1.8.1", "mocked-exports": "^0.1.1", "nypm": "^0.6.5", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "postcss": "^8.5.8", "seroval": "^1.5.1", "std-env": "^4.0.0", "ufo": "^1.6.3", "unenv": "^2.0.0-rc.24", "vite": "^7.3.1", "vite-node": "^5.3.0", "vite-plugin-checker": "^0.12.0", "vue-bundle-renderer": "^2.2.0" }, "peerDependencies": { "@babel/plugin-proposal-decorators": "^7.25.0", "@babel/plugin-syntax-jsx": "^7.25.0", "nuxt": "4.4.2", "rolldown": "^1.0.0-beta.38", "rollup-plugin-visualizer": "^6.0.0 || ^7.0.1", "vue": "^3.3.4" }, "optionalPeers": ["@babel/plugin-proposal-decorators", "@babel/plugin-syntax-jsx", "rolldown", "rollup-plugin-visualizer"] }, "sha512-fJaIwMA8ID6BU5EqmoDvnhq4qYDJeWjdHk4jfqy8D3Nm7CoUW0BvX7Ee92XoO05rtUiClGlk/NQ1Ii8hs3ZIbw=="], - "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="], + "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.9.0", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Seva+NCa0WUQnJIUE5GzHsUv1WTIeyqwz0ELl2VtS6NP+eF+77yCXGFVOMbvoCM7QMjlnhv7931e89R+8pJdcQ=="], - "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="], @@ -1076,7 +1077,7 @@ "@vercel/nft": ["@vercel/nft@0.27.10", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0-rc.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-zbaF9Wp/NsZtKLE4uVmL3FyfFwlpDyuymQM1kPbeT0mVOHKDQQNjnnfslB3REg3oZprmNFJuh3pkHBk2qAaizg=="], - "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], + "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], "@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="], @@ -1170,7 +1171,7 @@ "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], - "ai": ["ai@6.0.156", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uyi/5LYbugHQxZsR2PeAFOZEL4WqKkzZw4pv0nQvvdgxgVOsM7snOmGrYkp5fShxH/vnd08SXvHCVTX7oUW7xQ=="], + "ai": ["ai@6.0.182", "", { "dependencies": { "@ai-sdk/gateway": "3.0.114", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ooJdziFjYrYRcsCx107roqA8gDTI3P82nUfroNWIhVvwrkYzEN3W1l50YK+XNqkUew8AiimaW0/SLBewRXMuHQ=="], "ai-sdk-ollama": ["ai-sdk-ollama@3.8.3", "", { "dependencies": { "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.23", "jsonrepair": "^3.13.3", "ollama": "^0.6.3" }, "peerDependencies": { "ai": "^6.0.154" } }, "sha512-KId/S++eb0CgTPFTtHzCGCrO73kXZLK+hyyZx5k8LVqU2XOEHYKVbIwDiQ+hm3okHjnsGehn4zR4QNm14SUM3Q=="], @@ -2140,8 +2141,6 @@ "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], - "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], - "pretty-bytes": ["pretty-bytes@7.1.0", "", {}, "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw=="], "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], @@ -2566,6 +2565,10 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@ai-sdk/gateway/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.973.4", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q=="], "@aws-crypto/crc32c/@aws-sdk/types": ["@aws-sdk/types@3.973.4", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q=="], @@ -2602,28 +2605,6 @@ "@babel/traverse/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], - "@better-auth/core/better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="], - - "@better-auth/kysely-adapter/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="], - - "@better-auth/kysely-adapter/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], - - "@better-auth/memory-adapter/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="], - - "@better-auth/memory-adapter/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], - - "@better-auth/mongo-adapter/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="], - - "@better-auth/mongo-adapter/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], - - "@better-auth/prisma-adapter/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="], - - "@better-auth/prisma-adapter/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], - - "@better-auth/telemetry/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="], - - "@better-auth/telemetry/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], - "@dxup/nuxt/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "@fastify/otel/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.212.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.212.0", "import-in-the-middle": "^2.0.6", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg=="], @@ -2680,8 +2661,6 @@ "@nuxt/vite-builder/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], - "@opentelemetry/api-logs/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], - "@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="], "@opentelemetry/sql-common/@opentelemetry/core": ["@opentelemetry/core@2.5.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA=="], @@ -2712,10 +2691,6 @@ "@sentry/cli/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "@sentry/cloudflare/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], - - "@sentry/node/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], - "@sentry/nuxt/@nuxt/kit": ["@nuxt/kit@3.21.1", "", { "dependencies": { "c12": "^3.3.3", "consola": "^3.4.2", "defu": "^6.1.4", "destr": "^2.0.5", "errx": "^0.1.0", "exsolve": "^1.0.8", "ignore": "^7.0.5", "jiti": "^2.6.1", "klona": "^2.0.6", "knitwork": "^1.3.0", "mlly": "^1.8.0", "ohash": "^2.0.11", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "rc9": "^3.0.0", "scule": "^1.3.0", "semver": "^7.7.4", "tinyglobby": "^0.2.15", "ufo": "^1.6.3", "unctx": "^2.5.0", "untyped": "^2.0.0" } }, "sha512-QORZRjcuTKgo++XP1Pc2c2gqwRydkaExrIRfRI9vFsPA3AzuHVn5Gfmbv1ic8y34e78mr5DMBvJlelUaeOuajg=="], "@types/connect/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], @@ -2786,6 +2761,10 @@ "@vue/server-renderer/@vue/shared": ["@vue/shared@3.6.0-beta.10", "", {}, "sha512-13JUfIAd06F+IBnObE8mExDAMOknPIBjPBUN2JeemmuQwj5i20GduCLbHLVbxSkpFD0RGH4z2mOxUUdD+8M/Aw=="], + "ai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "archiver-utils/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], @@ -2798,14 +2777,8 @@ "ast-walker-scope/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], - "better-auth/@better-auth/core": ["@better-auth/core@1.6.2", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-nBftDp+eN1fwXor1O4KQorCXa0tJNDgpab7O1z4NcWUU+3faDpdzqLn5mbXZer2E8ZD4VhjqOfYZ041xnBF5NA=="], - "better-auth/@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.2", "", { "peerDependencies": { "@better-auth/core": "^1.6.2", "@better-auth/utils": "0.4.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-KawrNNuhgmpcc5PgLs6HesMckxCscz5J+BQ99iRmU1cLzG/A87IcydrmYtep+K8WHPN0HmZ/i4z/nOBCtxE2qA=="], - "better-auth/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], - - "better-call/@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], - "better-call/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001770", "", {}, "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw=="], @@ -2992,6 +2965,8 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@ai-sdk/gateway/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], + "@aws-crypto/crc32/@aws-sdk/types/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], "@aws-crypto/crc32c/@aws-sdk/types/@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="], @@ -3012,10 +2987,6 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "@better-auth/core/better-call/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], - - "@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], - "@dxup/nuxt/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "@fastify/otel/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.212.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg=="], @@ -3168,6 +3139,8 @@ "@vue/devtools-api/@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + "ai/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], + "archiver-utils/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "archiver-utils/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], @@ -3496,8 +3469,6 @@ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "@fastify/otel/@opentelemetry/instrumentation/@opentelemetry/api-logs/@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], - "@nuxt/kit/mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], "@nuxt/kit/mlly/pkg-types/mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], @@ -3522,6 +3493,8 @@ "@opentelemetry/instrumentation-pg/@types/pg/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs/@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@types/pg-pool/@types/pg/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "@unocss/transformer-attributify-jsx/oxc-parser/@oxc-parser/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="], diff --git a/package.json b/package.json index d669c72..f0886ee 100644 --- a/package.json +++ b/package.json @@ -20,17 +20,17 @@ "@ai-sdk/openai-compatible": "^2.0.41", "@aws-sdk/client-s3": "^3.1028.0", "@aws-sdk/s3-request-presigner": "^3.1028.0", - "@better-auth/drizzle-adapter": "https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@6913", + "@better-auth/drizzle-adapter": "https://pkg.pr.new/better-auth/better-auth/@better-auth/drizzle-adapter@9489", "@floating-ui/vue": "^1.1.11", "@iconify-json/mynaui": "^1.2.17", "@nuxt/fonts": "0.14.0", "@nuxt/hints": "1.0.0-alpha.5", "@nuxt/icon": "2.2.0", - "@openrouter/ai-sdk-provider": "^2.5.1", + "@openrouter/ai-sdk-provider": "^2.9.0", "@pydantic/monty": "^0.0.17", "@sentry/nuxt": "^10.48.0", "@tanstack/vue-virtual": "^3.13.23", - "ai": "^6.0.156", + "ai": "^6.0.182", "ai-sdk-ollama": "^3.8.3", "better-auth": "^1.6.2", "comlink": "^4.4.2", @@ -75,4 +75,4 @@ "@vercel/nft": "^0.27.4", "vite": "8.0.0-beta.15" } -} \ No newline at end of file +} diff --git a/server/api/files/[...key].get.ts b/server/api/files/[...key].get.ts index 2571d3a..6b1a972 100644 --- a/server/api/files/[...key].get.ts +++ b/server/api/files/[...key].get.ts @@ -1,4 +1,6 @@ import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; +import { auth } from "~~/lib/auth"; +import { verifyFileToken } from "~~/server/utils/file-token"; export default defineEventHandler(async (event) => { const key = getRouterParam(event, 'key'); @@ -6,6 +8,34 @@ export default defineEventHandler(async (event) => { throw createError({ statusCode: 400, statusMessage: 'Missing file key' }); } + const query = getQuery(event); + const exp = query.exp ? Number(query.exp) : undefined; + const sig = query.sig as string | undefined; + + let authorized = false; + + // Path 1: HMAC token (for AI model access) + if (exp && sig && process.env.BETTER_AUTH_SECRET) { + authorized = verifyFileToken(key, exp, sig, process.env.BETTER_AUTH_SECRET); + } + + // Path 2: Session auth (for client-side access) + if (!authorized) { + try { + const sessionData = await auth.api.getSession(event); + if (sessionData) { + event.context.user = sessionData.user; + authorized = true; + } + } catch { + // No valid session + } + } + + if (!authorized) { + throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }); + } + const config = useRuntimeConfig(); const s3 = new S3Client({ @@ -24,14 +54,12 @@ export default defineEventHandler(async (event) => { Key: key, })); - // 3. Set the correct headers so the browser knows what it's receiving setHeaders(event, { 'Content-Type': response.ContentType || 'application/octet-stream', 'Content-Length': response.ContentLength?.toString() || '', - 'Cache-Control': 'public, max-age=3600', // Optional: cache for 1 hour + 'Cache-Control': 'public, max-age=3600', }); - // 4. Return the body as a stream directly to the client return response.Body; } catch (error: any) { if (error.name === 'NoSuchKey') { diff --git a/server/api/topic/[topicId]/auto-rename/index.post.ts b/server/api/topic/[topicId]/auto-rename/index.post.ts index 632c013..d3f7e7c 100644 --- a/server/api/topic/[topicId]/auto-rename/index.post.ts +++ b/server/api/topic/[topicId]/auto-rename/index.post.ts @@ -44,6 +44,11 @@ export default defineEventHandler(async (event) => { }, with: { parts: true, + attachments: { + with: { + file: true, + } + }, }, }); @@ -160,8 +165,18 @@ export default defineEventHandler(async (event) => { }); } + let prompt = firstMessage.content!; + + if (firstMessage.attachments.length > 0) { + for (const attachment of firstMessage.attachments) { + if (attachment.file.mimeType.startsWith('image/')) { + prompt += `\n![${attachment.file.name}](${attachment.file.url})`; + } + } + } + const abortController = addPendingRename(topicId); - event.waitUntil(autoRename(topicId, abortController, { gateway: gateway.gateway, model }, gateway.textTransformer, firstMessage.content!, userId)); + event.waitUntil(autoRename(topicId, abortController, { gateway: gateway.gateway, model }, gateway.textTransformer, prompt, userId)); return { ok: true, diff --git a/server/api/topic/[topicId]/chat/index.post.ts b/server/api/topic/[topicId]/chat/index.post.ts index 44c0574..c26a042 100644 --- a/server/api/topic/[topicId]/chat/index.post.ts +++ b/server/api/topic/[topicId]/chat/index.post.ts @@ -3,7 +3,7 @@ import * as z from 'zod'; import { type MessageEntity } from '~/composables/useChat'; import { promises as fs } from 'fs'; import { glob } from 'glob'; -import { isLoopFinished, type ModelMessage, rerank, type RerankingModel, streamText, type StreamTextTransform, Tool, tool } from "ai"; +import { isLoopFinished, type ModelMessage, rerank, type RerankingModel, streamText, type StreamTextTransform, type Tool, tool } from "ai"; import { generations, messageParts, messages, toolCalls, ToolCallType } from "~~/drizzle/schema"; import { topicEvents } from "~~/server/utils/events"; import { nanoid } from "nanoid"; @@ -13,6 +13,7 @@ import { eq } from "drizzle-orm"; import { buildFocusedMessageTree, buildMessageTree, marshallMessages } from "~~/utils/message"; import path from "path"; import { isRerankingProvider } from "~~/server/utils/ai-provider"; +import { generateFileToken } from "~~/server/utils/file-token"; export default defineEventHandler(async (event) => { await protectRoute(event); @@ -237,7 +238,7 @@ export default defineEventHandler(async (event) => { } agentmessage.generationId = generation.id; - // @ts-ignore + // @ts-ignore - doesnt exist on the type but yeah it does now agentmessage.generation = generation; return agentmessage; @@ -311,7 +312,13 @@ export default defineEventHandler(async (event) => { } } - const topicMessages = marshallMessages(topic.agent, buildFocusedMessageTree(topicMessageTree)); + const fileTokenSecret = process.env.BETTER_AUTH_SECRET!; + const topicMessages = marshallMessages(topic.agent, buildFocusedMessageTree(topicMessageTree), { + signFileUrl: (fileKey) => { + const { exp, sig } = generateFileToken(fileKey, fileTokenSecret); + return `exp=${exp}&sig=${sig}`; + }, + }); if (topicMessages.ok === false) { throw createError({ statusCode: 500, @@ -353,7 +360,7 @@ const formatPartId = (partType: string, existingId: string) => { }; const formatToolCallId = (nativeId: string) => { - return `veridian__tool-${nativeId}-${nanoid()}`; + return `veridian__tool-${nativeId.slice(0, 16)}-${nanoid()}`; }; const evalPython = async (code: string) => { @@ -528,10 +535,20 @@ const { listDirectoryTool, globTool, readFileTool, readFilesTool, fetchUrlTool, content: z.string(), }), execute: async ({ url }) => { - const response = await fetch(url); - const content = await response.text(); + const response = await fetch('https://api.firecrawl.dev/v2/scrape', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + process.env.FIRECRAWL_API_KEY, + }, + body: JSON.stringify({ + url, + }), + }); + const content = await response.json(); + console.log(content); return { - content, + content: content.data.markdown, }; }, }), @@ -629,7 +646,7 @@ async function generateResponse( glob: globTool, readFile: readFileTool, readFiles: readFilesTool, - // fetchUrl: fetchUrlTool, + fetchUrl: fetchUrlTool, python: pythonTool, bash: bashTool, }; @@ -657,13 +674,14 @@ async function generateResponse( const response = streamText({ model: model.gateway(model.model.externalId), messages, + allowSystemInMessages: true, providerOptions: { openrouter: { debug: { echo_upstream_body: true, }, user: userId, - } + }, }, experimental_transform: streamTransoforms, stopWhen: isLoopFinished(), @@ -959,7 +977,7 @@ async function generateResponse( throw new Error('Failed to insert message part'); } - // @ts-ignore + // @ts-ignore - doesnt exist on the type but yeah it does now part.toolCall = toolCall; await topicEvents.emit(topicId, { @@ -1005,6 +1023,11 @@ async function generateResponse( }, }) .where(eq(toolCalls.id, dbToolCallId)); + if (token.providerMetadata) await db.update(messageParts) + .set({ + providerOptions: token.providerMetadata, + }) + .where(eq(messageParts.toolCallId, dbToolCallId)); await topicEvents.emit(topicId, { type: 'tool-call-delta', @@ -1049,6 +1072,7 @@ async function generateResponse( topicId, messageId: message.id, toolCallId: dbToolCallId, + providerOptions: token.providerMetadata, type: 'tool-call', content: null, finished: false, @@ -1060,7 +1084,7 @@ async function generateResponse( throw new Error('Failed to insert message part'); } - // @ts-ignore + // @ts-ignore - doesnt exist on the type but yeah it does now part.toolCall = toolCall; await topicEvents.emit(topicId, { @@ -1097,6 +1121,11 @@ async function generateResponse( status: 'failed', error: { type: ToolCallType.Text, value: 'Tool returned invalid output' } }).where(eq(toolCalls.id, dbToolCallId)); + if (token.providerMetadata) await db.update(messageParts) + .set({ + providerOptions: token.providerMetadata, + }) + .where(eq(messageParts.toolCallId, dbToolCallId)); await topicEvents.emit(topicId, { type: 'tool-call-delta', @@ -1125,6 +1154,11 @@ async function generateResponse( value: outputValue, }, }).where(eq(toolCalls.id, dbToolCallId)); + if (token.providerMetadata) await db.update(messageParts) + .set({ + providerOptions: token.providerMetadata, + }) + .where(eq(messageParts.toolCallId, dbToolCallId)); await topicEvents.emit(topicId, { type: 'tool-call-delta', @@ -1176,6 +1210,11 @@ async function generateResponse( value: outputValue as string, } }).where(eq(toolCalls.id, existingDbToolCallId)); + if (token.providerMetadata) await db.update(messageParts) + .set({ + providerOptions: token.providerMetadata, + }) + .where(eq(messageParts.toolCallId, existingDbToolCallId)); await topicEvents.emit(topicId, { type: 'tool-call-delta', @@ -1220,6 +1259,7 @@ async function generateResponse( topicId, messageId: message.id, toolCallId: dbToolCallId, + providerOptions: token.providerMetadata, type: 'tool-call', content: null, finished: false, @@ -1250,12 +1290,8 @@ async function generateResponse( case 'finish': { let tps; - if (ttft !== undefined && token.totalUsage.outputTokens !== undefined) { - const tokenStreamStart = requestStart! + ttft; - // this is the *real* request duration, excluding the - // TTFT - const requestDuration = performance.now() - tokenStreamStart; - + if (requestStart !== undefined && token.totalUsage.outputTokens !== undefined) { + const requestDuration = performance.now() - requestStart; tps = token.totalUsage.outputTokens / (requestDuration / 1000); } diff --git a/server/api/topic/[topicId]/message.post.ts b/server/api/topic/[topicId]/message.post.ts index 052ec07..5454192 100644 --- a/server/api/topic/[topicId]/message.post.ts +++ b/server/api/topic/[topicId]/message.post.ts @@ -1,6 +1,6 @@ import { modelMessageSchema } from 'ai'; import * as z from 'zod'; -import { attachments, messages } from '~~/drizzle/schema'; +import { attachments, messageParts, messages } from '~~/drizzle/schema'; import { db } from '~~/server/lib/db'; export default defineEventHandler(async (event) => { @@ -9,6 +9,21 @@ export default defineEventHandler(async (event) => { const topicId = getRouterParam(event, 'topicId')!; const userId = event.context.user!.id; + const topic = await db.query.topics.findFirst({ + where: { + id: topicId, + userId, + }, + }); + + if (!topic) { + throw createError({ + statusCode: 404, + statusMessage: 'Not Found', + message: 'Topic not found', + }); + } + const result = await readValidatedBody(event, z.object({ message: z.intersection( z.object({ @@ -19,6 +34,7 @@ export default defineEventHandler(async (event) => { ), }).safeParse); if (!result.success) { + console.log(result.error); throw createError({ statusCode: 400, statusMessage: 'Bad Request', @@ -28,49 +44,110 @@ export default defineEventHandler(async (event) => { const { message } = result.data; - await db.insert(messages).values({ - // @ts-ignore - drizzle bug - id: message.id, - userId, - topicId, - parentMessageId: null, - generationId: null, - role: message.role, - content: message.content, - }); - - for (const fileId of message.fileIds || []) { - await db.insert(attachments).values({ - userId, - topicId, - messageId: message.id, - fileId, - }); - } - - const usermessage = await db.query.messages.findFirst({ - where: { - id: message.id, - }, - with: { - attachments: { - with: { - file: true, - } - } - }, - }); - - if (!usermessage) { + if (['user', 'assistant'].includes(message.role) === false) { throw createError({ - statusCode: 500, - statusMessage: 'Failed to insert message', - message: 'Failed to insert message', + statusCode: 400, + statusMessage: 'Bad Request', + message: 'Invalid role', }); } - topicEvents.emit(topicId, { type: 'MESSAGE_CREATED', payload: usermessage }); - return { - ok: true - }; + switch (message.role) { + case 'user': { + await db.insert(messages).values({ + // @ts-ignore - drizzle bug + id: message.id, + userId, + topicId, + parentMessageId: null, + generationId: null, + role: message.role, + content: message.content, + }); + + for (const fileId of message.fileIds || []) { + await db.insert(attachments).values({ + userId, + topicId, + messageId: message.id, + fileId, + }); + } + + const usermessage = await db.query.messages.findFirst({ + where: { + id: message.id, + }, + with: { + attachments: { + with: { + file: true, + } + } + }, + }); + + if (!usermessage) { + throw createError({ + statusCode: 500, + statusMessage: 'Failed to insert message', + message: 'Failed to insert message', + }); + } + topicEvents.emit(topicId, { type: 'MESSAGE_CREATED', payload: usermessage }); + + return { + ok: true + }; + } + case 'assistant': { + const [dbmessage] = await db.insert(messages).values({ + // @ts-ignore - drizzle bug + id: message.id, + userId, + topicId, + parentMessageId: null, + generationId: null, + role: message.role, + content: message.content, + }).returning(); + + if (!dbmessage) { + throw createError({ + statusCode: 500, + statusMessage: 'Failed to insert message', + message: 'Failed to insert message', + }); + } + + const [part] = await db.insert(messageParts).values({ + userId, + topicId, + messageId: dbmessage.id, + type: 'text', + content: message.content, + providerOptions: null, + finished: true, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }).returning(); + + // TODO: transaction + if (!part) { + throw createError({ + statusCode: 500, + statusMessage: 'Failed to insert message part', + message: 'Failed to insert message part', + }); + } + + topicEvents.emit(topicId, { type: 'MESSAGE_CREATED', payload: dbmessage }); + topicEvents.emit(topicId, { type: 'text-start', payload: { messageId: dbmessage.id, part } }); + topicEvents.emit(topicId, { type: 'text-end', payload: { messageId: dbmessage.id, partId: part.id, lastUpdatedAt: new Date(), content: message.content } }); + + return { + ok: true + }; + } + } }) \ No newline at end of file diff --git a/server/api/upload/presigned.post.ts b/server/api/upload/presigned.post.ts index 7b085f5..e5b1927 100644 --- a/server/api/upload/presigned.post.ts +++ b/server/api/upload/presigned.post.ts @@ -39,7 +39,22 @@ export default defineEventHandler(async (event) => { responseChecksumValidation: 'WHEN_REQUIRED', }); - const key = `veridian__uploads/${Date.now()}-${file.name.replace(/[^a-zA-Z0-9.]/g, '_')}-${event.context.user!.id}` + const fileParts = file.name.split('.'); + if (fileParts.length < 2) { + throw createError({ + statusCode: 400, + statusMessage: 'Invalid file name', + data: { + code: 'INVALID_FILE_NAME', + ok: false, + } + }); + } + + const fileExt = fileParts.pop()!; + const fileName = fileParts.join('.').replace(/[^a-zA-Z0-9.]/g, '_'); + + const key = `veridian__uploads/${Date.now()}-${fileName}-${event.context.user!.id}.${fileExt}`; const command = new PutObjectCommand({ ACL: 'public-read', diff --git a/server/utils/file-token.ts b/server/utils/file-token.ts new file mode 100644 index 0000000..54e8612 --- /dev/null +++ b/server/utils/file-token.ts @@ -0,0 +1,37 @@ +import { createHmac, timingSafeEqual } from 'crypto'; + +const DEFAULT_EXPIRY_MS = 30 * 60 * 1000; // 30 minutes + +export function generateFileToken( + fileKey: string, + secret: string, + expiresInMs = DEFAULT_EXPIRY_MS, +): { exp: number; sig: string } { + const exp = Date.now() + expiresInMs; + const payload = `${fileKey}:${exp}`; + const sig = createHmac('sha256', secret).update(payload).digest('hex'); + return { exp, sig }; +} + +export function verifyFileToken( + fileKey: string, + exp: number, + sig: string, + secret: string, +): boolean { + if (Date.now() > exp) { + return false; + } + + const payload = `${fileKey}:${exp}`; + const expected = createHmac('sha256', secret).update(payload).digest('hex'); + + const sigBuffer = Buffer.from(sig, 'hex'); + const expectedBuffer = Buffer.from(expected, 'hex'); + + if (sigBuffer.length !== expectedBuffer.length) { + return false; + } + + return timingSafeEqual(sigBuffer, expectedBuffer); +} diff --git a/server/utils/longcat.ts b/server/utils/longcat.ts deleted file mode 100644 index 398a6d2..0000000 --- a/server/utils/longcat.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { type ToolSet, type TextStreamPart, type ToolCallPart } from 'ai'; - -export function createLongcatTransformer(): (options: { - tools: TOOLS; - stopStream: () => void; -}) => TransformStream, TextStreamPart> { - let buffer = ''; - let hasToolCallInStep = false; - let lastChunkId: string | undefined; - let lastChunkType: 'text' | 'reasoning' | undefined; - let step = 0; - - return (_opts) => { - return new TransformStream, TextStreamPart>({ - transform(chunk, controller) { - if (chunk.type === 'finish-step' || chunk.type === 'finish') { - step++; - - if (hasToolCallInStep) { - // We clone the chunk and overwrite the finishReason. - // This tricks the SDK into thinking the model requested a tool natively. - const modifiedChunk = { - ...chunk, - finishReason: 'tool-calls' as const, - }; - - // Reset for the next potential step - if (chunk.type === 'finish-step') { - hasToolCallInStep = false; - } - - controller.enqueue(modifiedChunk); - return; - } - } - - if (chunk.type === 'text-start' || chunk.type === 'reasoning-start') { - lastChunkId = chunk.id; - lastChunkType = chunk.type.split('-')[1] as 'text' | 'reasoning'; - } - - // We only care about text chunks - if (chunk.type !== 'text-delta' && chunk.type !== 'reasoning-delta') { - controller.enqueue(chunk); - return; - } - - buffer += chunk.text; - - // Check if we have a full tool call in the buffer - const pattern = /([\s\S]*?)<\/longcat_tool_call>/g; - let lastIndex = 0; - let match; - - while ((match = pattern.exec(buffer)) !== null) { - console.log("longcat tool call found at index", match.index); - - // 1. Enqueue any text that appeared BEFORE the tool call - const textBefore = buffer.substring(lastIndex, match.index); - if (textBefore) { - controller.enqueue({ type: chunk.type, text: textBefore, id: lastChunkId ?? chunk.type.includes('reasoning') ? `reasoning-${step}` : `text-${step}` }); - } - - // 2. Parse the XML content - const content = match[1]!.trim(); - const toolNameMatch = content.match(/^([^\s<]+)/); - - if (toolNameMatch) { - hasToolCallInStep = true; - - const toolName = toolNameMatch[1]; - const args: Record = {}; - const argRegex = /(.*?)<\/longcat_arg_key>\s*(.*?)<\/longcat_arg_value>/gs; - - let argMatch; - while ((argMatch = argRegex.exec(content)) !== null) { - args[argMatch[1]!.trim()] = argMatch[2]!.trim(); - } - - // 3. EMIT A TOOL CALL PART - // This is the "magic" - the SDK will see this and act as if the LLM - // called a native tool. - const toolCallId = `lc-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`; - controller.enqueue({ - type: 'tool-call', - // @ts-ignore - id: toolCallId, - toolCallId, - toolName, - input: args, - dynamic: true, - }); - } - - lastIndex = pattern.lastIndex; - } - - // Keep the remaining buffer (unclosed tags) for the next chunk - buffer = buffer.substring(lastIndex); - - // If there's no open tag starting, we can flush the buffer as text - if (!buffer.includes('')) { - if (buffer) { - controller.enqueue({ type: chunk.type, text: buffer, id: lastChunkId ?? chunk.type.includes('reasoning') ? `reasoning-${step}` : `text-${step}` }); - buffer = ''; - } - } - }, - flush(controller) { - if (buffer && lastChunkId && lastChunkType) { - controller.enqueue({ type: `${lastChunkType}-delta`, text: buffer, id: lastChunkId }); - } - } - }); - }; -} \ No newline at end of file diff --git a/server/utils/providers/closedrouter.ts b/server/utils/providers/closedrouter.ts index 469aead..dbad1d6 100644 --- a/server/utils/providers/closedrouter.ts +++ b/server/utils/providers/closedrouter.ts @@ -14,7 +14,11 @@ export default { } return Ok({ - gateway: createOpenAI({ name: 'ClosedRouter', apiKey, baseURL }), + gateway: createOpenAI({ + name: 'ClosedRouter', + apiKey, + baseURL + }), streamTransformer: undefined, textTransformer: undefined, }); diff --git a/utils/message.ts b/utils/message.ts index 6b5d77b..f4297db 100644 --- a/utils/message.ts +++ b/utils/message.ts @@ -1,4 +1,4 @@ -import type { FilePart, ImagePart, ModelMessage } from "ai"; +import type { AssistantContent, FilePart, ImagePart, JSONValue, ModelMessage, ToolContent } from "ai"; import { ToolCallType } from "~~/drizzle/schema"; import { Err, Ok, type Result } from "~~/types/result"; import type { Message, MessageEntity } from "~/composables/useChat"; @@ -37,7 +37,11 @@ export const buildFocusedMessageTree = (messages: Readonly): MessageE return focusedMessageTree; } -export const marshallMessages = (agent: Agent, messages: Readonly): Result => { +export interface MarshallOptions { + signFileUrl?: (fileKey: string) => string; +} + +export const marshallMessages = (agent: Agent, messages: Readonly, opts?: MarshallOptions): Result => { const marshalledMessages: ModelMessage[] = []; if (agent && agent.systemPrompt) { @@ -51,47 +55,72 @@ export const marshallMessages = (agent: Agent, messages: Readonly { - const url = resolveFileUrl(attachment.file.url); + let url = resolveFileUrl(attachment.file.url); + + if (opts?.signFileUrl && attachment.file.url.startsWith('/api/files/')) { + const fileKey = attachment.file.url.slice('/api/files/'.length); + const token = opts.signFileUrl(fileKey); + url = `${url}?${token}`; + } if (attachment.file.mimeType.startsWith('image/')) { return { type: 'image', - image: url, + image: new URL(url), }; } return { type: 'file', - data: url, + data: new URL(url), filename: attachment.file.name, mediaType: attachment.file.mimeType, }; }) as (FilePart | ImagePart)[]; let messageDate = new Date(message.createdAt); + const prompt = `[${messageDate.toDateString()} ${messageDate.toLocaleTimeString()}]: ${message.content!}`; + marshalledMessages.push({ role: 'user', - content: [ + content: attachments ? [ { type: 'text', text: `[${messageDate.toDateString()} ${messageDate.toLocaleTimeString()}]: ${message.content!}` }, ...attachments, - ], + ] : prompt, }); break; } case 'assistant': + let assistantPart: AssistantContent = []; + let toolParts: ToolContent = []; + for (const part of (message.parts || [])) { if (!part) return Err('Part is undefined'); switch (part.type) { case 'text': case 'reasoning': { - marshalledMessages.push({ - role: 'assistant', - content: part.content!, - }); + if (toolParts.length > 0) { + marshalledMessages.push({ + role: 'assistant', + content: assistantPart, + }); + marshalledMessages.push({ + role: 'tool', + content: toolParts, + }); + toolParts = []; + assistantPart = []; + } + + if (part.providerOptions || part.content) assistantPart.push({ + type: part.type, + text: part.content || '', + providerOptions: part.providerOptions ? part.providerOptions as Record : undefined, + }) break; } case 'tool-call': { @@ -101,33 +130,32 @@ export const marshallMessages = (agent: Agent, messages: Readonly : undefined, - }); + }) if (part.toolCall.status === 'failed') { let failureType: 'error-text' | 'error-json'; - let failureValue: string; + let failureValue: string | JSONValue; if (part.toolCall.error === null || part.toolCall.error === undefined) { failureType = 'error-text'; @@ -140,27 +168,32 @@ export const marshallMessages = (agent: Agent, messages: Readonly : undefined, }); break; @@ -177,23 +210,22 @@ export const marshallMessages = (agent: Agent, messages: Readonly : undefined, }); break; @@ -203,6 +235,15 @@ export const marshallMessages = (agent: Agent, messages: Readonly 0) marshalledMessages.push({ + role: 'assistant', + content: assistantPart, + }); + if (toolParts.length > 0) marshalledMessages.push({ + role: 'tool', + content: toolParts, + }); break; default: return Err(`Unknown message role: ${message.role}`);