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:
@@ -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,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>
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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?.();
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user