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
This commit is contained in:
Waleed Latif
2025-03-21 11:20:39 -07:00
committed by GitHub
parent 4128bfe8c3
commit e1466c4f6e
30 changed files with 869 additions and 471 deletions
+2 -7
View File
@@ -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() {
<main className="flex min-h-screen flex-col items-center justify-center bg-gray-50">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<h1 className="text-2xl font-bold text-center mb-8">Sim Studio</h1>
<VerifyContent
hasResendKey={hasResendKey}
baseUrl={baseUrl}
isProduction={isProduction}
/>
<VerifyContent hasResendKey={hasResendKey} baseUrl={baseUrl} isProduction={isProduction} />
</div>
</main>
)
+6 -3
View File
@@ -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,
}
}
}
+9 -5
View File
@@ -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 (
<CardHeader>
<CardTitle>Loading verification...</CardTitle>
<CardDescription>
Please wait while we load your verification details...
</CardDescription>
<CardDescription>Please wait while we load your verification details...</CardDescription>
</CardHeader>
)
}
+14 -5
View File
@@ -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<GitHubResponse> = {
type: 'github',
@@ -160,8 +169,8 @@ export const GitHubBlock: BlockConfig<GitHubResponse> = {
response: {
type: {
content: 'string',
metadata: 'json'
}
}
metadata: 'json',
},
},
},
}
+6
View File
@@ -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<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive> & {
+38 -112
View File
@@ -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": {}
}
}
}
+1 -1
View File
@@ -136,4 +136,4 @@
"breakpoints": true
}
]
}
}
+83 -76
View File
@@ -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
}
}
}
+59 -55
View File
@@ -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}` }
}
}
+25 -21
View File
@@ -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
}
+127 -5
View File
@@ -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<string, number> = {}
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}`, {
+9 -1
View File
@@ -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
}
+125
View File
@@ -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
+21
View File
@@ -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<string, ModelPricing>
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 {
+65
View File
@@ -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'
}
}
+10 -6
View File
@@ -426,7 +426,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
// 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<WorkflowStoreWithHistory>()(
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<WorkflowStoreWithHistory>()(
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<WorkflowStoreWithHistory>()(
}
return result
}
// Return unchanged for other types
return value
}
+2 -2
View File
@@ -100,10 +100,10 @@ export const commentTool: ToolConfig<CreateCommentParams, CreateCommentResponse>
transformResponse: async (response) => {
const data = await response.json()
// Create a human-readable content string
const content = `Comment created: "${data.body}"`
return {
success: true,
output: {
+11 -11
View File
@@ -21,7 +21,7 @@ export const latestCommitTool: ToolConfig<LatestCommitParams, LatestCommitRespon
branch: {
type: 'string',
required: false,
description: 'Branch name (defaults to the repository\'s default branch)',
description: "Branch name (defaults to the repository's default branch)",
},
apiKey: {
type: 'string',
@@ -34,9 +34,7 @@ export const latestCommitTool: ToolConfig<LatestCommitParams, LatestCommitRespon
request: {
url: (params) => {
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<LatestCommitParams, LatestCommitRespon
}
const data = await response.json()
// Create a human-readable content string
const content = `Latest commit: "${data.commit.message}" by ${data.commit.author.name} on ${data.commit.author.date}. SHA: ${data.sha}`
@@ -118,11 +116,13 @@ export const latestCommitTool: ToolConfig<LatestCommitParams, LatestCommitRespon
avatar_url: data.committer?.avatar_url || '',
html_url: data.committer?.html_url || '',
},
stats: data.stats ? {
additions: data.stats.additions,
deletions: data.stats.deletions,
total: data.stats.total,
} : undefined,
stats: data.stats
? {
additions: data.stats.additions,
deletions: data.stats.deletions,
total: data.stats.total,
}
: undefined,
files: fileDetailsWithContent.length > 0 ? fileDetailsWithContent : undefined,
},
},
@@ -141,4 +141,4 @@ export const latestCommitTool: ToolConfig<LatestCommitParams, LatestCommitRespon
}
return 'Failed to fetch commit information'
},
}
}
+1 -1
View File
@@ -155,7 +155,7 @@ URL: https://github.com/testuser/testrepo/pull/42`
// Check the result
expect(result.success).toBe(true)
// Verify content is present and correct
expect(result.output.content).toBe(expectedContent)
+3 -4
View File
@@ -25,7 +25,7 @@ URL: https://github.com/testuser/test-repo`
beforeEach(() => {
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({
+59 -49
View File
@@ -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,
};
}
}
}
+17 -13
View File
@@ -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
+58 -58
View File
@@ -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<AgentState>((set, get) => ({
// Collections
agents: [],
mcpServers: [],
chatSessions: [],
// UI state
selectedAgentId: null,
selectedSessionId: null,
@@ -29,162 +28,163 @@ export const useAgentStore = create<AgentState>((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 })
}
}))
},
}))
+11 -11
View File
@@ -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<Agent>) => void
deleteAgent: (id: string) => void
selectAgent: (id: string | null) => void
setMCPServers: (servers: MCPServer[]) => void
addMCPServer: (server: MCPServer) => void
updateMCPServer: (id: string, updates: Partial<MCPServer>) => void
deleteMCPServer: (id: string) => void
setChatSessions: (sessions: ChatSession[]) => void
addChatSession: (session: ChatSession) => void
updateChatSession: (id: string, updates: Partial<ChatSession>) => 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
}
}
+4 -4
View File
@@ -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
*/
*/
+4 -4
View File
@@ -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
*/
*/
+61 -1
View File
@@ -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<string | null>(null)
const scrollAreaRef = useRef<HTMLDivElement>(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({
</div>
)}
{/* Cost Information (if available) */}
{hasCostInfo && log.metadata?.cost && (
<div>
<h3 className="text-xs font-medium text-muted-foreground mb-1">
{isWorkflowWithCost ? 'Total Model Cost' : 'Model Cost'}
</h3>
<div
className={`space-y-1 text-sm ${isWorkflowWithCost ? 'border rounded-md p-3 bg-muted/10' : ''}`}
>
{log.metadata.cost.model && (
<div className="flex justify-between">
<span className="text-muted-foreground">Model:</span>
<span>{log.metadata.cost.model}</span>
</div>
)}
<div className="flex justify-between">
<span className="text-muted-foreground">Input:</span>
<span>{formatCost(log.metadata.cost.input || 0)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Output:</span>
<span>{formatCost(log.metadata.cost.output || 0)}</span>
</div>
<div className="flex justify-between font-medium">
<span className="text-muted-foreground">Total:</span>
<span className={isWorkflowWithCost ? 'text-primary font-bold' : ''}>
{formatCost(log.metadata.cost.total || 0)}
</span>
</div>
<div className="flex justify-between text-xs text-muted-foreground">
<span>Tokens:</span>
<span>
{log.metadata.cost.tokens?.prompt || 0} in /{' '}
{log.metadata.cost.tokens?.completion || 0} out
</span>
</div>
{isWorkflowWithCost && (
<div className="border-t mt-2 pt-2 text-xs text-muted-foreground">
<p>
This is the total cost for all agent blocks in this workflow execution.
</p>
</div>
)}
</div>
</div>
)}
{/* Message Content */}
<div className="pb-2">
<h3 className="text-xs font-medium text-muted-foreground mb-1">Message</h3>
@@ -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>
)}
{span.tokens && (
<span className="text-xs bg-secondary/40 px-1.5 py-0.5 rounded text-muted-foreground whitespace-nowrap">
{span.tokens} tokens
</span>
)}
</div>
<span className="text-xs text-muted-foreground">{formatDuration(duration)}</span>
</div>
+17
View File
@@ -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() {
<col className="w-[8%] hidden lg:table-column" />
<col className="w-auto lg:w-auto" />
<col className="w-[8%]" />
<col className="w-[8%] hidden lg:table-cell" />
</colgroup>
<thead>
<tr>
@@ -245,6 +247,9 @@ export default function Logs() {
<th className="px-4 pt-2 pb-3 text-left font-medium">
<span className="text-xs text-muted-foreground leading-none">Duration</span>
</th>
<th className="px-4 pt-2 pb-3 text-left font-medium hidden lg:table-cell">
<span className="text-xs text-muted-foreground leading-none">Cost</span>
</th>
</tr>
</thead>
</table>
@@ -283,6 +288,7 @@ export default function Logs() {
<col className="w-[8%] hidden lg:table-column" />
<col className="w-auto lg:w-auto" />
<col className="w-[8%]" />
<col className="w-[8%] hidden lg:table-cell" />
</colgroup>
<tbody>
{filteredLogs.map((log) => {
@@ -385,6 +391,17 @@ export default function Logs() {
{log.duration || '—'}
</div>
</td>
{/* Cost column - hidden on small screens */}
<td className="px-4 py-3 hidden lg:table-cell">
{log.metadata?.cost?.total ? (
<div className="text-xs text-muted-foreground">
{formatCost(log.metadata.cost.total)}
</div>
) : (
<div className="text-xs text-muted-foreground">—</div>
)}
</td>
</tr>
)
})}
+20 -2
View File
@@ -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
}
}