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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user