From 207bd7b4faacfc220bf7198605bdf101e139a141 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 3 Feb 2025 11:28:46 -0800 Subject: [PATCH] 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 --- app/api/proxy/route.ts | 110 ++++++++++++++++++++++++++++++++++++++++ executor/index.ts | 54 +++----------------- tools/anthropic/chat.ts | 46 ++++++++++------- tools/index.ts | 53 +++++++++---------- 4 files changed, 169 insertions(+), 94 deletions(-) create mode 100644 app/api/proxy/route.ts diff --git a/app/api/proxy/route.ts b/app/api/proxy/route.ts new file mode 100644 index 000000000..517ea2d9c --- /dev/null +++ b/app/api/proxy/route.ts @@ -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 } + ) + } +} \ No newline at end of file diff --git a/executor/index.ts b/executor/index.ts index f41827706..ad2f63933 100644 --- a/executor/index.ts +++ b/executor/index.ts @@ -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)) } diff --git a/tools/anthropic/chat.ts b/tools/anthropic/chat.ts index 65f74f117..dc11f0f1c 100644 --- a/tools/anthropic/chat.ts +++ b/tools/anthropic/chat.ts @@ -49,6 +49,11 @@ export const chatTool: ToolConfig = { 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 = { '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})` } } \ No newline at end of file diff --git a/tools/index.ts b/tools/index.ts index b3d93d156..0e0858514 100644 --- a/tools/index.ts +++ b/tools/index.ts @@ -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 ): Promise { - 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'}` } } } \ No newline at end of file