Added reverse proxy to resolve CORS issues, now all external API calls are routed through /api/proxy, API keys & credentials are now all on the server

This commit is contained in:
Waleed Latif
2025-02-03 11:28:46 -08:00
parent 8f958c319d
commit 207bd7b4fa
4 changed files with 169 additions and 94 deletions

110
app/api/proxy/route.ts Normal file
View File

@@ -0,0 +1,110 @@
import { NextResponse } from 'next/server'
import { getTool } from '@/tools'
export async function POST(request: Request) {
try {
// Expecting a tool identifier and the validated parameters
const { toolId, params } = await request.json()
// Look up the tool config from the registry
const tool = getTool(toolId)
if (!tool) {
return NextResponse.json(
{
success: false,
error: `Tool not found: ${toolId}`,
details: { toolId }
},
{ status: 400 }
)
}
// Destructure the request configuration from the tool
const { url: urlOrFn, method: defaultMethod, headers: headersFn, body: bodyFn } = tool.request
// Compute the external URL
const url = typeof urlOrFn === 'function' ? urlOrFn(params) : urlOrFn
// If the params override the method, allow it (or use the default)
const method = params.method || defaultMethod || 'GET'
// Compute headers using the tool's function
const headers = headersFn ? headersFn(params) : {}
// Build request body if needed
const hasBody = method !== 'GET' && method !== 'HEAD' && !!bodyFn
const body = hasBody ? JSON.stringify(bodyFn!(params)) : undefined
// Execute external fetch from the server side
const externalResponse = await fetch(url, { method, headers, body })
// If the response is not OK, transform the error
if (!externalResponse.ok) {
const errorContent = await externalResponse.json().catch(() => ({
message: externalResponse.statusText
}))
// Pass the complete error response to transformError
const error = tool.transformError
? tool.transformError({
...errorContent,
status: externalResponse.status,
statusText: externalResponse.statusText,
// Include raw headers as they were sent
headers: {
authorization: externalResponse.headers.get('authorization'),
'content-type': externalResponse.headers.get('content-type')
}
})
: errorContent.message || 'External API error'
// Return error in a format that matches the ToolResponse type
return NextResponse.json({
success: false,
output: {},
error: error
}, {
status: externalResponse.status
})
}
// Transform the response if needed
const transformResponse =
tool.transformResponse ||
(async (resp: Response) => ({
success: true,
output: await resp.json(),
}))
const result = await transformResponse(externalResponse)
if (!result.success) {
const error = tool.transformError
? tool.transformError(result)
: 'Tool returned an error'
return NextResponse.json({
success: false,
output: {},
error: error
}, {
status: 400
})
}
return NextResponse.json(result)
} catch (error: any) {
console.error('Proxy route error:', error)
return NextResponse.json(
{
success: false,
error: error.message || 'Internal server error',
details: {
name: error.name,
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
}
},
{ status: 500 }
)
}
}

View File

