merge(staging-to-main) (#577)

* feat(agent): agent model dropdown combobox (#572)

* feat: agent model dropdown combobox

* fix(cli): package type for esm imports, missing realtime (#574)

* fix: package type for esm imports, missing realtime calls and use of migrate

* chore: bump cli

* fix sourceBlock null check

* fix(kb): fix kb navigation URLs

* fix(csp): update CSP to allow for google drive picker

* feat(dropdown): added optional icon to agent model dropdown

---------

Co-authored-by: Aditya Tripathi <aditya@climactic.co>
Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local>
Co-authored-by: Waleed Latif <walif6@gmail.com>

* fix concurrent req check (#576)

Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net>

---------

Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
Co-authored-by: Aditya Tripathi <aditya@climactic.co>
Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local>
Co-authored-by: Waleed Latif <walif6@gmail.com>
Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net>
This commit is contained in:
Vikhyath Mondreti
2025-06-28 13:38:10 -07:00
committed by GitHub
parent 9be41b4b73
commit bf0ea10e62
9 changed files with 739 additions and 21 deletions

View File

@@ -29,7 +29,8 @@ export const runtime = 'nodejs'
// Define the schema for environment variables
const EnvVarsSchema = z.record(z.string())
// Keep track of running executions to prevent overlap
// Keep track of running executions to prevent duplicate requests
// Use a combination of workflow ID and request ID to allow concurrent executions with different inputs
const runningExecutions = new Set<string>()
// Custom error class for usage limit exceeded
@@ -47,10 +48,14 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) {
const workflowId = workflow.id
const executionId = uuidv4()
// Skip if this workflow is already running
if (runningExecutions.has(workflowId)) {
logger.warn(`[${requestId}] Workflow is already running: ${workflowId}`)
throw new Error('Workflow is already running')
// Create a unique execution key combining workflow ID and request ID
// This allows concurrent executions of the same workflow with different inputs
const executionKey = `${workflowId}:${requestId}`
// Skip if this exact execution is already running (prevents duplicate requests)
if (runningExecutions.has(executionKey)) {
logger.warn(`[${requestId}] Execution is already running: ${executionKey}`)
throw new Error('Execution is already running')
}
// Check if the user has exceeded their usage limits
@@ -86,7 +91,7 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) {
}
try {
runningExecutions.add(workflowId)
runningExecutions.add(executionKey)
logger.info(`[${requestId}] Starting workflow execution: ${workflowId}`)
// Use the deployed state if available, otherwise fall back to current state
@@ -291,7 +296,7 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) {
await persistExecutionError(workflowId, executionId, error, 'api')
throw error
} finally {
runningExecutions.delete(workflowId)
runningExecutions.delete(executionKey)
}
}
@@ -392,7 +397,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
}
}
export async function OPTIONS(request: NextRequest) {
export async function OPTIONS(_request: NextRequest) {
return new NextResponse(null, {
status: 200,
headers: {

View File

@@ -0,0 +1,461 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { Check, ChevronDown } from 'lucide-react'
import { useReactFlow } from 'reactflow'
import { Button } from '@/components/ui/button'
import { checkEnvVarTrigger, EnvVarDropdown } from '@/components/ui/env-var-dropdown'
import { formatDisplayText } from '@/components/ui/formatted-text'
import { Input } from '@/components/ui/input'
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
import type { SubBlockConfig } from '@/blocks/types'
import { useSubBlockValue } from '../hooks/use-sub-block-value'
const logger = createLogger('ComboBox')
interface ComboBoxProps {
options:
| Array<string | { label: string; id: string }>
| (() => Array<string | { label: string; id: string }>)
defaultValue?: string
blockId: string
subBlockId: string
value?: string
isPreview?: boolean
previewValue?: string | null
disabled?: boolean
placeholder?: string
isConnecting: boolean
config: SubBlockConfig
}
export function ComboBox({
options,
defaultValue,
blockId,
subBlockId,
value: propValue,
isPreview = false,
previewValue,
disabled,
placeholder = 'Type or select an option...',
isConnecting,
config,
}: ComboBoxProps) {
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlockId)
const [storeInitialized, setStoreInitialized] = useState(false)
const [open, setOpen] = useState(false)
const [isFocused, setIsFocused] = useState(false)
const [showEnvVars, setShowEnvVars] = useState(false)
const [showTags, setShowTags] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
const [cursorPosition, setCursorPosition] = useState(0)
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
const overlayRef = useRef<HTMLDivElement>(null)
const reactFlowInstance = useReactFlow()
// Use preview value when in preview mode, otherwise use store value or prop value
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
// Evaluate options if it's a function
const evaluatedOptions = useMemo(() => {
return typeof options === 'function' ? options() : options
}, [options])
const getOptionValue = (option: string | { label: string; id: string }) => {
return typeof option === 'string' ? option : option.id
}
const getOptionLabel = (option: string | { label: string; id: string }) => {
return typeof option === 'string' ? option : option.label
}
// Get the default option value (prefer gpt-4o, then provided defaultValue, then first option)
const defaultOptionValue = useMemo(() => {
if (defaultValue !== undefined) {
return defaultValue
}
// For model field, default to gpt-4o if available
if (subBlockId === 'model') {
const gpt4o = evaluatedOptions.find((opt) => getOptionValue(opt) === 'gpt-4o')
if (gpt4o) {
return getOptionValue(gpt4o)
}
}
if (evaluatedOptions.length > 0) {
return getOptionValue(evaluatedOptions[0])
}
return undefined
}, [defaultValue, evaluatedOptions, getOptionValue, subBlockId])
// Mark store as initialized on first render
useEffect(() => {
setStoreInitialized(true)
}, [])
// Only set default value once the store is confirmed to be initialized
// and we know the actual value is null/undefined (not just loading)
useEffect(() => {
if (
storeInitialized &&
(value === null || value === undefined) &&
defaultOptionValue !== undefined
) {
setStoreValue(defaultOptionValue)
}
}, [storeInitialized, value, defaultOptionValue, setStoreValue])
// Filter options based on current value for display
const filteredOptions = useMemo(() => {
// Always show all options when dropdown is not open
if (!open) return evaluatedOptions
// If no value or value matches an exact option, show all options
if (!value) return evaluatedOptions
const currentValue = value.toString()
const exactMatch = evaluatedOptions.find(
(opt) => getOptionValue(opt) === currentValue || getOptionLabel(opt) === currentValue
)
// If current value exactly matches an option, show all options (user just selected it)
if (exactMatch) return evaluatedOptions
// Otherwise filter based on current input
return evaluatedOptions.filter((option) => {
const label = getOptionLabel(option).toLowerCase()
const optionValue = getOptionValue(option).toLowerCase()
const search = currentValue.toLowerCase()
return label.includes(search) || optionValue.includes(search)
})
}, [evaluatedOptions, value, open, getOptionLabel, getOptionValue])
// Event handlers
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (disabled) {
e.preventDefault()
return
}
const newValue = e.target.value
const newCursorPosition = e.target.selectionStart ?? 0
// Update store value immediately (allow free text)
if (!isPreview) {
setStoreValue(newValue)
}
setCursorPosition(newCursorPosition)
// Check for environment variables trigger
const envVarTrigger = checkEnvVarTrigger(newValue, newCursorPosition)
setShowEnvVars(envVarTrigger.show)
setSearchTerm(envVarTrigger.show ? envVarTrigger.searchTerm : '')
// Check for tag trigger
const tagTrigger = checkTagTrigger(newValue, newCursorPosition)
setShowTags(tagTrigger.show)
}
const handleSelect = (selectedValue: string) => {
if (!isPreview && !disabled) {
setStoreValue(selectedValue)
}
setOpen(false)
inputRef.current?.blur()
}
const handleDropdownClick = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
if (!disabled) {
setOpen(!open)
if (!open) {
inputRef.current?.focus()
}
}
}
const handleFocus = () => {
setIsFocused(true)
setOpen(true)
}
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
setIsFocused(false)
setShowEnvVars(false)
setShowTags(false)
// Delay closing to allow dropdown selection
setTimeout(() => {
const activeElement = document.activeElement
if (!activeElement || !activeElement.closest('.absolute.top-full')) {
setOpen(false)
}
}, 150)
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Escape') {
setShowEnvVars(false)
setShowTags(false)
setOpen(false)
return
}
if (e.key === 'ArrowDown' && !open) {
setOpen(true)
e.preventDefault()
}
}
// Drag and drop handlers
const handleDragOver = (e: React.DragEvent<HTMLInputElement>) => {
if (config?.connectionDroppable === false) return
e.preventDefault()
}
const handleDrop = (e: React.DragEvent<HTMLInputElement>) => {
if (config?.connectionDroppable === false) return
e.preventDefault()
try {
const data = JSON.parse(e.dataTransfer.getData('application/json'))
if (data.type !== 'connectionBlock') return
const dropPosition = inputRef.current?.selectionStart ?? value?.toString().length ?? 0
const currentValue = value?.toString() ?? ''
const newValue = `${currentValue.slice(0, dropPosition)}<${currentValue.slice(dropPosition)}`
inputRef.current?.focus()
Promise.resolve().then(() => {
setStoreValue(newValue)
setCursorPosition(dropPosition + 1)
setShowTags(true)
if (data.connectionData?.sourceBlockId) {
setActiveSourceBlockId(data.connectionData.sourceBlockId)
}
setTimeout(() => {
if (inputRef.current) {
inputRef.current.selectionStart = dropPosition + 1
inputRef.current.selectionEnd = dropPosition + 1
}
}, 0)
})
} catch (error) {
logger.error('Failed to parse drop data:', { error })
}
}
// Scroll and paste handlers
const handleScroll = (e: React.UIEvent<HTMLInputElement>) => {
if (overlayRef.current) {
overlayRef.current.scrollLeft = e.currentTarget.scrollLeft
}
}
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
setTimeout(() => {
if (inputRef.current && overlayRef.current) {
overlayRef.current.scrollLeft = inputRef.current.scrollLeft
}
}, 0)
}
// ReactFlow zoom handler
const handleWheel = (e: React.WheelEvent<HTMLInputElement>) => {
if (e.ctrlKey || e.metaKey) {
e.preventDefault()
e.stopPropagation()
const currentZoom = reactFlowInstance.getZoom()
const { x: viewportX, y: viewportY } = reactFlowInstance.getViewport()
const delta = e.deltaY > 0 ? 1 : -1
const zoomFactor = 0.96 ** delta
const newZoom = Math.min(Math.max(currentZoom * zoomFactor, 0.1), 1)
const { x: pointerX, y: pointerY } = reactFlowInstance.screenToFlowPosition({
x: e.clientX,
y: e.clientY,
})
const newViewportX = viewportX + (pointerX * currentZoom - pointerX * newZoom)
const newViewportY = viewportY + (pointerY * currentZoom - pointerY * newZoom)
reactFlowInstance.setViewport(
{ x: newViewportX, y: newViewportY, zoom: newZoom },
{ duration: 0 }
)
return false
}
return true
}
// Environment variable and tag selection handler
const handleEnvVarSelect = (newValue: string) => {
if (!isPreview) {
setStoreValue(newValue)
}
}
// Effects
useEffect(() => {
if (inputRef.current && overlayRef.current) {
overlayRef.current.scrollLeft = inputRef.current.scrollLeft
}
}, [value])
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Element
if (
inputRef.current &&
!inputRef.current.contains(target) &&
!target.closest('[data-radix-popper-content-wrapper]') &&
!target.closest('.absolute.top-full')
) {
setOpen(false)
}
}
if (open) {
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}
}, [open])
// Display value with formatting
const displayValue = value?.toString() ?? ''
// Render component
return (
<div className='relative w-full'>
<div className='relative'>
<Input
ref={inputRef}
className={cn(
'allow-scroll w-full overflow-auto pr-10 text-transparent caret-foreground placeholder:text-muted-foreground/50',
isConnecting &&
config?.connectionDroppable !== false &&
'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
)}
placeholder={placeholder}
value={displayValue}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
onDrop={handleDrop}
onDragOver={handleDragOver}
onScroll={handleScroll}
onPaste={handlePaste}
onWheel={handleWheel}
disabled={disabled}
autoComplete='off'
style={{ overflowX: 'auto' }}
/>
<div
ref={overlayRef}
className='pointer-events-none absolute top-0 bottom-0 left-0 flex items-center bg-transparent pr-0 pl-3 text-sm'
style={{ right: '42px' }}
>
<div className='w-full truncate text-foreground' style={{ scrollbarWidth: 'none' }}>
{formatDisplayText(displayValue, true)}
</div>
</div>
{/* Chevron button */}
<Button
variant='ghost'
size='sm'
className='-translate-y-1/2 absolute top-1/2 right-1 z-10 h-6 w-6 p-0 hover:bg-transparent'
disabled={disabled}
onMouseDown={handleDropdownClick}
>
<ChevronDown
className={cn('h-4 w-4 opacity-50 transition-transform', open && 'rotate-180')}
/>
</Button>
</div>
{/* Dropdown */}
{open && (
<div className='absolute top-full left-0 z-[100] mt-1 w-full min-w-[286px]'>
<div className='allow-scroll fade-in-0 zoom-in-95 animate-in rounded-md border bg-popover text-popover-foreground shadow-lg'>
<div
className='allow-scroll max-h-48 overflow-y-auto p-1'
style={{ scrollbarWidth: 'thin' }}
>
{filteredOptions.length === 0 ? (
<div className='py-6 text-center text-muted-foreground text-sm'>
No matching options found.
</div>
) : (
filteredOptions.map((option) => {
const optionValue = getOptionValue(option)
const optionLabel = getOptionLabel(option)
const OptionIcon =
typeof option === 'object' && 'icon' in option
? (option.icon as React.ComponentType<{ className?: string }>)
: null
const isSelected = displayValue === optionValue || displayValue === optionLabel
return (
<div
key={optionValue}
onClick={() => handleSelect(optionValue)}
onMouseDown={(e) => {
e.preventDefault()
handleSelect(optionValue)
}}
className='relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground'
>
{OptionIcon && <OptionIcon className='mr-2 h-3 w-3 opacity-60' />}
<span className='flex-1 truncate'>{optionLabel}</span>
{isSelected && <Check className='ml-2 h-4 w-4 flex-shrink-0' />}
</div>
)
})
)}
</div>
</div>
</div>
)}
<EnvVarDropdown
visible={showEnvVars}
onSelect={handleEnvVarSelect}
searchTerm={searchTerm}
inputValue={displayValue}
cursorPosition={cursorPosition}
onClose={() => {
setShowEnvVars(false)
setSearchTerm('')
}}
/>
<TagDropdown
visible={showTags}
onSelect={handleEnvVarSelect}
blockId={blockId}
activeSourceBlockId={activeSourceBlockId}
inputValue={displayValue}
cursorPosition={cursorPosition}
onClose={() => {
setShowTags(false)
setActiveSourceBlockId(null)
}}
/>
</div>
)
}

