From e1466c4f6ec9dc8330f0a4223dd55c2712e5714d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 21 Mar 2025 11:20:39 -0700 Subject: [PATCH] feat[logs]: added cost approximations for each model, display in logs sidebar (#135) * added cost approximations for each model, display in logs sidebar * npm run format --- sim/app/(auth)/verify/page.tsx | 9 +- sim/app/(auth)/verify/use-verification.ts | 9 +- sim/app/(auth)/verify/verify-content.tsx | 14 +- sim/app/blocks/blocks/github.ts | 19 ++- sim/app/components/ui/command.tsx | 6 + sim/app/db/migrations/meta/0018_snapshot.json | 150 +++++------------ sim/app/db/migrations/meta/_journal.json | 2 +- sim/app/executor/handlers.ts | 159 +++++++++--------- sim/app/executor/index.ts | 114 +++++++------ sim/app/executor/resolver.ts | 46 ++--- sim/app/lib/logs/execution-logger.ts | 132 ++++++++++++++- sim/app/providers/index.ts | 10 +- sim/app/providers/pricing.ts | 125 ++++++++++++++ sim/app/providers/types.ts | 21 +++ sim/app/providers/utils.ts | 65 +++++++ sim/app/stores/workflows/workflow/store.ts | 16 +- sim/app/tools/github/comment.ts | 4 +- sim/app/tools/github/commit.ts | 22 +-- sim/app/tools/github/pr.test.ts | 2 +- sim/app/tools/github/repo.test.ts | 7 +- sim/app/tools/index.ts | 108 ++++++------ .../w/[id]/hooks/use-workflow-execution.ts | 30 ++-- sim/app/w/agents/stores/store.ts | 116 ++++++------- sim/app/w/agents/stores/types.ts | 22 +-- sim/app/w/agents/utils/agent.ts | 8 +- sim/app/w/agents/utils/mcp-server.ts | 8 +- sim/app/w/logs/components/sidebar/sidebar.tsx | 62 ++++++- .../trace-spans/trace-spans-display.tsx | 15 +- sim/app/w/logs/logs.tsx | 17 ++ sim/app/w/logs/stores/types.ts | 22 ++- 30 files changed, 869 insertions(+), 471 deletions(-) create mode 100644 sim/app/providers/pricing.ts diff --git a/sim/app/(auth)/verify/page.tsx b/sim/app/(auth)/verify/page.tsx index 6c71c10ef0..43171d589b 100644 --- a/sim/app/(auth)/verify/page.tsx +++ b/sim/app/(auth)/verify/page.tsx @@ -6,8 +6,7 @@ export default function VerifyPage() { const baseUrl = `${protocol}://${appUrl}` const hasResendKey = Boolean( - process.env.RESEND_API_KEY && - process.env.RESEND_API_KEY !== 'placeholder' + process.env.RESEND_API_KEY && process.env.RESEND_API_KEY !== 'placeholder' ) const isProduction = process.env.NODE_ENV === 'production' @@ -15,11 +14,7 @@ export default function VerifyPage() {

Sim Studio

- +
) diff --git a/sim/app/(auth)/verify/use-verification.ts b/sim/app/(auth)/verify/use-verification.ts index 7bd6f47539..eb4726eefb 100644 --- a/sim/app/(auth)/verify/use-verification.ts +++ b/sim/app/(auth)/verify/use-verification.ts @@ -28,7 +28,10 @@ interface UseVerificationReturn { handleOtpChange: (value: string) => void } -export function useVerification({ hasResendKey, isProduction }: UseVerificationParams): UseVerificationReturn { +export function useVerification({ + hasResendKey, + isProduction, +}: UseVerificationParams): UseVerificationReturn { const router = useRouter() const searchParams = useSearchParams() const { addNotification } = useNotificationStore() @@ -172,6 +175,6 @@ export function useVerification({ hasResendKey, isProduction }: UseVerificationP isProduction, verifyCode, resendCode, - handleOtpChange + handleOtpChange, } -} \ No newline at end of file +} diff --git a/sim/app/(auth)/verify/verify-content.tsx b/sim/app/(auth)/verify/verify-content.tsx index ea0175e25e..87f778de08 100644 --- a/sim/app/(auth)/verify/verify-content.tsx +++ b/sim/app/(auth)/verify/verify-content.tsx @@ -20,7 +20,13 @@ interface VerifyContentProps { isProduction: boolean } -function VerificationForm({ hasResendKey, isProduction }: { hasResendKey: boolean, isProduction: boolean }) { +function VerificationForm({ + hasResendKey, + isProduction, +}: { + hasResendKey: boolean + isProduction: boolean +}) { const { otp, email, @@ -31,7 +37,7 @@ function VerificationForm({ hasResendKey, isProduction }: { hasResendKey: boolea isOtpComplete, verifyCode, resendCode, - handleOtpChange + handleOtpChange, } = useVerification({ hasResendKey, isProduction }) return ( @@ -124,9 +130,7 @@ function VerificationFormFallback() { return ( Loading verification... - - Please wait while we load your verification details... - + Please wait while we load your verification details... ) } diff --git a/sim/app/blocks/blocks/github.ts b/sim/app/blocks/blocks/github.ts index 0772801c1b..3d870daeac 100644 --- a/sim/app/blocks/blocks/github.ts +++ b/sim/app/blocks/blocks/github.ts @@ -1,8 +1,17 @@ import { GithubIcon } from '@/components/icons' -import { CreateCommentResponse, LatestCommitResponse, PullRequestResponse, RepoInfoResponse } from '@/tools/github/types' +import { + CreateCommentResponse, + LatestCommitResponse, + PullRequestResponse, + RepoInfoResponse, +} from '@/tools/github/types' import { BlockConfig } from '../types' -type GitHubResponse = PullRequestResponse | CreateCommentResponse | LatestCommitResponse | RepoInfoResponse +type GitHubResponse = + | PullRequestResponse + | CreateCommentResponse + | LatestCommitResponse + | RepoInfoResponse export const GitHubBlock: BlockConfig = { type: 'github', @@ -160,8 +169,8 @@ export const GitHubBlock: BlockConfig = { response: { type: { content: 'string', - metadata: 'json' - } - } + metadata: 'json', + }, + }, }, } diff --git a/sim/app/components/ui/command.tsx b/sim/app/components/ui/command.tsx index b06c89ee88..c73faee67d 100644 --- a/sim/app/components/ui/command.tsx +++ b/sim/app/components/ui/command.tsx @@ -36,6 +36,12 @@ import { cn } from '@/lib/utils' // This file is not typed correctly from shadcn, so we're disabling the type checker // @ts-nocheck +// This file is not typed correctly from shadcn, so we're disabling the type checker +// @ts-nocheck + +// This file is not typed correctly from shadcn, so we're disabling the type checker +// @ts-nocheck + const Command = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { diff --git a/sim/app/db/migrations/meta/0018_snapshot.json b/sim/app/db/migrations/meta/0018_snapshot.json index 31a18593cf..8e3a63c895 100644 --- a/sim/app/db/migrations/meta/0018_snapshot.json +++ b/sim/app/db/migrations/meta/0018_snapshot.json @@ -93,12 +93,8 @@ "name": "account_user_id_user_id_fk", "tableFrom": "account", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -170,12 +166,8 @@ "name": "api_key_user_id_user_id_fk", "tableFrom": "api_key", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -185,9 +177,7 @@ "api_key_key_unique": { "name": "api_key_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] } }, "policies": {}, @@ -230,12 +220,8 @@ "name": "environment_user_id_user_id_fk", "tableFrom": "environment", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -245,9 +231,7 @@ "environment_user_id_unique": { "name": "environment_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -341,12 +325,8 @@ "name": "marketplace_workflow_id_workflow_id_fk", "tableFrom": "marketplace", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -354,12 +334,8 @@ "name": "marketplace_author_id_user_id_fk", "tableFrom": "marketplace", "tableTo": "user", - "columnsFrom": [ - "author_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["author_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -406,12 +382,8 @@ "name": "marketplace_execution_marketplace_id_marketplace_id_fk", "tableFrom": "marketplace_execution", "tableTo": "marketplace", - "columnsFrom": [ - "marketplace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["marketplace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -419,12 +391,8 @@ "name": "marketplace_execution_user_id_user_id_fk", "tableFrom": "marketplace_execution", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -493,12 +461,8 @@ "name": "marketplace_star_marketplace_id_marketplace_id_fk", "tableFrom": "marketplace_star", "tableTo": "marketplace", - "columnsFrom": [ - "marketplace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["marketplace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -506,12 +470,8 @@ "name": "marketplace_star_user_id_user_id_fk", "tableFrom": "marketplace_star", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -581,12 +541,8 @@ "name": "session_user_id_user_id_fk", "tableFrom": "session", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -596,9 +552,7 @@ "session_token_unique": { "name": "session_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -641,12 +595,8 @@ "name": "settings_user_id_user_id_fk", "tableFrom": "settings", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -656,9 +606,7 @@ "settings_user_id_unique": { "name": "settings_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -719,9 +667,7 @@ "user_email_unique": { "name": "user_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -822,9 +768,7 @@ "waitlist_email_unique": { "name": "waitlist_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -909,12 +853,8 @@ "name": "webhook_workflow_id_workflow_id_fk", "tableFrom": "webhook", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1017,12 +957,8 @@ "name": "workflow_user_id_user_id_fk", "tableFrom": "workflow", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1099,12 +1035,8 @@ "name": "workflow_logs_workflow_id_workflow_id_fk", "tableFrom": "workflow_logs", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1176,12 +1108,8 @@ "name": "workflow_schedule_workflow_id_workflow_id_fk", "tableFrom": "workflow_schedule", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1191,9 +1119,7 @@ "workflow_schedule_workflow_id_unique": { "name": "workflow_schedule_workflow_id_unique", "nullsNotDistinct": false, - "columns": [ - "workflow_id" - ] + "columns": ["workflow_id"] } }, "policies": {}, @@ -1212,4 +1138,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/sim/app/db/migrations/meta/_journal.json b/sim/app/db/migrations/meta/_journal.json index 2086f1b437..c1c51fc70d 100644 --- a/sim/app/db/migrations/meta/_journal.json +++ b/sim/app/db/migrations/meta/_journal.json @@ -136,4 +136,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/sim/app/executor/handlers.ts b/sim/app/executor/handlers.ts index ef766428c7..a468381def 100644 --- a/sim/app/executor/handlers.ts +++ b/sim/app/executor/handlers.ts @@ -285,6 +285,7 @@ export class AgentBlockHandler implements BlockHandler { } : undefined, providerTiming: response.timing || undefined, + cost: response.cost || undefined, }, } @@ -318,6 +319,7 @@ export class AgentBlockHandler implements BlockHandler { count: response.toolCalls?.length || 0, }, providerTiming: response.timing || undefined, + cost: response.cost || undefined, }, } } @@ -348,6 +350,7 @@ export class AgentBlockHandler implements BlockHandler { count: response.toolCalls?.length || 0, }, providerTiming: response.timing || undefined, + cost: response.cost || undefined, }, } } @@ -804,14 +807,14 @@ export class ApiBlockHandler implements BlockHandler { if (!inputs.url.match(/^https?:\/\//i)) { throw new Error( `Invalid URL: "${inputs.url}" - URL must include protocol (try "https://${inputs.url}")` - ); + ) } - + // Detect other common URL issues try { - new URL(inputs.url); + new URL(inputs.url) } catch (e: any) { - throw new Error(`Invalid URL format: "${inputs.url}" - ${e.message}`); + throw new Error(`Invalid URL format: "${inputs.url}" - ${e.message}`) } } @@ -822,40 +825,43 @@ export class ApiBlockHandler implements BlockHandler { }) if (!result.success) { - const errorDetails = []; - + const errorDetails = [] + // Add request details to error message - if (inputs.url) errorDetails.push(`URL: ${inputs.url}`); - if (inputs.method) errorDetails.push(`Method: ${inputs.method}`); - + if (inputs.url) errorDetails.push(`URL: ${inputs.url}`) + if (inputs.method) errorDetails.push(`Method: ${inputs.method}`) + // Add response details - if (result.error) errorDetails.push(`Error: ${result.error}`); - if (result.output?.status) errorDetails.push(`Status: ${result.output.status}`); - if (result.output?.statusText) errorDetails.push(`Status text: ${result.output.statusText}`); - + if (result.error) errorDetails.push(`Error: ${result.error}`) + if (result.output?.status) errorDetails.push(`Status: ${result.output.status}`) + if (result.output?.statusText) errorDetails.push(`Status text: ${result.output.statusText}`) + // Add specific suggestions for common error codes - let suggestion = ''; + let suggestion = '' if (result.output?.status === 403) { - suggestion = ' - This may be due to CORS restrictions or authorization issues'; + suggestion = ' - This may be due to CORS restrictions or authorization issues' } else if (result.output?.status === 404) { - suggestion = ' - The requested resource was not found'; + suggestion = ' - The requested resource was not found' } else if (result.output?.status === 429) { - suggestion = ' - Too many requests, you may need to implement rate limiting'; + suggestion = ' - Too many requests, you may need to implement rate limiting' } else if (result.output?.status >= 500) { - suggestion = ' - Server error, the target server is experiencing issues'; + suggestion = ' - Server error, the target server is experiencing issues' } else if (result.error && result.error.includes('CORS')) { - suggestion = ' - CORS policy prevented the request, try using a proxy or server-side request'; + suggestion = + ' - CORS policy prevented the request, try using a proxy or server-side request' } else if (result.error && result.error.includes('Failed to fetch')) { - suggestion = ' - Network error, check if the URL is accessible and if you have internet connectivity'; + suggestion = + ' - Network error, check if the URL is accessible and if you have internet connectivity' } - - const errorMessage = errorDetails.length > 0 - ? `HTTP Request failed: ${errorDetails.join(' | ')}${suggestion}` - : `API request to ${tool.name || block.config.tool} failed with no error message`; - + + const errorMessage = + errorDetails.length > 0 + ? `HTTP Request failed: ${errorDetails.join(' | ')}${suggestion}` + : `API request to ${tool.name || block.config.tool} failed with no error message` + // Create a detailed error object with formatted message - const error = new Error(errorMessage); - + const error = new Error(errorMessage) + // Add additional properties for debugging Object.assign(error, { toolId: block.config.tool, @@ -868,47 +874,47 @@ export class ApiBlockHandler implements BlockHandler { url: inputs.url, method: inputs.method || 'GET', }, - timestamp: new Date().toISOString() - }); - - throw error; + timestamp: new Date().toISOString(), + }) + + throw error } return { response: result.output } } catch (error: any) { // Ensure we have a meaningful error message - if (!error.message || error.message === "undefined (undefined)") { + if (!error.message || error.message === 'undefined (undefined)') { // Construct a detailed error message with available information - let errorMessage = `API request to ${tool.name || block.config.tool} failed`; - + let errorMessage = `API request to ${tool.name || block.config.tool} failed` + // Add details if available - if (inputs.url) errorMessage += `: ${inputs.url}`; - if (error.status) errorMessage += ` (Status: ${error.status})`; - if (error.statusText) errorMessage += ` - ${error.statusText}`; - + if (inputs.url) errorMessage += `: ${inputs.url}` + if (error.status) errorMessage += ` (Status: ${error.status})` + if (error.statusText) errorMessage += ` - ${error.statusText}` + // If we still have no details, give a generic but helpful message if (errorMessage === `API request to ${tool.name || block.config.tool} failed`) { - errorMessage += ` - ${block.metadata?.name || 'Unknown error'}`; + errorMessage += ` - ${block.metadata?.name || 'Unknown error'}` } - - error.message = errorMessage; + + error.message = errorMessage } - + // Add additional context to the error if (typeof error === 'object' && error !== null) { - if (!error.toolId) error.toolId = block.config.tool; - if (!error.blockName) error.blockName = block.metadata?.name || 'Unnamed Block'; - + if (!error.toolId) error.toolId = block.config.tool + if (!error.blockName) error.blockName = block.metadata?.name || 'Unnamed Block' + // Add request details if missing if (inputs && !error.request) { error.request = { url: inputs.url, - method: inputs.method || 'GET' - }; + method: inputs.method || 'GET', + } } } - - throw error; + + throw error } } } @@ -973,18 +979,19 @@ export class GenericBlockHandler implements BlockHandler { ...inputs, _context: { workflowId: context.workflowId }, }) - + if (!result.success) { - const errorDetails = []; - if (result.error) errorDetails.push(result.error); - - const errorMessage = errorDetails.length > 0 - ? errorDetails.join(' - ') - : `Block execution of ${tool.name || block.config.tool} failed with no error message`; - + const errorDetails = [] + if (result.error) errorDetails.push(result.error) + + const errorMessage = + errorDetails.length > 0 + ? errorDetails.join(' - ') + : `Block execution of ${tool.name || block.config.tool} failed with no error message` + // Create a detailed error object with formatted message - const error = new Error(errorMessage); - + const error = new Error(errorMessage) + // Add additional properties for debugging Object.assign(error, { toolId: block.config.tool, @@ -992,39 +999,39 @@ export class GenericBlockHandler implements BlockHandler { blockId: block.id, blockName: block.metadata?.name || 'Unnamed Block', output: result.output || {}, - timestamp: new Date().toISOString() - }); - - throw error; + timestamp: new Date().toISOString(), + }) + + throw error } return { response: result.output } } catch (error: any) { // Ensure we have a meaningful error message - if (!error.message || error.message === "undefined (undefined)") { + if (!error.message || error.message === 'undefined (undefined)') { // Construct a detailed error message with available information - let errorMessage = `Block execution of ${tool.name || block.config.tool} failed`; - + let errorMessage = `Block execution of ${tool.name || block.config.tool} failed` + // Add block name if available if (block.metadata?.name) { - errorMessage += `: ${block.metadata.name}`; + errorMessage += `: ${block.metadata.name}` } - + // Add status code if available if (error.status) { - errorMessage += ` (Status: ${error.status})`; + errorMessage += ` (Status: ${error.status})` } - - error.message = errorMessage; + + error.message = errorMessage } - + // Add additional context to the error if (typeof error === 'object' && error !== null) { - if (!error.toolId) error.toolId = block.config.tool; - if (!error.blockName) error.blockName = block.metadata?.name || 'Unnamed Block'; + if (!error.toolId) error.toolId = block.config.tool + if (!error.blockName) error.blockName = block.metadata?.name || 'Unnamed Block' } - - throw error; + + throw error } } } diff --git a/sim/app/executor/index.ts b/sim/app/executor/index.ts index 9f9124e0b3..1b9afbcba1 100644 --- a/sim/app/executor/index.ts +++ b/sim/app/executor/index.ts @@ -442,7 +442,9 @@ export class Executor { return output } catch (error: any) { blockLog.success = false - blockLog.error = error.message || `Error executing ${block.metadata?.id || 'unknown'} block: ${String(error)}` + blockLog.error = + error.message || + `Error executing ${block.metadata?.id || 'unknown'} block: ${String(error)}` blockLog.endedAt = new Date().toISOString() blockLog.durationMs = new Date(blockLog.endedAt).getTime() - new Date(blockLog.startedAt).getTime() @@ -450,7 +452,9 @@ export class Executor { context.blockLogs.push(blockLog) addConsole({ output: {}, - error: error.message || `Error executing ${block.metadata?.id || 'unknown'} block: ${String(error)}`, + error: + error.message || + `Error executing ${block.metadata?.id || 'unknown'} block: ${String(error)}`, durationMs: blockLog.durationMs, startedAt: blockLog.startedAt, endedAt: blockLog.endedAt, @@ -461,21 +465,21 @@ export class Executor { }) // Create a proper error message that is never undefined - let errorMessage = error.message; - + let errorMessage = error.message + // Handle the specific "undefined (undefined)" case - if (!errorMessage || errorMessage === "undefined (undefined)") { - errorMessage = `Error executing ${block.metadata?.id || 'unknown'} block: ${block.metadata?.name || 'Unnamed Block'}`; - + if (!errorMessage || errorMessage === 'undefined (undefined)') { + errorMessage = `Error executing ${block.metadata?.id || 'unknown'} block: ${block.metadata?.name || 'Unnamed Block'}` + // Try to get more details if possible if (error && typeof error === 'object') { - if (error.code) errorMessage += ` (code: ${error.code})`; - if (error.status) errorMessage += ` (status: ${error.status})`; - if (error.type) errorMessage += ` (type: ${error.type})`; + if (error.code) errorMessage += ` (code: ${error.code})` + if (error.status) errorMessage += ` (status: ${error.status})` + if (error.type) errorMessage += ` (type: ${error.type})` } } - - throw new Error(errorMessage); + + throw new Error(errorMessage) } } @@ -609,114 +613,114 @@ export class Executor { /** * Extracts a meaningful error message from any error object structure. * Handles nested error objects, undefined messages, and various error formats. - * + * * @param error - The error object to extract a message from * @returns A meaningful error message string */ private extractErrorMessage(error: any): string { - if (!error) return 'Unknown error occurred'; - + if (!error) return 'Unknown error occurred' + // Handle Error instances if (error instanceof Error) { - return error.message || `Error: ${String(error)}`; + return error.message || `Error: ${String(error)}` } - + // Handle string errors if (typeof error === 'string') { - return error; + return error } - + // Handle object errors with nested structure if (typeof error === 'object') { // Case: { error: { message: "msg" } } if (error.error && typeof error.error === 'object' && error.error.message) { - return error.error.message; + return error.error.message } - + // Case: { error: "msg" } if (error.error && typeof error.error === 'string') { - return error.error; + return error.error } - + // Case: { message: "msg" } if (error.message) { - return error.message; + return error.message } - + // Add specific handling for HTTP errors if (error.status || error.request) { - let message = 'API request failed'; - + let message = 'API request failed' + // Add URL information if available if (error.request && error.request.url) { - message += `: ${error.request.url}`; + message += `: ${error.request.url}` } - + // Add status code if available if (error.status) { - message += ` (Status: ${error.status})`; + message += ` (Status: ${error.status})` } - - return message; + + return message } - + // Last resort: try to stringify the object try { - return `Error details: ${JSON.stringify(error)}`; + return `Error details: ${JSON.stringify(error)}` } catch { - return 'Error occurred but details could not be displayed'; + return 'Error occurred but details could not be displayed' } } - - return 'Unknown error occurred'; + + return 'Unknown error occurred' } - + /** * Sanitizes an error object for logging purposes. * Ensures the error is in a format that won't cause "undefined" to appear in logs. - * + * * @param error - The error object to sanitize * @returns A sanitized version of the error for logging */ private sanitizeError(error: any): any { - if (!error) return { message: 'No error details available' }; - + if (!error) return { message: 'No error details available' } + // Handle Error instances if (error instanceof Error) { return { message: error.message || 'Error without message', - stack: error.stack - }; + stack: error.stack, + } } - + // Handle string errors if (typeof error === 'string') { - return { message: error }; + return { message: error } } - + // Handle object errors with nested structure if (typeof error === 'object') { // If error has a nested error object with undefined message, fix it if (error.error && typeof error.error === 'object') { if (!error.error.message) { - error.error.message = 'No specific error message provided'; + error.error.message = 'No specific error message provided' } } - + // If no message property exists at root level, add one if (!error.message) { if (error.error && typeof error.error === 'string') { - error.message = error.error; + error.message = error.error } else if (error.status) { - error.message = `API request failed with status ${error.status}`; + error.message = `API request failed with status ${error.status}` } else { - error.message = 'Error occurred during workflow execution'; + error.message = 'Error occurred during workflow execution' } } - - return error; + + return error } - - return { message: `Unexpected error type: ${typeof error}` }; + + return { message: `Unexpected error type: ${typeof error}` } } } diff --git a/sim/app/executor/resolver.ts b/sim/app/executor/resolver.ts index c28d893e7a..40b51b5e65 100644 --- a/sim/app/executor/resolver.ts +++ b/sim/app/executor/resolver.ts @@ -63,10 +63,11 @@ export class InputResolver { let resolvedValue = this.resolveBlockReferences(value, context, block) // Check if this is an API key field - const isApiKey = key.toLowerCase().includes('apikey') || - key.toLowerCase().includes('secret') || - key.toLowerCase().includes('token') - + const isApiKey = + key.toLowerCase().includes('apikey') || + key.toLowerCase().includes('secret') || + key.toLowerCase().includes('token') + // Resolve environment variables resolvedValue = this.resolveEnvVariables(resolvedValue, isApiKey) @@ -249,18 +250,18 @@ export class InputResolver { * Valid references are either: * 1. A standalone env var (entire string is just {{ENV_VAR}}) * 2. An explicit env var with clear boundaries (usually within a URL or similar) - * + * * @param value - The string to check * @returns Whether this contains a properly formatted env var reference */ private containsProperEnvVarReference(value: string): boolean { if (!value || typeof value !== 'string') return false - + // Case 1: String is just a single environment variable if (value.trim().match(/^\{\{[^{}]+\}\}$/)) { return true } - + // Case 2: Check for environment variables in specific contexts // For example, in URLs, bearer tokens, etc. const properContextPatterns = [ @@ -268,18 +269,18 @@ export class InputResolver { /Bearer\s+\{\{[^{}]+\}\}/i, /Authorization:\s+Bearer\s+\{\{[^{}]+\}\}/i, /Authorization:\s+\{\{[^{}]+\}\}/i, - + // API key in URL patterns /[?&]api[_-]?key=\{\{[^{}]+\}\}/i, /[?&]key=\{\{[^{}]+\}\}/i, /[?&]token=\{\{[^{}]+\}\}/i, - + // API key in header patterns /X-API-Key:\s+\{\{[^{}]+\}\}/i, - /api[_-]?key:\s+\{\{[^{}]+\}\}/i + /api[_-]?key:\s+\{\{[^{}]+\}\}/i, ] - - return properContextPatterns.some(pattern => pattern.test(value)) + + return properContextPatterns.some((pattern) => pattern.test(value)) } /** @@ -299,7 +300,7 @@ export class InputResolver { // 3. String contains environment variable references in proper contexts (auth headers, URLs) const isExplicitEnvVar = value.trim().startsWith('{{') && value.trim().endsWith('}}') const hasProperEnvVarReferences = this.containsProperEnvVarReference(value) - + if (isApiKey || isExplicitEnvVar || hasProperEnvVarReferences) { const envMatches = value.match(/\{\{([^}]+)\}\}/g) if (envMatches) { @@ -326,7 +327,10 @@ export class InputResolver { if (value && typeof value === 'object') { return Object.entries(value).reduce( - (acc, [k, v]) => ({ ...acc, [k]: this.resolveEnvVariables(v, k.toLowerCase() === 'apikey') }), + (acc, [k, v]) => ({ + ...acc, + [k]: this.resolveEnvVariables(v, k.toLowerCase() === 'apikey'), + }), {} ) } @@ -357,10 +361,10 @@ export class InputResolver { if (typeof value === 'string') { // First resolve block references const resolvedReferences = this.resolveBlockReferences(value, context, currentBlock) - + // Check if this is an API key field const isApiKey = this.isApiKeyField(currentBlock, value) - + // Then resolve environment variables with the API key flag return this.resolveEnvVariables(resolvedReferences, isApiKey) } @@ -386,7 +390,7 @@ export class InputResolver { /** * Determines if a given field in a block is an API key field. - * + * * @param block - Block containing the field * @param value - Value to check * @returns Whether this appears to be an API key field @@ -397,22 +401,22 @@ export class InputResolver { if (blockType !== 'api' && blockType !== 'agent') { return false } - + // Look for the value in the block params for (const [key, paramValue] of Object.entries(block.config.params)) { if (paramValue === value) { // Check if key name suggests it's an API key const normalizedKey = key.toLowerCase().replace(/[_\-\s]/g, '') return ( - normalizedKey === 'apikey' || - normalizedKey.includes('apikey') || + normalizedKey === 'apikey' || + normalizedKey.includes('apikey') || normalizedKey.includes('secretkey') || normalizedKey.includes('accesskey') || normalizedKey.includes('token') ) } } - + return false } diff --git a/sim/app/lib/logs/execution-logger.ts b/sim/app/lib/logs/execution-logger.ts index 291947ab65..735f887960 100644 --- a/sim/app/lib/logs/execution-logger.ts +++ b/sim/app/lib/logs/execution-logger.ts @@ -21,6 +21,23 @@ export interface LogEntry { // Define types for tool call tracking export interface ToolCallMetadata { toolCalls?: ToolCall[] + cost?: { + model?: string + input?: number + output?: number + total?: number + tokens?: { + prompt?: number + completion?: number + total?: number + } + pricing?: { + input: number + output: number + cachedInput?: number + updatedAt: string + } + } } export interface ToolCall { @@ -52,6 +69,16 @@ export async function persistExecutionLogs( triggerType: 'api' | 'webhook' | 'schedule' | 'manual' ) { try { + // Track accumulated cost data across all agent blocks + let totalCost = 0 + let totalInputCost = 0 + let totalOutputCost = 0 + let totalPromptTokens = 0 + let totalCompletionTokens = 0 + let totalTokens = 0 + let modelCounts: Record = {} + let primaryModel = '' + // Log each execution step for (const log of result.logs || []) { // Check for agent block and tool calls @@ -68,7 +95,64 @@ export async function persistExecutionLogs( hasResponse: !!log.output.response, }) - // Extract tool calls from different possible structures + // Extract tool calls and other metadata + if (log.output.response) { + const response = log.output.response + + // Process tool calls + if (response.toolCalls && response.toolCalls.list) { + metadata = { + toolCalls: response.toolCalls.list.map((tc: any) => ({ + name: tc.name, + duration: tc.duration || 0, + startTime: tc.startTime || new Date().toISOString(), + endTime: tc.endTime || new Date().toISOString(), + status: tc.error ? 'error' : 'success', + input: tc.input || tc.arguments, + output: tc.output || tc.result, + error: tc.error, + })), + } + } + + // Add cost information if available + if (response.cost) { + if (!metadata) metadata = {} + metadata.cost = { + model: response.model, + input: response.cost.input, + output: response.cost.output, + total: response.cost.total, + tokens: response.tokens, + pricing: response.cost.pricing, + } + + // Accumulate costs for workflow-level summary + if (response.cost.total) { + totalCost += response.cost.total + totalInputCost += response.cost.input || 0 + totalOutputCost += response.cost.output || 0 + + // Track tokens + if (response.tokens) { + totalPromptTokens += response.tokens.prompt || 0 + totalCompletionTokens += response.tokens.completion || 0 + totalTokens += response.tokens.total || 0 + } + + // Track model usage + if (response.model) { + modelCounts[response.model] = (modelCounts[response.model] || 0) + 1 + // Set the most frequently used model as primary + if (!primaryModel || modelCounts[response.model] > modelCounts[primaryModel]) { + primaryModel = response.model + } + } + } + } + } + + // Extract timing info - try various formats that providers might use const blockStartTime = log.startedAt const blockEndTime = log.endedAt || new Date().toISOString() const blockDuration = log.durationMs || 0 @@ -397,6 +481,47 @@ export async function persistExecutionLogs( const successMessage = getTriggerSuccessMessage(triggerType) const errorPrefix = getTriggerErrorPrefix(triggerType) + // Create workflow-level metadata with aggregated cost information + const workflowMetadata: any = { + traceSpans: (result as any).traceSpans || [], + totalDuration: (result as any).totalDuration || totalDuration, + } + + // Add accumulated cost data to workflow-level log + if (totalCost > 0) { + workflowMetadata.cost = { + model: primaryModel, + input: totalInputCost, + output: totalOutputCost, + total: totalCost, + tokens: { + prompt: totalPromptTokens, + completion: totalCompletionTokens, + total: totalTokens, + }, + } + + // Include pricing info if we have a model + if (primaryModel && result.logs && result.logs.length > 0) { + // Find the first agent log with pricing info + for (const log of result.logs) { + if (log.output?.response?.cost?.pricing) { + workflowMetadata.cost.pricing = log.output.response.cost.pricing + break + } + } + } + + logger.info(`Workflow execution total cost: ${totalCost}`, { + workflowId, + executionId, + totalCost, + inputCost: totalInputCost, + outputCost: totalOutputCost, + models: Object.keys(modelCounts), + }) + } + // Log the final execution result await persistLog({ id: uuidv4(), @@ -407,10 +532,7 @@ export async function persistExecutionLogs( duration: result.success ? `${totalDuration}ms` : 'NA', trigger: triggerType, createdAt: new Date(), - metadata: { - traceSpans: (result as any).traceSpans || [], - totalDuration: (result as any).totalDuration || totalDuration, - }, + metadata: workflowMetadata, }) } catch (error: any) { logger.error(`Error persisting execution logs: ${error.message}`, { diff --git a/sim/app/providers/index.ts b/sim/app/providers/index.ts index 9d08cc60ff..a67d87c4f7 100644 --- a/sim/app/providers/index.ts +++ b/sim/app/providers/index.ts @@ -1,7 +1,7 @@ import { createLogger } from '@/lib/logs/console-logger' import { supportsTemperature } from './model-capabilities' import { ProviderRequest, ProviderResponse } from './types' -import { generateStructuredOutputInstructions, getProvider } from './utils' +import { calculateCost, generateStructuredOutputInstructions, getProvider } from './utils' const logger = createLogger('Providers') @@ -74,5 +74,13 @@ export async function executeProviderRequest( toolCallsCount: response.toolCalls?.length || 0, }) + // Calculate cost based on token usage if tokens are available + if (response.tokens) { + const { prompt: promptTokens = 0, completion: completionTokens = 0 } = response.tokens + const useCachedInput = !!request.context && request.context.length > 0 + + response.cost = calculateCost(response.model, promptTokens, completionTokens, useCachedInput) + } + return response } diff --git a/sim/app/providers/pricing.ts b/sim/app/providers/pricing.ts new file mode 100644 index 0000000000..7cdd7f8850 --- /dev/null +++ b/sim/app/providers/pricing.ts @@ -0,0 +1,125 @@ +import { ModelPricingMap } from './types' + +/** + * Model pricing information per million tokens + * + * Prices are in USD per 1M tokens + * All prices should be regularly updated to reflect current market rates + */ +const modelPricing: ModelPricingMap = { + // OpenAI Models + 'gpt-4o': { + input: 2.5, + cachedInput: 1.25, // 50% discount for cached input + output: 10.0, + updatedAt: '2025-03-21', + }, + o1: { + input: 15.0, + cachedInput: 7.5, // 50% discount for cached input + output: 75.0, + updatedAt: '2025-03-21', + }, + 'o3-mini': { + input: 0.5, + cachedInput: 0.25, + output: 1.5, + updatedAt: '2025-03-21', + }, + + // Anthropic Models + 'claude-3-7-sonnet-20250219': { + input: 3.0, + cachedInput: 1.5, + output: 15.0, + updatedAt: '2025-03-21', + }, + + // Google Models + 'gemini-2.0-flash': { + input: 0.1, + cachedInput: 0.05, + output: 0.4, + updatedAt: '2025-03-21', + }, + + // Deepseek Models + 'deepseek-v3': { + input: 0.75, + cachedInput: 0.4, + output: 1.0, + updatedAt: '2025-03-21', + }, + 'deepseek-r1': { + input: 1.0, + cachedInput: 0.5, + output: 1.5, + updatedAt: '2025-03-21', + }, + + // xAI Models + 'grok-2-latest': { + input: 2.0, + cachedInput: 1.0, + output: 10.0, + updatedAt: '2025-03-21', + }, + + // Cerebras Models + 'cerebras/llama-3.3-70b': { + input: 0.94, + cachedInput: 0.47, + output: 0.94, + updatedAt: '2025-03-21', + }, + + // Groq Models + 'groq/llama-3.3-70b-specdec': { + input: 0.59, + cachedInput: 0.3, + output: 0.99, + updatedAt: '2025-03-21', + }, + 'groq/deepseek-r1-distill-llama-70b': { + input: 0.75, + cachedInput: 0.38, + output: 0.99, + updatedAt: '2025-03-21', + }, + 'groq/qwen-2.5-32b': { + input: 0.79, + cachedInput: 0.4, + output: 0.79, + updatedAt: '2025-03-21', + }, +} + +/** + * Get pricing for a specific model + * Returns default pricing if model not found + */ +export function getModelPricing(model: string) { + const normalizedModel = model.toLowerCase() + + // Exact match + if (normalizedModel in modelPricing) { + return modelPricing[normalizedModel] + } + + // Partial match (for models with prefixes/versions) + for (const [pricingModel, pricing] of Object.entries(modelPricing)) { + if (normalizedModel.includes(pricingModel.toLowerCase())) { + return pricing + } + } + + // Default pricing if model not found + return { + input: 1.0, + cachedInput: 0.5, + output: 5.0, + updatedAt: '2025-03-21', + } +} + +export default modelPricing diff --git a/sim/app/providers/types.ts b/sim/app/providers/types.ts index 787997ecbe..a541174596 100644 --- a/sim/app/providers/types.ts +++ b/sim/app/providers/types.ts @@ -7,6 +7,21 @@ export type ProviderId = | 'cerebras' | 'groq' +/** + * Model pricing information per million tokens + */ +export interface ModelPricing { + input: number // Cost per million tokens for input + cachedInput?: number // Cost per million tokens for cached input (optional) + output: number // Cost per million tokens for output + updatedAt: string // ISO timestamp when pricing was last updated +} + +/** + * Map of model IDs to their pricing information + */ +export type ModelPricingMap = Record + export interface TokenInfo { prompt?: number completion?: number @@ -67,6 +82,12 @@ export interface ProviderResponse { iterations?: number // Number of model calls for tool use timeSegments?: TimeSegment[] // Detailed timeline of all operations } + cost?: { + input: number // Cost in USD for input tokens + output: number // Cost in USD for output tokens + total: number // Total cost in USD + pricing: ModelPricing // The pricing used for calculation + } } export interface ProviderToolConfig { diff --git a/sim/app/providers/utils.ts b/sim/app/providers/utils.ts index c4845e84d5..303d9c8b5b 100644 --- a/sim/app/providers/utils.ts +++ b/sim/app/providers/utils.ts @@ -6,6 +6,7 @@ import { deepseekProvider } from './deepseek' import { googleProvider } from './google' import { groqProvider } from './groq' import { openaiProvider } from './openai' +import { getModelPricing } from './pricing' import { ProviderConfig, ProviderId, ProviderToolConfig } from './types' import { xAIProvider } from './xai' @@ -349,3 +350,67 @@ export function transformBlockTool( }, } } + +/** + * Calculate cost for token usage based on model pricing + * + * @param model The model name + * @param promptTokens Number of prompt tokens used + * @param completionTokens Number of completion tokens used + * @param useCachedInput Whether to use cached input pricing (default: false) + * @returns Cost calculation results with input, output and total costs + */ +export function calculateCost( + model: string, + promptTokens: number = 0, + completionTokens: number = 0, + useCachedInput: boolean = false +) { + const pricing = getModelPricing(model) + + // Calculate costs in USD + // Convert from "per million tokens" to "per token" by dividing by 1,000,000 + const inputCost = + promptTokens * + (useCachedInput && pricing.cachedInput + ? pricing.cachedInput / 1_000_000 + : pricing.input / 1_000_000) + + const outputCost = completionTokens * (pricing.output / 1_000_000) + const totalCost = inputCost + outputCost + + return { + input: parseFloat(inputCost.toFixed(6)), + output: parseFloat(outputCost.toFixed(6)), + total: parseFloat(totalCost.toFixed(6)), + pricing, + } +} + +/** + * Format cost as a currency string + * + * @param cost Cost in USD + * @returns Formatted cost string + */ +export function formatCost(cost: number): string { + if (cost === undefined || cost === null) return '—' + + if (cost >= 1) { + // For costs >= $1, show two decimal places + return `$${cost.toFixed(2)}` + } else if (cost >= 0.01) { + // For costs between 1¢ and $1, show three decimal places + return `$${cost.toFixed(3)}` + } else if (cost >= 0.001) { + // For costs between 0.1¢ and 1¢, show four decimal places + return `$${cost.toFixed(4)}` + } else if (cost > 0) { + // For very small costs, still show as fixed decimal instead of scientific notation + // Find the first non-zero digit and show a few more places + const places = Math.max(4, Math.abs(Math.floor(Math.log10(cost))) + 3) + return `$${cost.toFixed(places)}` + } else { + return '$0' + } +} diff --git a/sim/app/stores/workflows/workflow/store.ts b/sim/app/stores/workflows/workflow/store.ts index d51e15451f..aeb30b735d 100644 --- a/sim/app/stores/workflows/workflow/store.ts +++ b/sim/app/stores/workflows/workflow/store.ts @@ -426,7 +426,7 @@ export const useWorkflowStore = create()( // workflowValues: {[block_id]:{[subblock_id]:[subblock_value]}} const workflowValues = subBlockStore.workflowValues[activeWorkflowId] || {} const updatedWorkflowValues = { ...workflowValues } - + // Loop through blocks Object.entries(workflowValues).forEach(([blockId, blockValues]) => { if (blockId === id) return // Skip the block being renamed @@ -438,7 +438,11 @@ export const useWorkflowStore = create()( const regex = new RegExp(`<${oldBlockName}\\.`, 'g') // Use a recursive function to handle all object types - updatedWorkflowValues[blockId][subBlockId] = updateReferences(value, regex, `<${newBlockName}.`) + updatedWorkflowValues[blockId][subBlockId] = updateReferences( + value, + regex, + `<${newBlockName}.` + ) // Helper function to recursively update references in any data structure function updateReferences(value: any, regex: RegExp, replacement: string): any { @@ -446,12 +450,12 @@ export const useWorkflowStore = create()( if (typeof value === 'string') { return regex.test(value) ? value.replace(regex, replacement) : value } - + // Handle arrays if (Array.isArray(value)) { - return value.map(item => updateReferences(item, regex, replacement)) + return value.map((item) => updateReferences(item, regex, replacement)) } - + // Handle objects if (value !== null && typeof value === 'object') { const result = { ...value } @@ -460,7 +464,7 @@ export const useWorkflowStore = create()( } return result } - + // Return unchanged for other types return value } diff --git a/sim/app/tools/github/comment.ts b/sim/app/tools/github/comment.ts index 7fb44ce636..a214243a9f 100644 --- a/sim/app/tools/github/comment.ts +++ b/sim/app/tools/github/comment.ts @@ -100,10 +100,10 @@ export const commentTool: ToolConfig transformResponse: async (response) => { const data = await response.json() - + // Create a human-readable content string const content = `Comment created: "${data.body}"` - + return { success: true, output: { diff --git a/sim/app/tools/github/commit.ts b/sim/app/tools/github/commit.ts index d55cdcb837..636fc96cea 100644 --- a/sim/app/tools/github/commit.ts +++ b/sim/app/tools/github/commit.ts @@ -21,7 +21,7 @@ export const latestCommitTool: ToolConfig { const baseUrl = `https://api.github.com/repos/${params.owner}/${params.repo}` - return params.branch - ? `${baseUrl}/commits/${params.branch}` - : `${baseUrl}/commits/HEAD` + return params.branch ? `${baseUrl}/commits/${params.branch}` : `${baseUrl}/commits/HEAD` }, method: 'GET', headers: (params) => ({ @@ -52,7 +50,7 @@ export const latestCommitTool: ToolConfig 0 ? fileDetailsWithContent : undefined, }, }, @@ -141,4 +141,4 @@ export const latestCommitTool: ToolConfig { tester = new ToolTester(repoInfoTool) - + // Add HTML URL to the mock response for our content string mockGitHubResponses.repoInfo.html_url = 'https://github.com/testuser/test-repo' }) @@ -177,7 +177,7 @@ URL: https://github.com/testuser/test-repo` // Mock the transformError function to return the specific error message we're testing for const originalTransformError = repoInfoTool.transformError - repoInfoTool.transformError = () => + repoInfoTool.transformError = () => 'Repository not found. Please check the owner and repository name.' // Execute the tool @@ -203,8 +203,7 @@ URL: https://github.com/testuser/test-repo` // Mock the transformError function to return the specific error message we're testing for const originalTransformError = repoInfoTool.transformError - repoInfoTool.transformError = () => - 'Authentication failed. Please check your GitHub token.' + repoInfoTool.transformError = () => 'Authentication failed. Please check your GitHub token.' // Execute the tool const result = await tester.execute({ diff --git a/sim/app/tools/index.ts b/sim/app/tools/index.ts index aa08ff25bc..7f52f8885a 100644 --- a/sim/app/tools/index.ts +++ b/sim/app/tools/index.ts @@ -6,7 +6,12 @@ import { driveDownloadTool, driveListTool, driveUploadTool } from './drive' import { exaAnswerTool, exaFindSimilarLinksTool, exaGetContentsTool, exaSearchTool } from './exa' import { scrapeTool } from './firecrawl/scrape' import { functionExecuteTool, webcontainerExecuteTool } from './function' -import { githubCommentTool, githubLatestCommitTool, githubPrTool, githubRepoInfoTool } from './github' +import { + githubCommentTool, + githubLatestCommitTool, + githubPrTool, + githubRepoInfoTool, +} from './github' import { gmailReadTool, gmailSearchTool, gmailSendTool } from './gmail' import { guestyGuestTool, guestyReservationTool } from './guesty' import { requestTool as httpRequest } from './http/request' @@ -453,53 +458,56 @@ export async function executeTool( } // Process the error to ensure we have a useful message - let errorMessage = 'Unknown error occurred'; - let errorDetails = {}; - + let errorMessage = 'Unknown error occurred' + let errorDetails = {} + if (error instanceof Error) { - errorMessage = error.message || `Error executing tool ${toolId}`; + errorMessage = error.message || `Error executing tool ${toolId}` } else if (typeof error === 'string') { - errorMessage = error; + errorMessage = error } else if (error && typeof error === 'object') { // Handle API response errors if (error.response) { - const response = error.response; - errorMessage = `API Error: ${response.statusText || response.status || 'Unknown status'}`; - + const response = error.response + errorMessage = `API Error: ${response.statusText || response.status || 'Unknown status'}` + // Try to extract more details from the response if (response.data) { if (typeof response.data === 'string') { - errorMessage = `${errorMessage} - ${response.data}`; + errorMessage = `${errorMessage} - ${response.data}` } else if (response.data.message) { - errorMessage = `${errorMessage} - ${response.data.message}`; + errorMessage = `${errorMessage} - ${response.data.message}` } else if (response.data.error) { - errorMessage = `${errorMessage} - ${typeof response.data.error === 'string' ? - response.data.error : JSON.stringify(response.data.error)}`; + errorMessage = `${errorMessage} - ${ + typeof response.data.error === 'string' + ? response.data.error + : JSON.stringify(response.data.error) + }` } } - + // Include useful debugging information errorDetails = { status: response.status, statusText: response.statusText, - data: response.data - }; - } + data: response.data, + } + } // Handle fetch or other network errors else if (error.message) { // Don't pass along "undefined (undefined)" messages - if (error.message === "undefined (undefined)") { - errorMessage = `Error executing tool ${toolId}`; + if (error.message === 'undefined (undefined)') { + errorMessage = `Error executing tool ${toolId}` // Add status if available if (error.status) { - errorMessage += ` (Status: ${error.status})`; + errorMessage += ` (Status: ${error.status})` } } else { - errorMessage = error.message; + errorMessage = error.message } - + if (error.cause) { - errorMessage = `${errorMessage} (${error.cause})`; + errorMessage = `${errorMessage} (${error.cause})` } } } @@ -757,61 +765,63 @@ async function handleProxyRequest( method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ toolId, params }), - }); + }) if (!response.ok) { - const errorText = await response.text(); - let errorMessage = `HTTP error ${response.status}: ${response.statusText}`; - let errorDetails = { status: response.status, statusText: response.statusText }; - + const errorText = await response.text() + let errorMessage = `HTTP error ${response.status}: ${response.statusText}` + let errorDetails = { status: response.status, statusText: response.statusText } + try { // Try to parse as JSON for more details - const errorJson = JSON.parse(errorText); + const errorJson = JSON.parse(errorText) if (errorJson.error) { - errorMessage = typeof errorJson.error === 'string' ? - errorJson.error : - `API Error: ${response.status} ${response.statusText}`; + errorMessage = + typeof errorJson.error === 'string' + ? errorJson.error + : `API Error: ${response.status} ${response.statusText}` } - errorDetails = { ...errorDetails, ...errorJson }; + errorDetails = { ...errorDetails, ...errorJson } } catch { // If not JSON, use the raw text - if (errorText && errorText !== "undefined (undefined)") { - errorMessage = `${errorMessage} - ${errorText}`; + if (errorText && errorText !== 'undefined (undefined)') { + errorMessage = `${errorMessage} - ${errorText}` } } - + return { success: false, output: errorDetails, error: errorMessage, - }; + } } - const result = await response.json(); + const result = await response.json() if (!result.success) { return { success: false, output: result.output || {}, error: result.error || `API request to ${toolId} failed with no error message`, - }; + } } - return result; + return result } catch (error: any) { // Handle network or other fetch errors - logger.error(`Error in proxy request for tool ${toolId}:`, { error }); - - let errorMessage = error instanceof Error ? - error.message : - typeof error === 'string' ? - error : - `Unknown error in API request to ${toolId}`; - + logger.error(`Error in proxy request for tool ${toolId}:`, { error }) + + let errorMessage = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : `Unknown error in API request to ${toolId}` + return { success: false, output: { originalError: error }, error: errorMessage, - }; + } } } diff --git a/sim/app/w/[id]/hooks/use-workflow-execution.ts b/sim/app/w/[id]/hooks/use-workflow-execution.ts index 4a0293478a..577df7e4ba 100644 --- a/sim/app/w/[id]/hooks/use-workflow-execution.ts +++ b/sim/app/w/[id]/hooks/use-workflow-execution.ts @@ -352,16 +352,20 @@ export function useWorkflowExecution() { // Properly extract error message ensuring it's never undefined let errorMessage = 'Unknown error' - + if (error instanceof Error) { errorMessage = error.message || `Error: ${String(error)}` } else if (typeof error === 'string') { errorMessage = error } else if (error && typeof error === 'object') { // Fix the "undefined (undefined)" pattern specifically - if (error.message === 'undefined (undefined)' || - (error.error && typeof error.error === 'object' && error.error.message === 'undefined (undefined)')) { - errorMessage = 'API request failed - no specific error details available'; + if ( + error.message === 'undefined (undefined)' || + (error.error && + typeof error.error === 'object' && + error.error.message === 'undefined (undefined)') + ) { + errorMessage = 'API request failed - no specific error details available' } // Try to extract error details from potential API or execution errors else if (error.message) { @@ -382,7 +386,7 @@ export function useWorkflowExecution() { // Ensure errorMessage is never "undefined (undefined)" if (errorMessage === 'undefined (undefined)') { - errorMessage = 'API request failed - no specific error details available'; + errorMessage = 'API request failed - no specific error details available' } // Set error result and show notification immediately @@ -394,26 +398,26 @@ export function useWorkflowExecution() { } setExecutionResult(errorResult) - + // Create a more user-friendly notification message - let notificationMessage = `Workflow execution failed`; - + let notificationMessage = `Workflow execution failed` + // Add URL for HTTP errors if (error && error.request && error.request.url) { // Don't show empty URL errors if (error.request.url && error.request.url.trim() !== '') { - notificationMessage += `: Request to ${error.request.url} failed`; - + notificationMessage += `: Request to ${error.request.url} failed` + // Add status if available if (error.status) { - notificationMessage += ` (Status: ${error.status})`; + notificationMessage += ` (Status: ${error.status})` } } } else { // Regular errors - notificationMessage += `: ${errorMessage}`; + notificationMessage += `: ${errorMessage}` } - + addNotification('error', notificationMessage, activeWorkflowId) // Also send the error result to the API diff --git a/sim/app/w/agents/stores/store.ts b/sim/app/w/agents/stores/store.ts index 1b750ff0a4..3bfbebc76a 100644 --- a/sim/app/w/agents/stores/store.ts +++ b/sim/app/w/agents/stores/store.ts @@ -1,25 +1,24 @@ /** * Agent Builder Zustand Store - * + * * Central state management for the Agent Builder application: * - Maintains collections of agents, MCP servers, and chat sessions * - Handles CRUD operations for all entities * - Manages UI state including selections, filters, and sorting * - Provides actions for modifying state * - Implements filtering and sorting logic - * + * * @module AgentStore */ - import { create } from 'zustand' -import { AgentState, Agent, MCPServer, ChatSession, ChatMessage } from './types' +import { Agent, AgentState, ChatMessage, ChatSession, MCPServer } from './types' export const useAgentStore = create((set, get) => ({ // Collections agents: [], mcpServers: [], chatSessions: [], - + // UI state selectedAgentId: null, selectedSessionId: null, @@ -29,162 +28,163 @@ export const useAgentStore = create((set, get) => ({ sortDirection: 'asc', searchQuery: '', filteredAgents: [], - + // Status loading: true, error: null, - + // Agent actions setAgents: (agents) => { set({ agents, filteredAgents: agents, loading: false }) get().applyFilters() }, - + addAgent: (agent) => { - set((state) => ({ + set((state) => ({ agents: [...state.agents, agent], - loading: false + loading: false, })) get().applyFilters() }, - + updateAgent: (id, updates) => { set((state) => ({ - agents: state.agents.map(agent => - agent.id === id ? { ...agent, ...updates } : agent - ) + agents: state.agents.map((agent) => (agent.id === id ? { ...agent, ...updates } : agent)), })) get().applyFilters() }, - + deleteAgent: (id) => { set((state) => ({ - agents: state.agents.filter(agent => agent.id !== id) + agents: state.agents.filter((agent) => agent.id !== id), })) get().applyFilters() }, - + selectAgent: (id) => { set({ selectedAgentId: id }) }, - + // MCP Server actions setMCPServers: (servers) => { set({ mcpServers: servers }) }, - + addMCPServer: (server) => { - set((state) => ({ - mcpServers: [...state.mcpServers, server] + set((state) => ({ + mcpServers: [...state.mcpServers, server], })) }, - + updateMCPServer: (id, updates) => { set((state) => ({ - mcpServers: state.mcpServers.map(server => + mcpServers: state.mcpServers.map((server) => server.id === id ? { ...server, ...updates } : server - ) + ), })) }, - + deleteMCPServer: (id) => { set((state) => ({ - mcpServers: state.mcpServers.filter(server => server.id !== id) + mcpServers: state.mcpServers.filter((server) => server.id !== id), })) }, - + // Chat session actions setChatSessions: (sessions) => { set({ chatSessions: sessions }) }, - + addChatSession: (session) => { - set((state) => ({ - chatSessions: [...state.chatSessions, session] + set((state) => ({ + chatSessions: [...state.chatSessions, session], })) }, - + updateChatSession: (id, updates) => { set((state) => ({ - chatSessions: state.chatSessions.map(session => + chatSessions: state.chatSessions.map((session) => session.id === id ? { ...session, ...updates } : session - ) + ), })) }, - + deleteChatSession: (id) => { set((state) => ({ - chatSessions: state.chatSessions.filter(session => session.id !== id) + chatSessions: state.chatSessions.filter((session) => session.id !== id), })) }, - + selectChatSession: (id) => { set({ selectedSessionId: id }) }, - + addMessageToSession: (sessionId, message) => { set((state) => ({ - chatSessions: state.chatSessions.map(session => - session.id === sessionId - ? { ...session, messages: [...session.messages, message] } + chatSessions: state.chatSessions.map((session) => + session.id === sessionId + ? { ...session, messages: [...session.messages, message] } : session - ) + ), })) }, - + // UI state actions setSearchQuery: (query) => { set({ searchQuery: query }) get().applyFilters() }, - + setSortBy: (sortBy) => { set({ sortBy }) get().applyFilters() }, - + setSortDirection: (direction) => { set({ sortDirection: direction }) get().applyFilters() }, - + // Status actions setLoading: (loading) => set({ loading }), setError: (error) => set({ error }), - + // Filters and sorting applyFilters: () => { const { agents, searchQuery, sortBy, sortDirection } = get() - + // Filter by search query let filtered = [...agents] if (searchQuery.trim()) { const query = searchQuery.toLowerCase().trim() - filtered = filtered.filter(agent => - agent.name.toLowerCase().includes(query) || - agent.description.toLowerCase().includes(query) + filtered = filtered.filter( + (agent) => + agent.name.toLowerCase().includes(query) || + agent.description.toLowerCase().includes(query) ) } - + // Sort agents filtered.sort((a, b) => { let comparison = 0 - + switch (sortBy) { case 'name': comparison = a.name.localeCompare(b.name) break case 'createdAt': - comparison = new Date(a.config.createdAt).getTime() - new Date(b.config.createdAt).getTime() + comparison = + new Date(a.config.createdAt).getTime() - new Date(b.config.createdAt).getTime() break case 'updatedAt': - comparison = new Date(a.config.updatedAt).getTime() - new Date(b.config.updatedAt).getTime() + comparison = + new Date(a.config.updatedAt).getTime() - new Date(b.config.updatedAt).getTime() break } - + return sortDirection === 'asc' ? comparison : -comparison }) - + set({ filteredAgents: filtered }) - } -})) \ No newline at end of file + }, +})) diff --git a/sim/app/w/agents/stores/types.ts b/sim/app/w/agents/stores/types.ts index 0d2ebf8feb..1dca67f10f 100644 --- a/sim/app/w/agents/stores/types.ts +++ b/sim/app/w/agents/stores/types.ts @@ -1,12 +1,12 @@ /** * Type Definitions for Agent Builder - * + * * This module defines the TypeScript interfaces and types used throughout the Agent Builder: * - Data models for agents, MCP servers, and chat sessions * - State interfaces for Zustand stores * - Type definitions for UI state and actions * - Utility types for filtering and sorting - * + * * @module AgentTypes */ @@ -63,7 +63,7 @@ export interface AgentState { agents: Agent[] mcpServers: MCPServer[] chatSessions: ChatSession[] - + // UI state selectedAgentId: string | null selectedSessionId: string | null @@ -73,37 +73,37 @@ export interface AgentState { sortDirection: SortDirection searchQuery: string filteredAgents: Agent[] - + // Status loading: boolean error: string | null - + // Actions setAgents: (agents: Agent[]) => void addAgent: (agent: Agent) => void updateAgent: (id: string, updates: Partial) => void deleteAgent: (id: string) => void selectAgent: (id: string | null) => void - + setMCPServers: (servers: MCPServer[]) => void addMCPServer: (server: MCPServer) => void updateMCPServer: (id: string, updates: Partial) => void deleteMCPServer: (id: string) => void - + setChatSessions: (sessions: ChatSession[]) => void addChatSession: (session: ChatSession) => void updateChatSession: (id: string, updates: Partial) => void deleteChatSession: (id: string) => void selectChatSession: (id: string | null) => void addMessageToSession: (sessionId: string, message: ChatMessage) => void - + setSearchQuery: (query: string) => void setSortBy: (sortBy: AgentSortOption) => void setSortDirection: (direction: SortDirection) => void - + setLoading: (loading: boolean) => void setError: (error: string | null) => void - + // Filter and sort applyFilters: () => void -} \ No newline at end of file +} diff --git a/sim/app/w/agents/utils/agent.ts b/sim/app/w/agents/utils/agent.ts index e867d8af4c..7ac28d632c 100644 --- a/sim/app/w/agents/utils/agent.ts +++ b/sim/app/w/agents/utils/agent.ts @@ -1,15 +1,15 @@ /** * Agent Management Utilities - * + * * This module provides utilities for working with agent configurations: * - Validation of agent configurations * - Creation of default agents with sensible defaults * - Template generation for common agent use cases * - ID generation and unique identifier management * - Configuration transformation and normalization - * + * * These utilities help maintain consistency in agent configurations * and provide helper functions for common agent-related operations. - * + * * @module AgentUtils - */ \ No newline at end of file + */ diff --git a/sim/app/w/agents/utils/mcp-server.ts b/sim/app/w/agents/utils/mcp-server.ts index 62fbc7d5e5..4416ec24de 100644 --- a/sim/app/w/agents/utils/mcp-server.ts +++ b/sim/app/w/agents/utils/mcp-server.ts @@ -1,15 +1,15 @@ /** * MCP Server Connection Utilities - * + * * This module provides utilities for managing connections to MCP servers via Smithery: * - Connection testing and validation * - Connection pooling for efficient resource usage * - Model discovery and capability detection * - Agent execution on remote servers * - Health monitoring and status checks - * + * * These utilities abstract the complexities of Smithery integration and provide * a clean API for the rest of the application to use when interacting with MCP servers. - * + * * @module MCPServerUtils - */ \ No newline at end of file + */ diff --git a/sim/app/w/logs/components/sidebar/sidebar.tsx b/sim/app/w/logs/components/sidebar/sidebar.tsx index 7f9c7ed0aa..d1cc797b58 100644 --- a/sim/app/w/logs/components/sidebar/sidebar.tsx +++ b/sim/app/w/logs/components/sidebar/sidebar.tsx @@ -6,8 +6,10 @@ import { Button } from '@/components/ui/button' import { CopyButton } from '@/components/ui/copy-button' import { ScrollArea } from '@/components/ui/scroll-area' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' +import { cn } from '@/lib/utils' import { WorkflowLog } from '@/app/w/logs/stores/types' import { formatDate } from '@/app/w/logs/utils/format-date' +import { formatCost } from '@/providers/utils' import { ToolCallsDisplay } from '../tool-calls/tool-calls-display' import { TraceSpansDisplay } from '../trace-spans/trace-spans-display' @@ -127,7 +129,7 @@ export function Sidebar({ hasNext = false, hasPrev = false, }: LogSidebarProps) { - const [width, setWidth] = useState(400) // Default width from the original styles + const [width, setWidth] = useState(500) // Default width from the original styles const [isDragging, setIsDragging] = useState(false) const [currentLogId, setCurrentLogId] = useState(null) const scrollAreaRef = useRef(null) @@ -167,9 +169,19 @@ export function Sidebar({ return !!(log?.metadata?.traceSpans && log.metadata.traceSpans.length > 0) }, [log]) + // Helper to determine if we have cost information to display + const hasCostInfo = useMemo(() => { + return !!(log?.metadata?.cost && (log.metadata.cost.input || log.metadata.cost.output)) + }, [log]) + + const isWorkflowWithCost = useMemo(() => { + return isWorkflowExecutionLog && hasCostInfo + }, [isWorkflowExecutionLog, hasCostInfo]) + const handleMouseDown = (e: React.MouseEvent) => { setIsDragging(true) e.preventDefault() + e.stopPropagation() } useEffect(() => { @@ -385,6 +397,54 @@ export function Sidebar({ )} + {/* Cost Information (if available) */} + {hasCostInfo && log.metadata?.cost && ( +
+

+ {isWorkflowWithCost ? 'Total Model Cost' : 'Model Cost'} +

+
+ {log.metadata.cost.model && ( +
+ Model: + {log.metadata.cost.model} +
+ )} +
+ Input: + {formatCost(log.metadata.cost.input || 0)} +
+
+ Output: + {formatCost(log.metadata.cost.output || 0)} +
+
+ Total: + + {formatCost(log.metadata.cost.total || 0)} + +
+
+ Tokens: + + {log.metadata.cost.tokens?.prompt || 0} in /{' '} + {log.metadata.cost.tokens?.completion || 0} out + +
+ + {isWorkflowWithCost && ( +
+

+ This is the total cost for all agent blocks in this workflow execution. +

+
+ )} +
+
+ )} + {/* Message Content */}

Message

diff --git a/sim/app/w/logs/components/trace-spans/trace-spans-display.tsx b/sim/app/w/logs/components/trace-spans/trace-spans-display.tsx index e041c3d670..f31250dd57 100644 --- a/sim/app/w/logs/components/trace-spans/trace-spans-display.tsx +++ b/sim/app/w/logs/components/trace-spans/trace-spans-display.tsx @@ -1,15 +1,7 @@ 'use client' import { useState } from 'react' -import { - BarChart, - ChevronDown, - ChevronRight, - Code, - Cpu, - Database, - ExternalLink, -} from 'lucide-react' +import { ChevronDown, ChevronRight, Code, Cpu, ExternalLink } from 'lucide-react' import { AgentIcon, ApiIcon, @@ -266,11 +258,6 @@ function TraceSpanItem({ : formatRelativeTime(startOffset)} )} - {span.tokens && ( - - {span.tokens} tokens - - )}
{formatDuration(duration)} diff --git a/sim/app/w/logs/logs.tsx b/sim/app/w/logs/logs.tsx index fc271db160..f2b2b8ce6b 100644 --- a/sim/app/w/logs/logs.tsx +++ b/sim/app/w/logs/logs.tsx @@ -3,6 +3,7 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { AlertCircle, Info, Loader2 } from 'lucide-react' import { createLogger } from '@/lib/logs/console-logger' +import { formatCost } from '@/providers/utils' import { ControlBar } from './components/control-bar/control-bar' import { Filters } from './components/filters/filters' import { Sidebar } from './components/sidebar/sidebar' @@ -221,6 +222,7 @@ export default function Logs() { + @@ -245,6 +247,9 @@ export default function Logs() { Duration + + Cost + @@ -283,6 +288,7 @@ export default function Logs() { + {filteredLogs.map((log) => { @@ -385,6 +391,17 @@ export default function Logs() { {log.duration || '—'} + + {/* Cost column - hidden on small screens */} + + {log.metadata?.cost?.total ? ( +
+ {formatCost(log.metadata.cost.total)} +
+ ) : ( +
—
+ )} + ) })} diff --git a/sim/app/w/logs/stores/types.ts b/sim/app/w/logs/stores/types.ts index a4d2c966e1..8eca09124d 100644 --- a/sim/app/w/logs/stores/types.ts +++ b/sim/app/w/logs/stores/types.ts @@ -4,7 +4,6 @@ export interface WorkflowData { description: string | null color: string state: any - // Add other workflow fields as needed } export interface ToolCall { @@ -22,6 +21,24 @@ export interface ToolCallMetadata { toolCalls?: ToolCall[] } +export interface CostMetadata { + model?: string + input?: number + output?: number + total?: number + tokens?: { + prompt?: number + completion?: number + total?: number + } + pricing?: { + input: number + output: number + cachedInput?: number + updatedAt: string + } +} + export interface TraceSpan { id: string name: string @@ -49,6 +66,7 @@ export interface WorkflowLog { metadata?: ToolCallMetadata & { traceSpans?: TraceSpan[] totalDuration?: number + cost?: CostMetadata } } @@ -87,4 +105,4 @@ export interface FilterState { setError: (error: string | null) => void // Apply filters applyFilters: () => void -} \ No newline at end of file +}