@@ -17,7 +17,7 @@ import {
ExecutionResult,
BlockLog
} from './types'
import { tools } from '@/tools'
import { tools, executeTool } from '@/tools'
export class Executor {
constructor(
@@ -219,59 +219,17 @@ export class Executor {
throw new Error(`Tool not found: ${toolId}`)
}
// Merge block's static params with dynamic inputs
// Merge block's static params with dynamic inputs and validate them
const validatedParams = this.validateToolParams(tool, {
...block.config.params,
...inputs,
})
if (!tool.request) {
throw new Error(`Tool "${toolId}" has no request config.`)
}
const { url: urlOrFn, method: defaultMethod, headers: headersFn, body: bodyFn } =
tool.request
// Build the URL
const url = typeof urlOrFn === 'function' ? urlOrFn(validatedParams) : urlOrFn
// Determine HTTP method
const methodFromParams =
typeof validatedParams.method === 'object'
? validatedParams.method.method
: validatedParams.method
const method = methodFromParams || defaultMethod || 'GET'
// Safely compute headers
const headers = headersFn?.(validatedParams) ?? {}
// Build body if needed
const bodyNeeded = method !== 'GET' && method !== 'HEAD' && !!bodyFn
const body = bodyNeeded
? JSON.stringify(bodyFn!(validatedParams))
: undefined
// Perform fetch()
const response = await fetch(url || '', { method, headers, body })
if (!response.ok) {
// In case there is a custom transformError
const transformError = tool.transformError ?? (() => 'Unknown error')
const errorBody = await response.json().catch(() => ({
message: response.statusText,
}))
throw new Error(transformError(errorBody))
}
// Transform the response
const transformResponse =
tool.transformResponse ??
(async (resp: Response) => ({
success: true,
output: await resp.json(),
}))
const result = await transformResponse(response)
// Call centralized reverse proxy endpoint via executeTool().
const result = await executeTool(toolId, validatedParams)
if (!result.success) {
const transformError = tool.transformError ?? (() => 'Tool returned an error object')
const transformError = tool.transformError ?? (() => 'Tool returned an error')
throw new Error(transformError(result))
}

View File

@@ -49,6 +49,11 @@ export const chatTool: ToolConfig<ChatParams, ChatResponse> = {
type: 'number',
default: 0.7,
description: 'Controls randomness in the response'
},
maxTokens: {
type: 'number',
default: 4096,
description: 'Maximum number of tokens to generate'
}
},
@@ -61,41 +66,46 @@ export const chatTool: ToolConfig<ChatParams, ChatResponse> = {
'anthropic-version': '2023-06-01'
}),
body: (params) => {
const messages = [
{ role: 'user', content: params.systemPrompt }
]
const messages = []
// Add user message if context is provided
if (params.context) {
messages.push({ role: 'user', content: params.context })
messages.push({
role: 'user',
content: params.context
})
}
const body = {
return {
model: params.model || 'claude-3-5-sonnet-20241022',
messages,
temperature: params.temperature,
max_tokens: params.maxTokens,
top_p: params.topP,
stream: params.stream
}
return body
system: params.systemPrompt,
temperature: params.temperature || 0.7,
max_tokens: params.maxTokens || 4096
}
}
},
transformResponse: async (response: Response) => {
const data = await response.json()
const data = await response.json()
if (!data.content) {
throw new Error('Unable to extract content from Anthropic API response')
}
return {
success: true,
output: {
content: data.completion,
content: data.content[0].text,
model: data.model,
tokens: data.usage?.total_tokens
tokens: data.usage?.input_tokens + data.usage?.output_tokens
}
}
}
},
transformError: (error) => {
const message = error.error?.message || error.message
const code = error.error?.type || error.code
return `${message} (${code})`
const message = error.error?.message || error.message
const code = error.error?.type || error.code
return `${message} (${code})`
}
}

View File

@@ -45,43 +45,40 @@ export function getTool(toolId: string): ToolConfig | undefined {
return tools[toolId]
}
// Execute a tool with parameters
// Execute a tool by calling the reverse proxy endpoint.
export async function executeTool(
toolId: string,
params: Record<string, any>
): Promise<ToolResponse> {
const tool = getTool(toolId)
if (!tool) {
return {
success: false,
output: {},
error: `Tool not found: ${toolId}`
}
}
try {
// Get the URL (which might be a function or string)
const url = typeof tool.request.url === 'function'
? tool.request.url(params)
: tool.request.url
// Make the HTTP request
const response = await fetch(url, {
method: tool.request.method,
headers: tool.request.headers(params),
body: tool.request.body ? JSON.stringify(tool.request.body(params)) : undefined
})
// Transform the response
const result = await tool.transformResponse(response)
const response = await fetch('/api/proxy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ toolId, params }),
})
const result = await response.json()
if (!result.success) {
// Format error message to include details if available
const errorMessage = result.details
? `${result.error} (${JSON.stringify(result.details)})`
: result.error
return {
success: false,
output: {},
error: errorMessage
}
}
return result
} catch (error) {
} catch (error: any) {
console.error('Tool execution error:', error)
return {
success: false,
output: {},
error: tool.transformError(error)
error: `Error executing tool: ${error.message || 'Unknown error'}`
}
}
}