View File

@@ -7,6 +7,7 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { ChannelSelectorInput } from './components/channel-selector/channel-selector-input'
import { CheckboxList } from './components/checkbox-list'
import { Code } from './components/code'
import { ComboBox } from './components/combobox'
import { ConditionInput } from './components/condition-input'
import { CredentialSelector } from './components/credential-selector/credential-selector'
import { DateInput } from './components/date-input'
@@ -114,6 +115,22 @@ export function SubBlock({
/>
</div>
)
case 'combobox':
return (
<div onMouseDown={handleMouseDown}>
<ComboBox
blockId={blockId}
subBlockId={config.id}
options={config.options as string[]}
placeholder={config.placeholder}
isPreview={isPreview}
previewValue={previewValue}
disabled={isDisabled}
isConnecting={isConnecting}
config={config}
/>
</div>
)
case 'slider':
return (
<SliderInput

View File

@@ -5,8 +5,10 @@ import {
getAllModelProviders,
getBaseModelProviders,
getHostedModels,
getProviderIcon,
MODELS_TEMP_RANGE_0_1,
MODELS_TEMP_RANGE_0_2,
MODELS_WITH_TEMPERATURE_SUPPORT,
providers,
} from '@/providers/utils'
import { useOllamaStore } from '@/stores/ollama/store'
@@ -87,12 +89,30 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
{
id: 'model',
title: 'Model',
type: 'dropdown',
type: 'combobox',
layout: 'half',
placeholder: 'Type or select a model...',
options: () => {
const ollamaModels = useOllamaStore.getState().models
const baseModels = Object.keys(getBaseModelProviders())
return [...baseModels, ...ollamaModels]
const allModels = [...baseModels, ...ollamaModels]
return allModels.map((model) => {
const icon = getProviderIcon(model)
return { label: model, id: model, ...(icon && { icon }) }
})
},
},
{
id: 'temperature',
title: 'Temperature',
type: 'slider',
layout: 'half',
min: 0,
max: 1,
condition: {
field: 'model',
value: MODELS_TEMP_RANGE_0_1,
},
},
{
@@ -111,12 +131,20 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
id: 'temperature',
title: 'Temperature',
type: 'slider',
layout: 'half',
layout: 'full',
min: 0,
max: 1,
max: 2,
condition: {
field: 'model',
value: MODELS_TEMP_RANGE_0_1,
value: [...MODELS_TEMP_RANGE_0_1, ...MODELS_TEMP_RANGE_0_2],
not: true,
and: {
field: 'model',
value: Object.keys(getBaseModelProviders()).filter(
(model) => !MODELS_WITH_TEMPERATURE_SUPPORT.includes(model)
),
not: true,
},
},
},
{

View File

@@ -14,6 +14,7 @@ export type SubBlockType =
| 'short-input' // Single line input
| 'long-input' // Multi-line input
| 'dropdown' // Select menu
| 'combobox' // Searchable dropdown with text input
| 'slider' // Range input
| 'table' // Grid layout
| 'code' // Code editor
@@ -92,8 +93,10 @@ export interface SubBlockConfig {
mode?: 'basic' | 'advanced' | 'both' // Default is 'both' if not specified
options?:
| string[]
| { label: string; id: string }[]
| (() => string[] | { label: string; id: string }[])
| { label: string; id: string; icon?: React.ComponentType<{ className?: string }> }[]
| (() =>
| string[]
| { label: string; id: string; icon?: React.ComponentType<{ className?: string }> }[])
min?: number
max?: number
columns?: string[]

View File

@@ -2953,3 +2953,162 @@ export const ResponseIcon = (props: SVGProps<SVGSVGElement>) => (
/>
</svg>
)
export const AnthropicIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
{...props}
fill='currentColor'
fillRule='evenodd'
height='1em'
viewBox='0 0 24 24'
width='1em'
xmlns='http://www.w3.org/2000/svg'
>
<title>Anthropic</title>
<path d='M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z' />
</svg>
)
export const AzureIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
{...props}
width='18'
height='18'
viewBox='0 0 18 18'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M5.33492 1.37491C5.44717 1.04229 5.75909 0.818359 6.11014 0.818359H11.25L5.91513 16.6255C5.80287 16.9581 5.49095 17.182 5.13991 17.182H1.13968C0.579936 17.182 0.185466 16.6325 0.364461 16.1022L5.33492 1.37491Z'
fill='url(#paint0_linear_6102_134469)'
/>
<path
d='M13.5517 11.4546H5.45126C5.1109 11.4546 4.94657 11.8715 5.19539 12.1037L10.4005 16.9618C10.552 17.1032 10.7515 17.1819 10.9587 17.1819H15.5453L13.5517 11.4546Z'
fill='#0078D4'
/>
<path
d='M6.11014 0.818359C5.75909 0.818359 5.44717 1.04229 5.33492 1.37491L0.364461 16.1022C0.185466 16.6325 0.579936 17.182 1.13968 17.182H5.13991C5.49095 17.182 5.80287 16.9581 5.91513 16.6255L6.90327 13.6976L10.4005 16.9617C10.552 17.1032 10.7515 17.1818 10.9588 17.1818H15.5454L13.5517 11.4545H7.66032L11.25 0.818359H6.11014Z'
fill='url(#paint1_linear_6102_134469)'
/>
<path
d='M12.665 1.37478C12.5528 1.04217 12.2409 0.818237 11.8898 0.818237H6.13629H6.16254C6.51358 0.818237 6.82551 1.04217 6.93776 1.37478L11.9082 16.1021C12.0872 16.6324 11.6927 17.1819 11.133 17.1819H11.0454H16.8603C17.42 17.1819 17.8145 16.6324 17.6355 16.1021L12.665 1.37478Z'
fill='url(#paint2_linear_6102_134469)'
/>
<defs>
<linearGradient
id='paint0_linear_6102_134469'
x1='6.07512'
y1='1.38476'
x2='0.738178'
y2='17.1514'
gradientUnits='userSpaceOnUse'
>
<stop stopColor='#114A8B' />
<stop offset='1' stopColor='#0669BC' />
</linearGradient>
<linearGradient
id='paint1_linear_6102_134469'
x1='10.3402'
y1='11.4564'
x2='9.107'
y2='11.8734'
gradientUnits='userSpaceOnUse'
>
<stop stop-opacity='0.3' />
<stop offset='0.0711768' stop-opacity='0.2' />
<stop offset='0.321031' stop-opacity='0.1' />
<stop offset='0.623053' stop-opacity='0.05' />
<stop offset='1' stop-opacity='0' />
</linearGradient>
<linearGradient
id='paint2_linear_6102_134469'
x1='9.45858'
y1='1.38467'
x2='15.3168'
y2='16.9926'
gradientUnits='userSpaceOnUse'
>
<stop stopColor='#3CCBF4' />
<stop offset='1' stopColor='#2892DF' />
</linearGradient>
</defs>
</svg>
)
export const GroqIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
{...props}
fill='currentColor'
fillRule='evenodd'
height='1em'
viewBox='0 0 24 24'
width='1em'
xmlns='http://www.w3.org/2000/svg'
>
<title>Groq</title>
<path d='M12.036 2c-3.853-.035-7 3-7.036 6.781-.035 3.782 3.055 6.872 6.908 6.907h2.42v-2.566h-2.292c-2.407.028-4.38-1.866-4.408-4.23-.029-2.362 1.901-4.298 4.308-4.326h.1c2.407 0 4.358 1.915 4.365 4.278v6.305c0 2.342-1.944 4.25-4.323 4.279a4.375 4.375 0 01-3.033-1.252l-1.851 1.818A7 7 0 0012.029 22h.092c3.803-.056 6.858-3.083 6.879-6.816v-6.5C18.907 4.963 15.817 2 12.036 2z' />
</svg>
)
export const DeepseekIcon = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} height='1em' viewBox='0 0 24 24' width='1em' xmlns='http://www.w3.org/2000/svg'>
<title>DeepSeek</title>
<path
d='M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z'
fill='#4D6BFE'
/>
</svg>
)
export const GeminiIcon = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} height='1em' viewBox='0 0 24 24' width='1em' xmlns='http://www.w3.org/2000/svg'>
<title>Gemini</title>
<defs>
<linearGradient id='lobe-icons-gemini-fill' x1='0%' x2='68.73%' y1='100%' y2='30.395%'>
<stop offset='0%' stopColor='#1C7DFF' />
<stop offset='52.021%' stopColor='#1C69FF' />
<stop offset='100%' stopColor='#F0DCD6' />
</linearGradient>
</defs>
<path
d='M12 24A14.304 14.304 0 000 12 14.304 14.304 0 0012 0a14.305 14.305 0 0012 12 14.305 14.305 0 00-12 12'
fill='url(#lobe-icons-gemini-fill)'
fillRule='nonzero'
/>
</svg>
)
export const CerebrasIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
{...props}
fill='currentColor'
height='1em'
viewBox='0 0 24 24'
width='1em'
xmlns='http://www.w3.org/2000/svg'
>
<title>Cerebras</title>
<path
clipRule='evenodd'
d='M14.121 2.701a9.299 9.299 0 000 18.598V22.7c-5.91 0-10.7-4.791-10.7-10.701S8.21 1.299 14.12 1.299V2.7zm4.752 3.677A7.353 7.353 0 109.42 17.643l-.901 1.074a8.754 8.754 0 01-1.08-12.334 8.755 8.755 0 0112.335-1.08l-.901 1.075zm-2.255.844a5.407 5.407 0 00-5.048 9.563l-.656 1.24a6.81 6.81 0 016.358-12.043l-.654 1.24zM14.12 8.539a3.46 3.46 0 100 6.922v1.402a4.863 4.863 0 010-9.726v1.402z'
fill='#F15A29'
fillRule='evenodd'
/>
<path d='M15.407 10.836a2.24 2.24 0 00-.51-.409 1.084 1.084 0 00-.544-.152c-.255 0-.483.047-.684.14a1.58 1.58 0 00-.84.912c-.074.203-.11.416-.11.631 0 .218.036.43.11.631a1.594 1.594 0 00.84.913c.2.093.43.14.684.14.216 0 .417-.046.602-.135.188-.09.35-.225.475-.392l.928 1.006c-.14.14-.3.261-.482.363a3.367 3.367 0 01-1.083.38c-.17.026-.317.04-.44.04a3.315 3.315 0 01-1.182-.21 2.825 2.825 0 01-.961-.597 2.816 2.816 0 01-.644-.929 2.987 2.987 0 01-.238-1.21c0-.444.08-.847.238-1.21.15-.35.368-.666.643-.929.278-.261.605-.464.962-.596a3.315 3.315 0 011.182-.21c.355 0 .712.068 1.072.204.361.138.685.36.944.649l-.962.97z' />
</svg>
)
export const OllamaIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
{...props}
fill='currentColor'
fillRule='evenodd'
height='1em'
viewBox='0 0 24 24'
width='1em'
xmlns='http://www.w3.org/2000/svg'
>
<title>Ollama</title>
<path d='M7.905 1.09c.216.085.411.225.588.41.295.306.544.744.734 1.263.191.522.315 1.1.362 1.68a5.054 5.054 0 012.049-.636l.051-.004c.87-.07 1.73.087 2.48.474.101.053.2.11.297.17.05-.569.172-1.134.36-1.644.19-.52.439-.957.733-1.264a1.67 1.67 0 01.589-.41c.257-.1.53-.118.796-.042.401.114.745.368 1.016.737.248.337.434.769.561 1.287.23.934.27 2.163.115 3.645l.053.04.026.019c.757.576 1.284 1.397 1.563 2.35.435 1.487.216 3.155-.534 4.088l-.018.021.002.003c.417.762.67 1.567.724 2.4l.002.03c.064 1.065-.2 2.137-.814 3.19l-.007.01.01.024c.472 1.157.62 2.322.438 3.486l-.006.039a.651.651 0 01-.747.536.648.648 0 01-.54-.742c.167-1.033.01-2.069-.48-3.123a.643.643 0 01.04-.617l.004-.006c.604-.924.854-1.83.8-2.72-.046-.779-.325-1.544-.8-2.273a.644.644 0 01.18-.886l.009-.006c.243-.159.467-.565.58-1.12a4.229 4.229 0 00-.095-1.974c-.205-.7-.58-1.284-1.105-1.683-.595-.454-1.383-.673-2.38-.61a.653.653 0 01-.632-.371c-.314-.665-.772-1.141-1.343-1.436a3.288 3.288 0 00-1.772-.332c-1.245.099-2.343.801-2.67 1.686a.652.652 0 01-.61.425c-1.067.002-1.893.252-2.497.703-.522.39-.878.935-1.066 1.588a4.07 4.07 0 00-.068 1.886c.112.558.331 1.02.582 1.269l.008.007c.212.207.257.53.109.785-.36.622-.629 1.549-.673 2.44-.05 1.018.186 1.902.719 2.536l.016.019a.643.643 0 01.095.69c-.576 1.236-.753 2.252-.562 3.052a.652.652 0 01-1.269.298c-.243-1.018-.078-2.184.473-3.498l.014-.035-.008-.012a4.339 4.339 0 01-.598-1.309l-.005-.019a5.764 5.764 0 01-.177-1.785c.044-.91.278-1.842.622-2.59l.012-.026-.002-.002c-.293-.418-.51-.953-.63-1.545l-.005-.024a5.352 5.352 0 01.093-2.49c.262-.915.777-1.701 1.536-2.269.06-.045.123-.09.186-.132-.159-1.493-.119-2.73.112-3.67.127-.518.314-.95.562-1.287.27-.368.614-.622 1.015-.737.266-.076.54-.059.797.042zm4.116 9.09c.936 0 1.8.313 2.446.855.63.527 1.005 1.235 1.005 1.94 0 .888-.406 1.58-1.133 2.022-.62.375-1.451.557-2.403.557-1.009 0-1.871-.259-2.493-.734-.617-.47-.963-1.13-.963-1.845 0-.707.398-1.417 1.056-1.946.668-.537 1.55-.849 2.485-.849zm0 .896a3.07 3.07 0 00-1.916.65c-.461.37-.722.835-.722 1.25 0 .428.21.829.61 1.134.455.347 1.124.548 1.943.548.799 0 1.473-.147 1.932-.426.463-.28.7-.686.7-1.257 0-.423-.246-.89-.683-1.256-.484-.405-1.14-.643-1.864-.643zm.662 1.21l.004.004c.12.151.095.37-.056.49l-.292.23v.446a.375.375 0 01-.376.373.375.375 0 01-.376-.373v-.46l-.271-.218a.347.347 0 01-.052-.49.353.353 0 01.494-.051l.215.172.22-.174a.353.353 0 01.49.051zm-5.04-1.919c.478 0 .867.39.867.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zm8.706 0c.48 0 .868.39.868.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zM7.44 2.3l-.003.002a.659.659 0 00-.285.238l-.005.006c-.138.189-.258.467-.348.832-.17.692-.216 1.631-.124 2.782.43-.128.899-.208 1.404-.237l.01-.001.019-.034c.046-.082.095-.161.148-.239.123-.771.022-1.692-.253-2.444-.134-.364-.297-.65-.453-.813a.628.628 0 00-.107-.09L7.44 2.3zm9.174.04l-.002.001a.628.628 0 00-.107.09c-.156.163-.32.45-.453.814-.29.794-.387 1.776-.23 2.572l.058.097.008.014h.03a5.184 5.184 0 011.466.212c.086-1.124.038-2.043-.128-2.722-.09-.365-.21-.643-.349-.832l-.004-.006a.659.659 0 00-.285-.239h-.004z' />
</svg>
)

