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
This commit is contained in:
Zoe
2026-06-06 00:16:39 -05:00
parent 47009b1f0a
commit 8ccaa824dd
20 changed files with 827 additions and 406 deletions
+18 -1
View File
@@ -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">
<div class="text-center">
<span class="i-svg-spinners-90-ring-with-bg text-6 text-white block mb-1"></span>
<span class="text-xs text-white">{{ file.progress || 0 }}%</span>
<span class="text-xs text-white">
{{ file.retryCount ? `Retry ${file.retryCount}/3` : `${file.progress || 0}%` }}
</span>
</div>
</div>
<div v-else-if="file.status === 'error'"
class="absolute inset-0 bg-black/60 rounded-lg flex flex-col items-center justify-center gap-1">
<span class="i-mynaui-danger-triangle text-6 text-red-400"></span>
<span class="text-xs text-red-300">Upload failed</span>
<button @click.stop="emit('retry')"
class="mt-1 flex items-center gap-1 px-2 py-0.5 rounded-md bg-white/10 hover:bg-white/20 text-xs text-white transition-colors">
<span class="i-mynaui-refresh text-3"></span>
Retry
</button>
</div>
</div>
</template>
+8 -2
View File
@@ -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 () => {
</script>
<template>
<div class="relative h-full w-fit flex">
<Display :file="props.file" />
<div class="relative h-full w-fit flex flex-shrink-0">
<Display :file="props.file" @retry="emit('retry')" />
<button @click="handleDelete"
class="absolute top-0 right-0 translate-x-1/2 -translate-y-1/2 flex items-center justify-center w-4 h-4 rounded-full bg-[var(--bg-base)] border border-[var(--color-border)] text-xs text-[#ff3b3b]">
<span class="i-mynaui-x text-3"></span>
</button>
<div v-if="file.status === 'error'"
class="absolute bottom-0 right-0 translate-x-1/4 translate-y-1/4 flex items-center justify-center w-4 h-4 rounded-full bg-red-500">
<span class="i-mynaui-danger-triangle text-2.5 text-white"></span>
</div>
</div>
</template>
+94 -8
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { BaseMessage } from '~/composables/useChat';
import { onMounted, ref, watch, onUnmounted, nextTick, type Ref } from 'vue';
import { computed, onMounted, ref, watch, onUnmounted, nextTick, type Ref } from 'vue';
import type { ModelWithProvider, ProviderWithModels } from '~/composables/useModels';
import type { Agent } from '~/composables/useAgents';
import type FileSelector from './FileSelector.vue';
@@ -19,6 +19,7 @@ const files = ref<{
status: 'uploading' | 'uploaded' | 'error';
url: string;
progress: number;
retryCount: number;
}[]>([]);
const inputValue = defineModel<BaseMessage>({ required: false, default: { content: '', fileIds: [] } });
const textAreaValue = ref('');
@@ -31,12 +32,14 @@ watch(files, (newFiles) => {
const emit = defineEmits<{
submit: [value: BaseMessage, model: ModelWithProvider | null];
addMessage: [value: BaseMessage, role: 'user' | 'assistant', model: ModelWithProvider | null];
cancel: [];
resize: [];
}>();
const props = defineProps<{
loading?: boolean;
allowManualRole?: boolean;
agent: Readonly<Agent> | null;
providers?: ProviderWithModels[];
}>();
@@ -70,6 +73,9 @@ const saveSearchConfig = async () => {
updateAgent(props.agent.id, { config: newConfig });
};
const isUploading = computed(() => files.value.some((f) => f.status === 'uploading'));
const hasFailedUploads = computed(() => files.value.some((f) => f.status === 'error'));
const selectedModel = ref<ModelWithProvider | null>(null);
const handlePaste = async (event: ClipboardEvent) => {
@@ -109,11 +115,18 @@ const initializeModel = () => {
const updateAgentDefaultModel = async (modelId: string) => {
if (!props.agent) return;
try {
const currentModelId = props.agent.defaultModelId;
await $fetch(`/api/agent/${props.agent.id}`, {
method: 'PATCH',
body: {
defaultModelId: modelId,
},
onRequest: () => {
patchAgentLocally(props.agent!.id, { defaultModelId: modelId });
},
onResponseError: () => {
patchAgentLocally(props.agent!.id, { defaultModelId: currentModelId });
},
});
} catch (error) {
console.error('Failed to update agent default model:', error);
@@ -144,7 +157,35 @@ const handleSubmit = () => {
}
if (inputValue.value.content.trim() || inputValue.value.fileIds.length > 0) {
emit('submit', structuredClone(toRaw(inputValue.value)), selectedModel.value);
const message: BaseMessage = {
content: inputValue.value.content,
fileIds: inputValue.value.fileIds,
files: files.value.map(f => ({
id: f.id,
name: f.name,
mimeType: f.mimeType,
url: f.url,
})),
};
emit('submit', message, selectedModel.value);
files.value = [];
textAreaValue.value = '';
}
};
const handleAddMessage = (role: 'user' | 'assistant') => {
if (inputValue.value.content.trim() || inputValue.value.fileIds.length > 0) {
const message: BaseMessage = {
content: inputValue.value.content,
fileIds: inputValue.value.fileIds,
files: files.value.map(f => ({
id: f.id,
name: f.name,
mimeType: f.mimeType,
url: f.url,
})),
};
emit('addMessage', message, role, selectedModel.value);
files.value = [];
textAreaValue.value = '';
}
@@ -320,12 +361,13 @@ onUnmounted(() => {
class="absolute top-0 left-0 pointer-events-none invisible overflow-hidden h-0"></div>
<div class="relative w-full flex flex-shrink-1 flex-col gap-3 p-2 rounded-2xl border transition-border ease-in-out duration-300 bg-[var(--bg-container)]
border-[var(--color-border)] focus-within:border-[var(--color-border-active)]">
<div v-if="files.length > 0" class="flex-1 flex gap-2 pt-2 px-2 pb-1 overflow-x-auto flex-wrap">
<div v-if="files.length > 0"
class="flex-shrink-0 flex flex-nowrap gap-2 pt-2 px-2 pb-1 overflow-x-auto overflow-y-hidden [scrollbar-width:thin]">
<!-- TODO: show attachment previews -->
<AttachmentPreview v-for="file in files" @delete="files = files.filter((f) => f.id !== file.id)"
:key="file.id" :file="file" />
@retry="fileSelectorRef?.retryFile(file.id)" :key="file.id" :file="file" />
</div>
<div class="flex-1 min-w-0 max-h-full">
<div class="min-w-0 max-h-full">
<!-- Grammarly literally breaks everything, go fuck yourself -->
<!-- It is absolutely paramount that the closing tag for the textare has ZERO whitespace between the end of the textarea opening tag, otherwise there will be hydration errors -->
<textarea data-gramm="false" id="chat" v-model="textAreaValue" ref="inputRef"
@@ -346,10 +388,54 @@ onUnmounted(() => {
@update:rerank="(v: boolean) => { searchConfig.rerank = v; saveSearchConfig() }" />
<FileSelector ref="fileSelectorRef" :selected-model="selectedModel" v-model="files" />
</div>
<button aria-label="Send message" @click="handleSubmit"
:disabled="(!inputValue.content.trim() && files.length === 0) && !loading" :class="[
<div v-if="allowManualRole" class="flex items-center">
<button aria-label="Send message" @click="handleSubmit"
:disabled="((!inputValue.content.trim() && files.length === 0) && !loading) || isUploading || hasFailedUploads"
:class="[
'h-8 w-8 rounded-l-lg rounded-r-none transition-all duration-200 flex items-center justify-center disabled:cursor-not-allowed disabled:bg-transparent',
(inputValue.content.trim() || files.length > 0) && !loading && !isUploading && !hasFailedUploads
? 'bg-[var(--color-accent)] text-[var(--color-accent-text)] @hover:bg-[var(--color-accent-hover)]'
: 'text-[var(--text-dim)]',
loading && 'bg-[var(--color-hover)] @hover:bg-[var(--color-active)]',
]">
<span v-if="loading" class="text-6.5 i-mynaui-stop-solid"></span>
<span v-else class="text-5 i-mynaui-send-solid"></span>
</button>
<Dropdown placement="bottom-end">
<template #default="{ setRef, isOpen, toggle }">
<button :ref="setRef" aria-label="Message options" @click="toggle"
:disabled="(!inputValue.content.trim() && files.length === 0) || loading || isUploading || hasFailedUploads"
:class="[
'h-8 w-6 rounded-r-lg rounded-l-none transition-all duration-200 flex items-center justify-center disabled:cursor-not-allowed border-l border-[var(--text-dim)] disabled:border-transparent',
(inputValue.content.trim() || files.length > 0) && !loading && !isUploading && !hasFailedUploads
? 'bg-[var(--color-accent)] text-[var(--color-accent-text)] @hover:bg-[var(--color-accent-hover)] disabled:bg-transparent'
: 'text-[var(--text-dim)]',
loading && 'bg-[var(--color-hover)] @hover:bg-[var(--color-active)]',
]">
<span class="text-4.5 i-mynaui-chevron-down" :class="{ 'rotate-180': isOpen }"></span>
</button>
</template>
<template #dropdown="{ close }">
<button
class="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm text-[var(--text-primary)] @hover:bg-[var(--color-hover)] transition-colors whitespace-nowrap"
@click="handleAddMessage('user'); close()">
<span class="text-4 i-mynaui-user"></span>
Add as User
</button>
<button
class="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm text-[var(--text-primary)] @hover:bg-[var(--color-hover)] transition-colors whitespace-nowrap"
@click="handleAddMessage('assistant'); close()">
<span class="text-4 i-mynaui-sparkles"></span>
Add as Assistant
</button>
</template>
</Dropdown>
</div>
<button v-else aria-label="Send message" @click="handleSubmit"
:disabled="((!inputValue.content.trim() && files.length === 0) && !loading) || isUploading || hasFailedUploads"
:class="[
'h-8 w-8 rounded-xl transition-all duration-200 flex items-center justify-center disabled:cursor-not-allowed disabled:bg-transparent',
(inputValue.content.trim() || files.length > 0) && !loading
(inputValue.content.trim() || files.length > 0) && !loading && !isUploading && !hasFailedUploads
? 'bg-[var(--color-accent)] text-[var(--color-accent-text)] @hover:bg-[var(--color-accent-hover)]'
: 'text-[var(--text-dim)]',
loading && 'bg-[var(--color-hover)] @hover:bg-[var(--color-active)]',
+115 -50
View File
@@ -1,6 +1,9 @@
<script setup lang="ts">
import { nanoid } from 'nanoid';
const MAX_RETRIES = 3;
const RETRY_BASE_DELAY_MS = 1000;
const props = defineProps<{
selectedModel?: ModelWithProvider | null;
}>();
@@ -15,10 +18,12 @@ const files = defineModel<{
status: 'uploading' | 'uploaded' | 'error';
url: string;
progress: number;
retryCount: number;
}[]>({ required: false, default: [] });
const rawFiles = ref<File[]>([]);
const activeUploads = ref<Map<string, XMLHttpRequest>>(new Map());
const pendingFiles = ref<Map<string, File>>(new Map());
const uploadWithProgress = (file: File, url: string, id: string) => {
return new Promise((resolve, reject) => {
@@ -39,12 +44,16 @@ const uploadWithProgress = (file: File, url: string, id: string) => {
xhr.onload = () => {
activeUploads.value.delete(id);
resolve(xhr);
if (xhr.status >= 400) {
reject(new Error(`Upload failed with status ${xhr.status}`));
} else {
resolve(xhr);
}
};
xhr.onerror = () => {
activeUploads.value.delete(id);
reject(xhr);
reject(new Error('Network error during upload'));
};
xhr.onabort = () => {
@@ -57,68 +66,122 @@ const uploadWithProgress = (file: File, url: string, id: string) => {
});
};
const uploadFile = async (file: File) => {
try {
const id = nanoid();
const fileName = file.name || `${id}.png`;
const fileType = file.type || 'application/octet-stream';
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
files.value.push({
const uploadFileAttempt = async (file: File, id: string, fileName: string, fileType: string) => {
const { url: uploadUrl, assetUrl } = await $fetch('/api/upload/presigned', {
method: 'POST',
body: {
file: {
name: fileName,
mimeType: fileType,
}
},
}) as { url: string; assetUrl: string };
await uploadWithProgress(file, uploadUrl, id);
await $fetch('/api/file', {
method: 'POST',
body: {
id,
name: fileName,
mimeType: fileType,
status: 'uploading',
url: URL.createObjectURL(file),
progress: 0
});
size: file.size,
url: assetUrl,
},
});
};
const { url: uploadUrl, assetUrl } = await $fetch('/api/upload/presigned', {
method: 'POST',
body: {
file: {
name: fileName,
mimeType: fileType,
}
},
}) as { url: string; assetUrl: string };
const uploadFile = async (file: File) => {
const id = nanoid();
const fileName = file.name || `${id}.png`;
const fileType = file.type || 'application/octet-stream';
await uploadWithProgress(file, uploadUrl, id);
files.value.push({
id,
name: fileName,
mimeType: fileType,
status: 'uploading',
url: URL.createObjectURL(file),
progress: 0,
retryCount: 0,
});
pendingFiles.value.set(id, file);
await $fetch('/api/file', {
method: 'POST',
body: {
id,
name: fileName,
mimeType: fileType,
size: file.size,
url: assetUrl,
},
})
await attemptUpload(file, id, fileName, fileType);
};
const attemptUpload = async (file: File, id: string, fileName: string, fileType: string, attempt = 0) => {
try {
await uploadFileAttempt(file, id, fileName, fileType);
files.value = files.value.map(f => {
if (f.name === file.name) {
return {
...f,
status: 'uploaded',
progress: 100
};
if (f.id === id) {
return { ...f, status: 'uploaded' as const, progress: 100 };
}
return f;
});
pendingFiles.value.delete(id);
} catch (error) {
files.value = files.value.map(f => {
if (f.name === file.name) {
return {
...f,
status: 'error'
};
}
return f;
});
}
}
if (error instanceof Error && error.message === 'Upload aborted') {
pendingFiles.value.delete(id);
return;
}
defineExpose({ uploadFile });
const nextAttempt = attempt + 1;
if (nextAttempt < MAX_RETRIES) {
files.value = files.value.map(f => {
if (f.id === id) {
return { ...f, retryCount: nextAttempt, progress: 0 };
}
return f;
});
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt);
await sleep(delay);
const stillExists = files.value.some(f => f.id === id);
if (!stillExists) return;
files.value = files.value.map(f => {
if (f.id === id) {
return { ...f, status: 'uploading' as const };
}
return f;
});
await attemptUpload(file, id, fileName, fileType, nextAttempt);
} else {
files.value = files.value.map(f => {
if (f.id === id) {
return { ...f, status: 'error' as const };
}
return f;
});
}
}
};
const retryFile = async (id: string) => {
const file = pendingFiles.value.get(id);
if (!file) return;
const entry = files.value.find(f => f.id === id);
if (!entry) return;
files.value = files.value.map(f => {
if (f.id === id) {
return { ...f, status: 'uploading' as const, progress: 0, retryCount: 0 };
}
return f;
});
await attemptUpload(file, id, entry.name, entry.mimeType);
};
defineExpose({ uploadFile, retryFile });
watch(rawFiles, async (newFiles, oldFiles) => {
const diff = newFiles.filter(f =>
@@ -146,6 +209,8 @@ watch(files, async (newFiles, oldFiles) => {
activeUploads.value.delete(removedFile.id);
}
pendingFiles.value.delete(removedFile.id);
if (removedFile.url.startsWith('blob:')) {
URL.revokeObjectURL(removedFile.url);
}
+13
View File
@@ -1,7 +1,9 @@
<script setup lang="ts">
import RowVirtualizerFixed from '~/components/RowVirtualizerFixed.vue';
import { useFloating, offset, flip, shift, autoUpdate, hide } from '@floating-ui/vue';
import { DialogType } from '~/composables/useDialog';
const { openDialog } = useDialog();
const dropdownOpen = ref(false);
const dropdownTrigger = ref<HTMLElement | null>(null);
const dropdownContent = ref(null);
@@ -151,6 +153,17 @@ onMounted(() => {
<SidenavItem :to="`/agent/${agentId}/profile`" name="Agent Info" icon="i-mynaui-info-square"
:active="route.path.endsWith('/profile')" />
<SidenavItem @click="openDialog(DialogType.QuickSwitcher)" name="Search" icon="i-mynaui-search">
<div class="flex items-center gap-1">
<span class="flex bg-[var(--bg-container)] px-1 rounded border border-[var(--color-border)]">
<kbd class="font-mono text-[10px] case-capital">ctrl</kbd>
</span>
<span class="flex bg-[var(--bg-container)] px-1 rounded border border-[var(--color-border)]">
<kbd class="font-mono text-[10px] case-capital">k</kbd>
</span>
</div>
</SidenavItem>
<!-- Topics Section -->
<button @click="topicsOpen = !topicsOpen"
class="flex items-center justify-between gap-2 px-2 py-2 rounded-lg bg-transparent @hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] transition-colors w-full text-left">
+88 -26
View File
@@ -6,6 +6,13 @@ import { buildMessageTree } from '~~/utils/message';
export type BaseMessage = {
content: string;
fileIds: string[];
files?: {
id: string;
name: string;
mimeType: string;
url: string;
size?: number;
}[];
}
export type ToolCall = typeof schema.toolCalls.$inferSelect
@@ -263,12 +270,6 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
const { data: ssrData } = await useFetch<Topic & { messages: Message[] }>(`/api/topic/${id}`);
data.value = ssrData.value;
} else {
// if (data.value === undefined) {
// const id = unref(topicId);
// const { data: ssrData } = await useFetch<Topic & { messages: Message[] }>(`/api/topic/${id}`);
// data.value = ssrData.value;
// }
await connectSSE();
}
}
@@ -305,6 +306,7 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
const sendMessage = async (
baseMessage: BaseMessage,
onRequest?: () => void,
role: 'user' | 'assistant' = 'user',
): Promise<Result<void, ChatErrorType>> => {
const { user } = useAuth();
if (!user.value) return Err(ChatErrorType.NoUser);
@@ -312,34 +314,94 @@ export const useChat = async (topicId: MaybeRef<string>, connect = true) => {
try {
const message = {
id: nanoid(),
role: 'user',
content: baseMessage.content,
fileIds: baseMessage.fileIds,
role,
}
await $fetch(`/api/topic/${unref(topicId)}/message`, {
method: 'POST',
body: {
message
message: {
...message,
content: baseMessage.content,
fileIds: baseMessage.fileIds,
}
},
onRequest() {
if (data.value) {
data.value!.messages.push({
topicId: unref(topicId),
userId: user.value!.id,
// TODO
attachments: [],
parts: undefined,
generation: null,
parentMessageId: null,
generationId: null,
activeChildId: null,
deleted: null,
createdAt: new Date(),
updatedAt: new Date(),
children: [],
...message
} as Message);
switch (role) {
case 'user': {
const optimisticAttachments = baseMessage.files?.map(file => ({
id: nanoid(),
userId: user.value!.id,
topicId: unref(topicId),
messageId: message.id,
fileId: file.id,
createdAt: new Date(),
file: {
id: file.id,
userId: user.value!.id,
name: file.name,
mimeType: file.mimeType,
size: file.size ?? 0,
url: file.url,
createdAt: new Date(),
},
})) ?? [];
data.value!.messages.push({
topicId: unref(topicId),
userId: user.value!.id,
attachments: optimisticAttachments,
parts: undefined,
generation: null,
parentMessageId: null,
generationId: null,
activeChildId: null,
children: [],
deleted: false,
content: baseMessage.content,
createdAt: new Date(),
updatedAt: new Date(),
...message
});
break;
}
case 'assistant': {
data.value!.messages.push({
topicId: unref(topicId),
userId: user.value!.id,
// TODO
attachments: [],
generation: null,
parentMessageId: null,
generationId: null,
activeChildId: null,
children: [],
deleted: false,
content: null,
createdAt: new Date(),
updatedAt: new Date(),
parts: [
{
id: 'bogus-id',
userId: user.value!.id,
topicId: unref(topicId),
messageId: message.id,
type: 'text',
content: baseMessage.content,
finished: true,
createdAt: new Date(),
lastUpdatedAt: new Date(),
providerOptions: null,
toolCallId: null,
toolCall: null,
}
],
...message
});
break;
}
}
}
onRequest?.();
},
+41 -2
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { nanoid } from 'nanoid';
import type { BaseMessage } from '~/composables/useChat';
import type { ModelWithProvider } from '~/composables/useModels';
@@ -43,7 +44,23 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
children: [],
generationId: null,
activeChildId: null,
attachments: [],
attachments: message.files?.map(file => ({
id: nanoid(),
userId: user.value!.id,
topicId: '',
messageId: '',
fileId: file.id,
createdAt: new Date(),
file: {
id: file.id,
userId: user.value!.id,
name: file.name,
mimeType: file.mimeType,
size: file.size ?? 0,
url: file.url,
createdAt: new Date(),
},
})) ?? [],
deleted: false,
createdAt: new Date(),
updatedAt: new Date(),
@@ -75,6 +92,27 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
return startGeneration(model, undefined, route.params.id as string);
};
const handleAddMessage = async (message: BaseMessage, role: 'user' | 'assistant', _model: ModelWithProvider | null) => {
const user = useAuth().user;
if (!user) {
console.error('No user');
return;
}
const topic = await createTopic(agent.value!.id);
if (!topic) throw new Error('Failed to create topic');
const { sendMessage } = await useChat(topic.id, false);
const res = await sendMessage(message, undefined, role);
if (res.ok === false) {
console.error('Failed to send message:', res.error);
return;
}
await navigateTo(`/agent/${route.params.id}/topic/${topic.id}`);
};
</script>
@@ -108,7 +146,8 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
<div class="sticky bottom-0 z-10 bg-[var(--bg-surface)] pb-4 w-full rounded-t-2xl">
<ChatInput v-model="inputValue" class="[view-transition-name:chat-prompt] duration-150 ease-in-out"
:agent="agent" :providers="providers?.filter(p => p.enabled)" @submit="handleSubmit" />
:allow-manual-role="true" :agent="agent" :providers="providers?.filter(p => p.enabled)"
@submit="handleSubmit" @add-message="handleAddMessage" />
</div>
</div>
</div>
+25 -23
View File
@@ -55,30 +55,32 @@ const changeSystemPrompt = async (e: Event) => {
</script>
<template>
<div class="h-14 flex items-center justify-between px-4">
<div class="flex items-center gap-2">
<button v-if="!sidebarOpen" @click="openSidebar"
class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent @hover:bg-[var(--color-hover)] transition-colors">
<span class="i-mynaui-panel-left-open text-5"></span>
</button>
</div>
</div>
<div class="flex flex-col gap-4 px-14 w-full h-full">
<div class="flex items-center gap-4">
<div>
<img v-if="agent?.imageUrl" :src="agent.imageUrl" class="w-16 h-16 rounded-full object-cover" />
<span class="text-16 i-mynaui-check-hexagon"></span>
<div class="h-full flex flex-col overflow-hidden pb-4">
<div class="h-14 flex items-center justify-between px-4 shrink-0">
<div class="flex items-center gap-2">
<button v-if="!sidebarOpen" @click="openSidebar"
class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent @hover:bg-[var(--color-hover)] transition-colors">
<span class="i-mynaui-panel-left-open text-5"></span>
</button>
</div>
<input v-model="name" @input="handleNameInput" placeholder="Agent Name..."
class="placeholder:text-[var(--text-tertiary)] w-full bg-transparent rounded-none border-b-4 border-b-[var(--color-border)] text-12 p-0"
type="text" />
</div>
<div class="flex flex-col gap-2 w-full h-full mb-14">
<label class="text-sm text-[var(--text-secondary)]">System Message</label>
<textarea v-model="systemPrompt" placeholder="You are a helpful assistant."
class="p-4 w-full h-full resize-none bg-transparent rounded-lg border border-[var(--color-border)]"
@input="changeSystemPrompt"></textarea>
<div class="flex flex-col gap-4 px-4 md:px-14 w-full flex-1 min-h-0">
<div class="flex items-center gap-4">
<div>
<img v-if="agent?.imageUrl" :src="agent.imageUrl" class="w-16 h-16 rounded-full object-cover" />
<span class="text-16 i-mynaui-check-hexagon"></span>
</div>
<input v-model="name" @input="handleNameInput" placeholder="Agent Name..."
class="placeholder:text-[var(--text-tertiary)] w-full bg-transparent rounded-none border-b-4 border-b-[var(--color-border)] text-12 p-0"
type="text" />
</div>
<div class="flex flex-col gap-2 w-full flex-1 min-h-0">
<label class="text-sm text-[var(--text-secondary)]">System Message</label>
<textarea v-model="systemPrompt" placeholder="You are a helpful assistant."
class="p-4 w-full flex-1 min-h-0 resize-none bg-transparent rounded-lg border border-[var(--color-border)]"
@input="changeSystemPrompt"></textarea>
</div>
</div>
</div>
</template>
</template>
+23 -2
View File
@@ -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);
<div class="sticky bottom-0 z-10 bg-[var(--bg-surface)] pb-4 w-full rounded-t-2xl">
<ChatInput v-model="inputValue" class="[view-transition-name:chat-prompt] duration-150 ease-in-out"
:loading="activeGeneration !== null" :agent="agent"
:providers="providers?.filter(p => p.enabled)" @submit="submitMessage" @cancel="handleCancel"
:loading="activeGeneration !== null" :allow-manual-role="true" :agent="agent"
:providers="providers?.filter(p => p.enabled)" @submit="submitMessage" @add-message="handleAddMessage" @cancel="handleCancel"
@resize="handleResize" />
</div>
</div>
+26 -53
View File
@@ -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=="],
+4 -4
View File
@@ -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"
}
}
}
+31 -3
View File
@@ -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') {
@@ -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,
+53 -17
View File
@@ -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);
}
+119 -42
View File
@@ -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
};
}
}
})
+16 -1
View File
@@ -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',
+37
View File
@@ -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);
}
-116
View File
@@ -1,116 +0,0 @@
import { type ToolSet, type TextStreamPart, type ToolCallPart } from 'ai';
export function createLongcatTransformer<TOOLS extends ToolSet>(): (options: {
tools: TOOLS;
stopStream: () => void;
}) => TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>> {
let buffer = '';
let hasToolCallInStep = false;
let lastChunkId: string | undefined;
let lastChunkType: 'text' | 'reasoning' | undefined;
let step = 0;
return (_opts) => {
return new TransformStream<TextStreamPart<TOOLS>, TextStreamPart<TOOLS>>({
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 = /<longcat_tool_call>([\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<string, any> = {};
const argRegex = /<longcat_arg_key>(.*?)<\/longcat_arg_key>\s*<longcat_arg_value>(.*?)<\/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('<longcat_tool_call>')) {
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 });
}
}
});
};
}
+5 -1
View File
@@ -14,7 +14,11 @@ export default {
}
return Ok({
gateway: createOpenAI({ name: 'ClosedRouter', apiKey, baseURL }),
gateway: createOpenAI({
name: 'ClosedRouter',
apiKey,
baseURL
}),
streamTransformer: undefined,
textTransformer: undefined,
});
+95 -54
View File
@@ -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<Message[]>): MessageE
return focusedMessageTree;
}
export const marshallMessages = (agent: Agent, messages: Readonly<MessageEntity[]>): Result<ModelMessage[], string> => {
export interface MarshallOptions {
signFileUrl?: (fileKey: string) => string;
}
export const marshallMessages = (agent: Agent, messages: Readonly<MessageEntity[]>, opts?: MarshallOptions): Result<ModelMessage[], string> => {
const marshalledMessages: ModelMessage[] = [];
if (agent && agent.systemPrompt) {
@@ -51,47 +55,72 @@ export const marshallMessages = (agent: Agent, messages: Readonly<MessageEntity[
switch (message.role) {
case 'user': {
const attachments = message.attachments.map(attachment => {
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<string, any> : undefined,
})
break;
}
case 'tool-call': {
@@ -101,33 +130,32 @@ export const marshallMessages = (agent: Agent, messages: Readonly<MessageEntity[
return Err('Marshalling tool call that is still pending. This is likely a UI bug if this happens.');
}
let inputValue: string = '';
let inputValue: string | object = '';
switch (part.toolCall.input!.type) {
case ToolCallType.Text:
inputValue = part.toolCall.input!.value;
break;
case ToolCallType.Json:
inputValue = JSON.stringify(part.toolCall.input!.value);
if (typeof part.toolCall.input!.value === 'string') {
inputValue = JSON.parse(part.toolCall.input!.value);
} else {
inputValue = part.toolCall.input!.value;
}
break;
}
marshalledMessages.push({
role: 'assistant',
content: [
{
type: 'tool-call',
toolCallId: part.toolCall.id,
toolName: part.toolCall.toolName,
input: inputValue,
},
],
assistantPart.push({
type: 'tool-call',
toolCallId: part.toolCall.id!,
toolName: part.toolCall.toolName!,
input: inputValue,
providerOptions: part.providerOptions ? part.providerOptions as Record<string, any> : 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<MessageEntity[
break;
case ToolCallType.Json:
failureType = 'error-json';
failureValue = JSON.stringify(part.toolCall.error!.value);
if (typeof part.toolCall.error!.value === 'string') {
failureValue = JSON.parse(part.toolCall.error!.value);
} else {
failureValue = part.toolCall.error!.value;
}
break;
}
failureType = 'error-json';
failureValue = JSON.stringify(part.toolCall.error!.value);
// failureValue = JSON.stringify(part.toolCall.error!.value);
if (typeof part.toolCall.error!.value === 'string') {
failureValue = JSON.parse(part.toolCall.error!.value);
} else {
failureValue = part.toolCall.error!.value;
}
}
marshalledMessages.push({
role: 'tool',
content: [
{
type: 'tool-result',
toolCallId: part.toolCall.id,
toolName: part.toolCall.toolName,
output: {
type: failureType,
value: failureValue,
},
},
],
toolParts.push({
type: 'tool-result',
toolCallId: part.toolCall.id!,
toolName: part.toolCall.toolName!,
// @ts-expect-error - This is a type error, because typescript cant provie that the value must be a string when the type is error-text
output: {
type: failureType,
value: failureValue,
},
providerOptions: part.providerOptions ? part.providerOptions as Record<string, any> : undefined,
});
break;
@@ -177,23 +210,22 @@ export const marshallMessages = (agent: Agent, messages: Readonly<MessageEntity[
break;
case ToolCallType.Json:
outputType = 'json';
outputValue = JSON.stringify(part.toolCall.output!.value);
if (typeof part.toolCall.output!.value === 'string') {
outputValue = JSON.parse(part.toolCall.output!.value);
} else {
outputValue = part.toolCall.output!.value;
}
break;
}
marshalledMessages.push({
role: 'tool',
content: [
{
type: 'tool-result',
toolCallId: part.toolCall.id,
toolName: part.toolCall.toolName,
output: {
type: outputType,
value: outputValue,
},
},
],
toolParts.push({
type: 'tool-result',
toolCallId: part.toolCall.id!,
toolName: part.toolCall.toolName!,
output: {
type: outputType,
value: outputValue,
},
providerOptions: part.providerOptions ? part.providerOptions as Record<string, any> : undefined,
});
break;
@@ -203,6 +235,15 @@ export const marshallMessages = (agent: Agent, messages: Readonly<MessageEntity[
return Err(`Unknown part type: ${part.type}`);
}
}
if (assistantPart.length > 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}`);