mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-04 00:37:48 +08:00
- Vertex: a client-supplied base URL only works with the client's own Vertex key - Accept only data: URLs for file parts in every message, so the server never downloads them - Output budget retry accounts for the thinking budget Bedrock/Anthropic add, and reads Volcengine, DashScope, SGLang and vLLM rejections; falls back to 16000 once - x-max-output-tokens can only lower the budget on server credentials - On server credentials only server models or AI_MODEL entries can be used - Drop tool results together with the invalid tool calls they belong to - Count quota tokens as input + output (cached tokens were counted twice) - Private-URL check for custom base URLs, end Langfuse traces on error/abort/early return - Fix repairToolCall ordering and placeholder, align edit_diagram prompt with operations - Panel Bedrock keys are read from ADMIN_AWS_*; forward the access code to EdgeOne - isMinimalDiagram only treats root cells as an empty canvas
143 lines
5.4 KiB
TypeScript
143 lines
5.4 KiB
TypeScript
// Shared helper functions for chat route
|
|
// Exported for testing
|
|
|
|
// File upload limits (must match client-side)
|
|
export const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
|
|
export const MAX_FILES = 5
|
|
|
|
// Helper function to validate file parts in messages
|
|
// Checks every message, since history is sent to the model too
|
|
export function validateFileParts(messages: any[]): {
|
|
valid: boolean
|
|
error?: string
|
|
} {
|
|
for (const message of messages) {
|
|
const fileParts =
|
|
message?.parts?.filter((p: any) => p.type === "file") || []
|
|
|
|
if (fileParts.length > MAX_FILES) {
|
|
return {
|
|
valid: false,
|
|
error: `Too many files. Maximum ${MAX_FILES} allowed.`,
|
|
}
|
|
}
|
|
|
|
for (const filePart of fileParts) {
|
|
// The client sends files inline. Any other URL would be downloaded
|
|
// by the server (AI SDK does that for models without URL support).
|
|
if (
|
|
typeof filePart.url !== "string" ||
|
|
!filePart.url.startsWith("data:")
|
|
) {
|
|
return {
|
|
valid: false,
|
|
error: "Files must be uploaded inline as data URLs.",
|
|
}
|
|
}
|
|
|
|
// Data URLs format: data:image/png;base64,<data>
|
|
// Base64 increases size by ~33%, so we check the decoded size
|
|
const base64Data = filePart.url.split(",")[1]
|
|
if (base64Data) {
|
|
const sizeInBytes = Math.ceil((base64Data.length * 3) / 4)
|
|
if (sizeInBytes > MAX_FILE_SIZE) {
|
|
return {
|
|
valid: false,
|
|
error: `File exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return { valid: true }
|
|
}
|
|
|
|
// Helper function to check if diagram is minimal/empty
|
|
// Empty means no mxCell besides the root cells "0" and "1". Cells drawn in
|
|
// draw.io get random ids, so checking for id="2" is not enough.
|
|
export function isMinimalDiagram(xml: string): boolean {
|
|
return !/<mxCell\b[^>]*\bid="(?![01]")/.test(xml)
|
|
}
|
|
|
|
// A tool-call input providers accept: a non-empty JSON object
|
|
function isValidToolInput(input: unknown): boolean {
|
|
return !!input && typeof input === "object" && Object.keys(input).length > 0
|
|
}
|
|
|
|
// Helper function to replace historical tool call XML with placeholders
|
|
// This reduces token usage and forces LLM to rely on the current diagram XML (source of truth)
|
|
// Tool calls with invalid inputs are left for dropInvalidToolCalls to remove
|
|
export function replaceHistoricalToolInputs(messages: any[]): any[] {
|
|
return messages.map((msg) => {
|
|
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
|
return msg
|
|
}
|
|
const replacedContent = msg.content.map((part: any) => {
|
|
if (
|
|
part.type === "tool-call" &&
|
|
isValidToolInput(part.input) &&
|
|
(part.toolName === "display_diagram" ||
|
|
part.toolName === "edit_diagram")
|
|
) {
|
|
return {
|
|
...part,
|
|
input: {
|
|
placeholder:
|
|
"[XML content replaced - see current diagram XML in system context]",
|
|
},
|
|
}
|
|
}
|
|
return part
|
|
})
|
|
return { ...msg, content: replacedContent }
|
|
})
|
|
}
|
|
|
|
// Remove tool-calls with invalid inputs (from failed repair or interrupted streaming),
|
|
// together with their tool-results: providers reject a result whose call is missing.
|
|
// Messages left empty are removed too (Bedrock rejects empty content arrays).
|
|
export function dropInvalidToolCalls(messages: any[]): any[] {
|
|
const droppedIds = new Set<string>()
|
|
return messages
|
|
.map((msg) => {
|
|
if (!Array.isArray(msg.content)) return msg
|
|
const content = msg.content.filter((part: any) => {
|
|
if (
|
|
msg.role === "assistant" &&
|
|
part.type === "tool-call" &&
|
|
!isValidToolInput(part.input)
|
|
) {
|
|
console.warn(
|
|
`[chat-helpers] Dropping tool-call with invalid input:`,
|
|
{ toolName: part.toolName, input: part.input },
|
|
)
|
|
droppedIds.add(part.toolCallId)
|
|
return false
|
|
}
|
|
// Results always come after their call, so the id is known by now
|
|
return !(
|
|
part.type === "tool-result" &&
|
|
droppedIds.has(part.toolCallId)
|
|
)
|
|
})
|
|
return { ...msg, content }
|
|
})
|
|
.filter((msg) => !Array.isArray(msg.content) || msg.content.length > 0)
|
|
}
|
|
|
|
// Fix common LLM JSON mistakes in tool-call input before jsonrepair runs
|
|
export function fixToolInputJson(input: string): string {
|
|
return (
|
|
input
|
|
// Inconsistent quote escaping in XML attributes inside JSON strings:
|
|
// y="-20\" (opening quote unescaped, closing escaped) becomes y=\"-20\".
|
|
// Must run before the key fix below, which would rewrite the `="`.
|
|
.replace(/(\w+)="([^"]*?)\\"/g, '$1=\\"$2\\"')
|
|
// `:=` instead of `: `
|
|
.replace(/:=/g, ": ")
|
|
// `"key"= "` instead of `"key": "`, only for JSON keys
|
|
.replace(/"(\w+)"\s*=\s*"/g, '"$1": "')
|
|
)
|
|
}
|