View File

@@ -7,6 +7,19 @@
* - Provider configurations
*/
import type React from 'react'
import {
AnthropicIcon,
AzureIcon,
CerebrasIcon,
DeepseekIcon,
GeminiIcon,
GroqIcon,
OllamaIcon,
OpenAIIcon,
xAIIcon,
} from '@/components/icons'
export interface ModelPricing {
input: number // Per 1M tokens
cachedInput?: number // Per 1M tokens (if supported)
@@ -36,6 +49,7 @@ export interface ProviderDefinition {
models: ModelDefinition[]
defaultModel: string
modelPatterns?: RegExp[]
icon?: React.ComponentType<{ className?: string }>
}
/**
@@ -48,6 +62,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
description: "OpenAI's models",
defaultModel: 'gpt-4o',
modelPatterns: [/^gpt/, /^o1/],
icon: OpenAIIcon,
models: [
{
id: 'gpt-4o',
@@ -142,6 +157,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
description: 'Microsoft Azure OpenAI Service models',
defaultModel: 'azure/gpt-4o',
modelPatterns: [/^azure\//],
icon: AzureIcon,
models: [
{
id: 'azure/gpt-4o',
@@ -212,6 +228,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
description: "Anthropic's Claude models",
defaultModel: 'claude-sonnet-4-0',
modelPatterns: [/^claude/],
icon: AnthropicIcon,
models: [
{
id: 'claude-sonnet-4-0',
@@ -275,6 +292,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
description: "Google's Gemini models",
defaultModel: 'gemini-2.5-pro',
modelPatterns: [/^gemini/],
icon: GeminiIcon,
models: [
{
id: 'gemini-2.5-pro',
@@ -310,6 +328,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
description: "Deepseek's chat models",
defaultModel: 'deepseek-chat',
modelPatterns: [],
icon: DeepseekIcon,
models: [
{
id: 'deepseek-chat',
@@ -356,6 +375,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
description: "xAI's Grok models",
defaultModel: 'grok-3-latest',
modelPatterns: [/^grok/],
icon: xAIIcon,
models: [
{
id: 'grok-3-latest',
@@ -391,6 +411,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
description: 'Cerebras Cloud LLMs',
defaultModel: 'cerebras/llama-3.3-70b',
modelPatterns: [/^cerebras/],
icon: CerebrasIcon,
models: [
{
id: 'cerebras/llama-3.3-70b',
@@ -412,6 +433,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
description: "Groq's LLM models with high-performance inference",
defaultModel: 'groq/meta-llama/llama-4-scout-17b-16e-instruct',
modelPatterns: [/^groq/],
icon: GroqIcon,
models: [
{
id: 'groq/meta-llama/llama-4-scout-17b-16e-instruct',
@@ -457,6 +479,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
description: 'Local LLM models via Ollama',
defaultModel: '',
modelPatterns: [],
icon: OllamaIcon,
models: [], // Populated dynamically
},
}

View File

@@ -178,6 +178,14 @@ export function getProviderModels(providerId: ProviderId): string[] {
return getProviderModelsFromDefinitions(providerId)
}
/**
* Get provider icon for a given model
*/
export function getProviderIcon(model: string): React.ComponentType<{ className?: string }> | null {
const providerId = getProviderFromModel(model)
return PROVIDER_DEFINITIONS[providerId]?.icon || null
}
export function generateStructuredOutputInstructions(responseFormat: any): string {
// Handle null/undefined input
if (!responseFormat) return ''

View File

@@ -70,9 +70,16 @@ export class Serializer {
// For non-custom tools, we determine the tool ID
const nonCustomTools = tools.filter((tool: any) => tool.type !== 'custom-tool')
if (nonCustomTools.length > 0) {
toolId = blockConfig.tools.config?.tool
? blockConfig.tools.config.tool(params)
: blockConfig.tools.access[0]
try {
toolId = blockConfig.tools.config?.tool
? blockConfig.tools.config.tool(params)
: blockConfig.tools.access[0]
} catch (error) {
logger.warn('Tool selection failed during serialization, using default:', {
error: error instanceof Error ? error.message : String(error),
})
toolId = blockConfig.tools.access[0]
}
}
} catch (error) {
logger.error('Error processing tools in agent block:', { error })
@@ -81,9 +88,16 @@ export class Serializer {
}
} else {
// For non-agent blocks, get tool ID from block config as usual
toolId = blockConfig.tools.config?.tool
? blockConfig.tools.config.tool(params)
: blockConfig.tools.access[0]
try {
toolId = blockConfig.tools.config?.tool
? blockConfig.tools.config.tool(params)
: blockConfig.tools.access[0]
} catch (error) {
logger.warn('Tool selection failed during serialization, using default:', {
error: error instanceof Error ? error.message : String(error),
})
toolId = blockConfig.tools.access[0]
}
}
// Get inputs from block config