2025-12-06 12:46:40 +09:00
import {
2025-12-08 19:52:18 +08:00
APICallError ,
2025-12-06 12:46:40 +09:00
convertToModelMessages ,
createUIMessageStream ,
createUIMessageStreamResponse ,
2025-12-14 12:34:34 +09:00
InvalidToolInputError ,
2025-12-08 19:52:18 +08:00
LoadAPIKeyError ,
2025-12-07 00:40:13 +09:00
stepCountIs ,
2025-12-06 12:46:40 +09:00
streamText ,
} from "ai"
2025-12-14 12:34:34 +09:00
import { jsonrepair } from "jsonrepair"
2025-12-06 12:46:40 +09:00
import { z } from "zod"
2025-12-09 15:53:59 +09:00
import { getAIModel , supportsPromptCaching } from "@/lib/ai-providers"
2025-12-06 12:46:40 +09:00
import { findCachedResponse } from "@/lib/cached-responses"
import {
getTelemetryConfig ,
setTraceInput ,
setTraceOutput ,
wrapWithObserve ,
} from "@/lib/langfuse"
import { getSystemPrompt } from "@/lib/system-prompts"
2025-12-12 16:13:06 +09:00
export const maxDuration = 120
2025-03-19 11:03:37 +00:00
2025-12-05 19:30:50 +09:00
// File upload limits (must match client-side)
2025-12-06 12:46:40 +09:00
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
const MAX_FILES = 5
2025-12-05 19:30:50 +09:00
// Helper function to validate file parts in messages
2025-12-06 12:46:40 +09:00
function validateFileParts ( messages : any [ ] ) : {
valid : boolean
error? : string
} {
const lastMessage = messages [ messages . length - 1 ]
const fileParts =
lastMessage ? . parts ? . filter ( ( p : any ) = > p . type === "file" ) || [ ]
if ( fileParts . length > MAX_FILES ) {
return {
valid : false ,
error : ` Too many files. Maximum ${ MAX_FILES } allowed. ` ,
2025-12-05 19:30:50 +09:00
}
}
2025-12-06 12:46:40 +09:00
for ( const filePart of fileParts ) {
// Data URLs format: data:image/png;base64,<data>
// Base64 increases size by ~33%, so we check the decoded size
2025-12-06 16:18:26 +09:00
if ( filePart . url ? . startsWith ( "data:" ) ) {
2025-12-06 12:46:40 +09:00
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 }
2025-12-05 19:30:50 +09:00
}
2025-12-01 14:07:50 +09:00
// Helper function to check if diagram is minimal/empty
function isMinimalDiagram ( xml : string ) : boolean {
2025-12-06 12:46:40 +09:00
const stripped = xml . replace ( /\s/g , "" )
return ! stripped . includes ( 'id="2"' )
2025-12-01 14:07:50 +09:00
}
2025-12-10 18:04:37 +09:00
// 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)
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" ) {
const toolName = part . toolName
if (
toolName === "display_diagram" ||
toolName === "edit_diagram"
) {
return {
. . . part ,
input : {
placeholder :
"[XML content replaced - see current diagram XML in system context]" ,
} ,
}
}
}
return part
} )
return { . . . msg , content : replacedContent }
} )
}
2025-12-01 14:07:50 +09:00
// Helper function to create cached stream response
function createCachedStreamResponse ( xml : string ) : Response {
2025-12-06 12:46:40 +09:00
const toolCallId = ` cached- ${ Date . now ( ) } `
const stream = createUIMessageStream ( {
execute : async ( { writer } ) = > {
writer . write ( { type : "start" } )
writer . write ( {
type : "tool-input-start" ,
toolCallId ,
toolName : "display_diagram" ,
} )
writer . write ( {
type : "tool-input-delta" ,
toolCallId ,
inputTextDelta : xml ,
} )
writer . write ( {
type : "tool-input-available" ,
toolCallId ,
toolName : "display_diagram" ,
input : { xml } ,
} )
writer . write ( { type : "finish" } )
} ,
} )
return createUIMessageStreamResponse ( { stream } )
2025-12-01 14:07:50 +09:00
}
2025-12-04 11:24:26 +09:00
// Inner handler function
async function handleChatRequest ( req : Request ) : Promise < Response > {
2025-12-06 12:46:40 +09:00
// Check for access code
const accessCodes =
process . env . ACCESS_CODE_LIST ? . split ( "," )
. map ( ( code ) = > code . trim ( ) )
. filter ( Boolean ) || [ ]
if ( accessCodes . length > 0 ) {
const accessCodeHeader = req . headers . get ( "x-access-code" )
if ( ! accessCodeHeader || ! accessCodes . includes ( accessCodeHeader ) ) {
return Response . json (
{
error : "Invalid or missing access code. Please configure it in Settings." ,
} ,
{ status : 401 } ,
)
}
2025-12-05 21:09:34 +08:00
}
2025-12-06 12:46:40 +09:00
2025-12-10 18:04:37 +09:00
const { messages , xml , previousXml , sessionId } = await req . json ( )
2025-12-06 12:46:40 +09:00
// Get user IP for Langfuse tracking
const forwardedFor = req . headers . get ( "x-forwarded-for" )
const userId = forwardedFor ? . split ( "," ) [ 0 ] ? . trim ( ) || "anonymous"
// Validate sessionId for Langfuse (must be string, max 200 chars)
const validSessionId =
sessionId && typeof sessionId === "string" && sessionId . length <= 200
? sessionId
: undefined
// Extract user input text for Langfuse trace
2025-12-13 23:28:41 +09:00
const lastMessage = messages [ messages . length - 1 ]
2025-12-06 12:46:40 +09:00
const userInputText =
2025-12-13 23:28:41 +09:00
lastMessage ? . parts ? . find ( ( p : any ) = > p . type === "text" ) ? . text || ""
2025-12-06 12:46:40 +09:00
// Update Langfuse trace with input, session, and user
setTraceInput ( {
input : userInputText ,
sessionId : validSessionId ,
userId : userId ,
} )
// === FILE VALIDATION START ===
const fileValidation = validateFileParts ( messages )
if ( ! fileValidation . valid ) {
return Response . json ( { error : fileValidation.error } , { status : 400 } )
2025-12-01 14:07:50 +09:00
}
2025-12-06 12:46:40 +09:00
// === FILE VALIDATION END ===
// === CACHE CHECK START ===
const isFirstMessage = messages . length === 1
const isEmptyDiagram = ! xml || xml . trim ( ) === "" || isMinimalDiagram ( xml )
2025-12-04 13:26:06 +09:00
2025-12-06 12:46:40 +09:00
if ( isFirstMessage && isEmptyDiagram ) {
const lastMessage = messages [ 0 ]
const textPart = lastMessage . parts ? . find ( ( p : any ) = > p . type === "text" )
const filePart = lastMessage . parts ? . find ( ( p : any ) = > p . type === "file" )
2025-12-04 13:26:06 +09:00
2025-12-06 12:46:40 +09:00
const cached = findCachedResponse ( textPart ? . text || "" , ! ! filePart )
2025-12-04 13:26:06 +09:00
2025-12-06 12:46:40 +09:00
if ( cached ) {
return createCachedStreamResponse ( cached . xml )
}
}
// === CACHE CHECK END ===
2025-12-04 13:26:06 +09:00
feat: add bring-your-own-API-key support (#186)
- Add AI provider settings to config panel (provider, model, API key, base URL)
- Support 7 providers: OpenAI, Anthropic, Google, Azure, OpenRouter, DeepSeek, SiliconFlow
- Client API keys stored in localStorage, never stored on server
- Client settings override server env vars when provided
- Skip server credential validation when client provides API key
- Bypass usage limits (request/token/TPM) when using own API key
- Add /api/config endpoint for fetching usage limits
- Add privacy notices to settings dialog, about pages, and quota toast
- Add clear settings button to reset saved API keys
- Update README files (EN/CN/JA) with BYOK documentation
Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-09 17:50:07 +09:00
// Read client AI provider overrides from headers
const clientOverrides = {
provider : req.headers.get ( "x-ai-provider" ) ,
baseUrl : req.headers.get ( "x-ai-base-url" ) ,
apiKey : req.headers.get ( "x-ai-api-key" ) ,
modelId : req.headers.get ( "x-ai-model" ) ,
}
2025-12-14 19:38:40 +09:00
// Read minimal style preference from header
const minimalStyle = req . headers . get ( "x-minimal-style" ) === "true"
feat: add bring-your-own-API-key support (#186)
- Add AI provider settings to config panel (provider, model, API key, base URL)
- Support 7 providers: OpenAI, Anthropic, Google, Azure, OpenRouter, DeepSeek, SiliconFlow
- Client API keys stored in localStorage, never stored on server
- Client settings override server env vars when provided
- Skip server credential validation when client provides API key
- Bypass usage limits (request/token/TPM) when using own API key
- Add /api/config endpoint for fetching usage limits
- Add privacy notices to settings dialog, about pages, and quota toast
- Add clear settings button to reset saved API keys
- Update README files (EN/CN/JA) with BYOK documentation
Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-09 17:50:07 +09:00
// Get AI model with optional client overrides
const { model , providerOptions , headers , modelId } =
getAIModel ( clientOverrides )
2025-12-04 13:26:06 +09:00
2025-12-09 15:53:59 +09:00
// Check if model supports prompt caching
const shouldCache = supportsPromptCaching ( modelId )
console . log (
` [Prompt Caching] ${ shouldCache ? "ENABLED" : "DISABLED" } for model: ${ modelId } ` ,
)
2025-12-06 12:46:40 +09:00
// Get the appropriate system prompt based on model (extended for Opus/Haiku 4.5)
2025-12-14 19:38:40 +09:00
const systemMessage = getSystemPrompt ( modelId , minimalStyle )
2025-12-04 13:26:06 +09:00
2025-12-06 12:46:40 +09:00
// Extract file parts (images) from the last message
const fileParts =
lastMessage . parts ? . filter ( ( part : any ) = > part . type === "file" ) || [ ]
// User input only - XML is now in a separate cached system message
const formattedUserInput = ` User input:
2025-03-24 02:38:27 +00:00
"" " md
2025-12-13 23:28:41 +09:00
$ { userInputText }
2025-12-06 12:46:40 +09:00
"" " `
// Convert UIMessages to ModelMessages and add system message
const modelMessages = convertToModelMessages ( messages )
2025-12-11 17:36:18 +09:00
// Replace historical tool call XML with placeholders to reduce tokens
// Disabled by default - some models (e.g. minimax) copy placeholders instead of generating XML
const enableHistoryReplace =
process . env . ENABLE_HISTORY_XML_REPLACE === "true"
const placeholderMessages = enableHistoryReplace
2025-12-13 23:28:41 +09:00
? replaceHistoricalToolInputs ( modelMessages )
: modelMessages
2025-12-10 18:04:37 +09:00
2025-12-06 12:46:40 +09:00
// Filter out messages with empty content arrays (Bedrock API rejects these)
// This is a safety measure - ideally convertToModelMessages should handle all cases
2025-12-10 18:04:37 +09:00
let enhancedMessages = placeholderMessages . filter (
2025-12-06 12:46:40 +09:00
( msg : any ) = >
msg . content && Array . isArray ( msg . content ) && msg . content . length > 0 ,
)
// Update the last message with user input only (XML moved to separate cached system message)
if ( enhancedMessages . length >= 1 ) {
const lastModelMessage = enhancedMessages [ enhancedMessages . length - 1 ]
if ( lastModelMessage . role === "user" ) {
// Build content array with user input text and file parts
const contentParts : any [ ] = [
{ type : "text" , text : formattedUserInput } ,
]
// Add image parts back
for ( const filePart of fileParts ) {
contentParts . push ( {
type : "image" ,
image : filePart.url ,
mimeType : filePart.mediaType ,
} )
}
enhancedMessages = [
. . . enhancedMessages . slice ( 0 , - 1 ) ,
{ . . . lastModelMessage , content : contentParts } ,
]
}
2025-08-31 12:54:14 +09:00
}
2025-12-06 12:46:40 +09:00
// Add cache point to the last assistant message in conversation history
// This caches the entire conversation prefix for subsequent requests
// Strategy: system (cached) + history with last assistant (cached) + new user message
2025-12-09 15:53:59 +09:00
if ( shouldCache && enhancedMessages . length >= 2 ) {
2025-12-06 12:46:40 +09:00
// Find the last assistant message (should be second-to-last, before current user message)
for ( let i = enhancedMessages . length - 2 ; i >= 0 ; i -- ) {
if ( enhancedMessages [ i ] . role === "assistant" ) {
enhancedMessages [ i ] = {
. . . enhancedMessages [ i ] ,
providerOptions : {
bedrock : { cachePoint : { type : "default" } } ,
} ,
}
break // Only cache the last assistant message
}
}
2025-12-01 10:43:33 +09:00
}
2025-12-06 12:46:40 +09:00
// System messages with multiple cache breakpoints for optimal caching:
// - Breakpoint 1: Static instructions (~1500 tokens) - rarely changes
// - Breakpoint 2: Current XML context - changes per diagram, but constant within a conversation turn
// This allows: if only user message changes, both system caches are reused
// if XML changes, instruction cache is still reused
const systemMessages = [
// Cache breakpoint 1: Instructions (rarely change)
{
role : "system" as const ,
content : systemMessage ,
2025-12-09 15:53:59 +09:00
. . . ( shouldCache && {
providerOptions : {
bedrock : { cachePoint : { type : "default" } } ,
} ,
} ) ,
2025-12-06 12:46:40 +09:00
} ,
2025-12-10 18:04:37 +09:00
// Cache breakpoint 2: Previous and Current diagram XML context
2025-12-06 12:46:40 +09:00
{
role : "system" as const ,
2025-12-10 18:04:37 +09:00
content : ` ${ previousXml ? ` Previous diagram XML (before user's last message): \ n"""xml \ n ${ previousXml } \ n""" \ n \ n ` : "" } Current diagram XML (AUTHORITATIVE - the source of truth): \ n"""xml \ n ${ xml || "" } \ n""" \ n \ nIMPORTANT: The "Current diagram XML" is the SINGLE SOURCE OF TRUTH for what's on the canvas right now. The user can manually add, delete, or modify shapes directly in draw.io. Always count and describe elements based on the CURRENT XML, not on what you previously generated. If both previous and current XML are shown, compare them to understand what the user changed. When using edit_diagram, COPY search patterns exactly from the CURRENT XML - attribute order matters! ` ,
2025-12-09 15:53:59 +09:00
. . . ( shouldCache && {
providerOptions : {
bedrock : { cachePoint : { type : "default" } } ,
} ,
} ) ,
2025-12-06 12:46:40 +09:00
} ,
]
const allMessages = [ . . . systemMessages , . . . enhancedMessages ]
const result = streamText ( {
model ,
2025-12-13 23:28:41 +09:00
. . . ( process . env . MAX_OUTPUT_TOKENS && {
maxOutputTokens : parseInt ( process . env . MAX_OUTPUT_TOKENS , 10 ) ,
} ) ,
2025-12-07 00:40:13 +09:00
stopWhen : stepCountIs ( 5 ) ,
2025-12-14 12:34:34 +09:00
// Repair truncated tool calls when maxOutputTokens is reached mid-JSON
experimental_repairToolCall : async ( { toolCall , error } ) = > {
// Only attempt repair for invalid tool input (broken JSON from truncation)
if (
error instanceof InvalidToolInputError ||
error . name === "AI_InvalidToolInputError"
) {
try {
// Use jsonrepair to fix truncated JSON
const repairedInput = jsonrepair ( toolCall . input )
console . log (
` [repairToolCall] Repaired truncated JSON for tool: ${ toolCall . toolName } ` ,
)
return { . . . toolCall , input : repairedInput }
} catch ( repairError ) {
console . warn (
` [repairToolCall] Failed to repair JSON for tool: ${ toolCall . toolName } ` ,
repairError ,
)
return null
}
}
// Don't attempt to repair other errors (like NoSuchToolError)
return null
} ,
2025-12-06 12:46:40 +09:00
messages : allMessages ,
2025-12-10 20:54:43 +05:30
. . . ( providerOptions && { providerOptions } ) , // This now includes all reasoning configs
2025-12-06 12:46:40 +09:00
. . . ( headers && { headers } ) ,
// Langfuse telemetry config (returns undefined if not configured)
. . . ( getTelemetryConfig ( { sessionId : validSessionId , userId } ) && {
experimental_telemetry : getTelemetryConfig ( {
sessionId : validSessionId ,
userId ,
} ) ,
} ) ,
2025-12-08 18:56:34 +09:00
onFinish : ( { text , usage } ) = > {
2025-12-06 12:46:40 +09:00
// Pass usage to Langfuse (Bedrock streaming doesn't auto-report tokens to telemetry)
setTraceOutput ( text , {
promptTokens : usage?.inputTokens ,
completionTokens : usage?.outputTokens ,
} )
} ,
tools : {
// Client-side tool that will be executed on the client
display_diagram : {
2025-12-14 14:04:44 +09:00
description : ` Display a diagram on draw.io. Pass ONLY the mxCell elements - wrapper tags and root cells are added automatically.
2025-12-03 16:14:53 +09:00
VALIDATION RULES ( XML will be rejected if violated ) :
2025-12-14 14:04:44 +09:00
1 . Generate ONLY mxCell elements - NO wrapper tags ( < mxfile > , < mxGraphModel > , < root > )
2 . Do NOT include root cells ( id = "0" or id = "1" ) - they are added automatically
3 . All mxCell elements must be siblings - never nested
4 . Every mxCell needs a unique id ( start from "2" )
5 . Every mxCell needs a valid parent attribute ( use "1" for top - level )
6 . Escape special chars in values : & lt ; & gt ; & amp ; & quot ;
Example ( generate ONLY this - no wrapper tags ) :
< mxCell id = "lane1" value = "Frontend" style = "swimlane;" vertex = "1" parent = "1" >
< mxGeometry x = "40" y = "40" width = "200" height = "200" as = "geometry" / >
< / mxCell >
< mxCell id = "step1" value = "Step 1" style = "rounded=1;" vertex = "1" parent = "lane1" >
< mxGeometry x = "20" y = "60" width = "160" height = "40" as = "geometry" / >
< / mxCell >
< mxCell id = "lane2" value = "Backend" style = "swimlane;" vertex = "1" parent = "1" >
< mxGeometry x = "280" y = "40" width = "200" height = "200" as = "geometry" / >
< / mxCell >
< mxCell id = "step2" value = "Step 2" style = "rounded=1;" vertex = "1" parent = "lane2" >
< mxGeometry x = "20" y = "60" width = "160" height = "40" as = "geometry" / >
< / mxCell >
< mxCell id = "edge1" style = "edgeStyle=orthogonalEdgeStyle;endArrow=classic;" edge = "1" parent = "1" source = "step1" target = "step2" >
< mxGeometry relative = "1" as = "geometry" / >
< / mxCell >
2025-12-03 16:14:53 +09:00
Notes :
- For AWS diagrams , use * * AWS 2025 icons * * .
- For animated connectors , add "flowAnimation=1" to edge style .
` ,
2025-12-06 12:46:40 +09:00
inputSchema : z.object ( {
xml : z
. string ( )
. describe ( "XML string to be displayed on draw.io" ) ,
} ) ,
} ,
edit_diagram : {
description : ` Edit specific parts of the current diagram by replacing exact line matches. Use this tool to make targeted fixes without regenerating the entire XML.
2025-12-06 12:41:01 +09:00
CRITICAL : Copy - paste the EXACT search pattern from the "Current diagram XML" in system context . Do NOT reorder attributes or reformat - the attribute order in draw . io XML varies and you MUST match it exactly .
IMPORTANT : Keep edits concise :
- COPY the exact mxCell line from the current XML ( attribute order matters ! )
- Only include the lines that are changing , plus 1 - 2 surrounding lines for context if needed
- Break large changes into multiple smaller edits
- Each search must contain complete lines ( never truncate mid - line )
2025-12-07 00:40:13 +09:00
- First match only - be specific enough to target the right element
⚠ ️ JSON ESCAPING : Every " inside string values MUST be escaped as \\" . Example : x = \ \ "100\\" y = \ \ "200\\" - BOTH quotes need backslashes ! ` ,
2025-12-06 12:46:40 +09:00
inputSchema : z.object ( {
edits : z
. array (
z . object ( {
search : z
. string ( )
. describe (
"EXACT lines copied from current XML (preserve attribute order!)" ,
) ,
replace : z
. string ( )
. describe ( "Replacement lines" ) ,
} ) ,
)
. describe (
"Array of search/replace pairs to apply sequentially" ,
) ,
} ) ,
} ,
2025-12-14 12:34:34 +09:00
append_diagram : {
description : ` Continue generating diagram XML when previous display_diagram output was truncated due to length limits.
WHEN TO USE : Only call this tool after display_diagram was truncated ( you ' ll see an error message about truncation ) .
CRITICAL INSTRUCTIONS :
2025-12-14 14:04:44 +09:00
1 . Do NOT include any wrapper tags - just continue the mxCell elements
2025-12-14 12:34:34 +09:00
2 . Continue from EXACTLY where your previous output stopped
2025-12-14 14:04:44 +09:00
3 . Complete the remaining mxCell elements
2025-12-14 12:34:34 +09:00
4 . If still truncated , call append_diagram again with the next fragment
Example : If previous output ended with '<mxCell id="x" style="rounded=1' , continue with ';" vertex="1">...' and complete the remaining elements . ` ,
inputSchema : z.object ( {
xml : z
. string ( )
. describe (
"Continuation XML fragment to append (NO wrapper tags)" ,
) ,
} ) ,
} ,
2025-12-06 12:46:40 +09:00
} ,
2025-12-06 22:04:59 +05:30
. . . ( process . env . TEMPERATURE !== undefined && {
temperature : parseFloat ( process . env . TEMPERATURE ) ,
} ) ,
2025-12-06 12:46:40 +09:00
} )
2025-12-08 18:56:34 +09:00
return result . toUIMessageStreamResponse ( {
2025-12-10 20:54:43 +05:30
sendReasoning : true ,
2025-12-08 18:56:34 +09:00
messageMetadata : ( { part } ) = > {
if ( part . type === "finish" ) {
const usage = ( part as any ) . totalUsage
if ( ! usage ) {
console . warn (
"[messageMetadata] No usage data in finish part" ,
)
return undefined
}
// Total input = non-cached + cached (these are separate counts)
// Note: cacheWriteInputTokens is not available on finish part
const totalInputTokens =
( usage . inputTokens ? ? 0 ) + ( usage . cachedInputTokens ? ? 0 )
return {
inputTokens : totalInputTokens ,
outputTokens : usage.outputTokens ? ? 0 ,
2025-12-14 12:34:34 +09:00
finishReason : ( part as any ) . finishReason ,
2025-12-08 18:56:34 +09:00
}
}
return undefined
} ,
} )
2025-12-04 11:24:26 +09:00
}
2025-12-08 19:52:18 +08:00
// Helper to categorize errors and return appropriate response
function handleError ( error : unknown ) : Response {
console . error ( "Error in chat route:" , error )
const isDev = process . env . NODE_ENV === "development"
// Check for specific AI SDK error types
if ( APICallError . isInstance ( error ) ) {
return Response . json (
{
error : error.message ,
. . . ( isDev && {
details : error.responseBody ,
stack : error.stack ,
} ) ,
} ,
{ status : error.statusCode || 500 } ,
)
}
if ( LoadAPIKeyError . isInstance ( error ) ) {
return Response . json (
{
error : "Authentication failed. Please check your API key." ,
. . . ( isDev && {
stack : error.stack ,
} ) ,
} ,
{ status : 401 } ,
)
}
// Fallback for other errors with safety filter
const message =
error instanceof Error ? error . message : "An unexpected error occurred"
const status = ( error as any ) ? . statusCode || ( error as any ) ? . status || 500
// Prevent leaking API keys, tokens, or other sensitive data
const lowerMessage = message . toLowerCase ( )
const safeMessage =
lowerMessage . includes ( "key" ) ||
lowerMessage . includes ( "token" ) ||
lowerMessage . includes ( "sig" ) ||
lowerMessage . includes ( "signature" ) ||
lowerMessage . includes ( "secret" ) ||
lowerMessage . includes ( "password" ) ||
lowerMessage . includes ( "credential" )
? "Authentication failed. Please check your credentials."
: message
return Response . json (
{
error : safeMessage ,
. . . ( isDev && {
details : message ,
stack : error instanceof Error ? error.stack : undefined ,
} ) ,
} ,
{ status } ,
)
}
2025-12-05 21:15:02 +09:00
// Wrap handler with error handling
async function safeHandler ( req : Request ) : Promise < Response > {
2025-12-06 12:46:40 +09:00
try {
return await handleChatRequest ( req )
} catch ( error ) {
2025-12-08 19:52:18 +08:00
return handleError ( error )
2025-12-06 12:46:40 +09:00
}
2025-03-19 08:16:44 +00:00
}
2025-12-05 21:15:02 +09:00
// Wrap with Langfuse observe (if configured)
2025-12-06 12:46:40 +09:00
const observedHandler = wrapWithObserve ( safeHandler )
2025-12-05 21:15:02 +09:00
export async function POST ( req : Request ) {
2025-12-06 12:46:40 +09:00
return observedHandler ( req )
2025-12-05 21:15:02 +09:00
}