Skip to content
4 changes: 2 additions & 2 deletions apps/sim/app/api/tools/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export async function POST(request: NextRequest) {
query: validated.query,
type: 'auto',
useAutoprompt: true,
text: true,
highlights: true,
apiKey: env.EXA_API_KEY,
})

Expand All @@ -77,7 +77,7 @@ export async function POST(request: NextRequest) {
const results = (result.output.results || []).map((r: any, index: number) => ({
title: r.title || '',
link: r.url || '',
snippet: r.text || '',
snippet: Array.isArray(r.highlights) ? r.highlights.join(' ... ') : '',
date: r.publishedDate || undefined,
position: index + 1,
}))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'https://www.googleapis.com/auth/admin.directory.group.readonly': 'View Google Workspace groups',
'https://www.googleapis.com/auth/admin.directory.group.member.readonly':
'View Google Workspace group memberships',
'https://www.googleapis.com/auth/cloud-platform':
'Full access to Google Cloud resources for Vertex AI',
'read:confluence-content.all': 'Read all Confluence content',
'read:confluence-space.summary': 'Read Confluence space information',
'read:space:confluence': 'View Confluence spaces',
Expand Down
80 changes: 75 additions & 5 deletions apps/sim/blocks/blocks/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {
getMaxTemperature,
getProviderIcon,
getReasoningEffortValuesForModel,
getThinkingLevelsForModel,
getVerbosityValuesForModel,
MODELS_WITH_REASONING_EFFORT,
MODELS_WITH_THINKING,
MODELS_WITH_VERBOSITY,
providers,
supportsTemperature,
Expand Down Expand Up @@ -108,7 +110,19 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
})
},
},

{
id: 'vertexCredential',
title: 'Google Cloud Account',
type: 'oauth-input',
serviceId: 'vertex-ai',
requiredScopes: ['https://www.googleapis.com/auth/cloud-platform'],
placeholder: 'Select Google Cloud account',
required: true,
condition: {
field: 'model',
value: providers.vertex.models,
},
},
{
id: 'reasoningEffort',
title: 'Reasoning Effort',
Expand Down Expand Up @@ -215,6 +229,57 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
value: MODELS_WITH_VERBOSITY,
},
},
{
id: 'thinkingLevel',
title: 'Thinking Level',
type: 'dropdown',
placeholder: 'Select thinking level...',
options: [
{ label: 'minimal', id: 'minimal' },
{ label: 'low', id: 'low' },
{ label: 'medium', id: 'medium' },
{ label: 'high', id: 'high' },
],
dependsOn: ['model'],
fetchOptions: async (blockId: string) => {
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')

const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!activeWorkflowId) {
return [
{ label: 'low', id: 'low' },
{ label: 'high', id: 'high' },
]
}

const workflowValues = useSubBlockStore.getState().workflowValues[activeWorkflowId]
const blockValues = workflowValues?.[blockId]
const modelValue = blockValues?.model as string

if (!modelValue) {
return [
{ label: 'low', id: 'low' },
{ label: 'high', id: 'high' },
]
}

const validOptions = getThinkingLevelsForModel(modelValue)
if (!validOptions) {
return [
{ label: 'low', id: 'low' },
{ label: 'high', id: 'high' },
]
}

return validOptions.map((opt) => ({ label: opt, id: opt }))
},
value: () => 'high',
condition: {
field: 'model',
value: MODELS_WITH_THINKING,
},
},

{
id: 'azureEndpoint',
Expand Down Expand Up @@ -275,17 +340,21 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
password: true,
connectionDroppable: false,
required: true,
// Hide API key for hosted models, Ollama models, and vLLM models
// Hide API key for hosted models, Ollama models, vLLM models, and Vertex models (uses OAuth)
condition: isHosted
? {
field: 'model',
value: getHostedModels(),
value: [...getHostedModels(), ...providers.vertex.models],
not: true, // Show for all models EXCEPT those listed
}
: () => ({
field: 'model',
value: [...getCurrentOllamaModels(), ...getCurrentVLLMModels()],
not: true, // Show for all models EXCEPT Ollama and vLLM models
value: [
...getCurrentOllamaModels(),
...getCurrentVLLMModels(),
...providers.vertex.models,
],
not: true, // Show for all models EXCEPT Ollama, vLLM, and Vertex models
}),
},
{
Expand Down Expand Up @@ -609,6 +678,7 @@ Example 3 (Array Input):
temperature: { type: 'number', description: 'Response randomness level' },
reasoningEffort: { type: 'string', description: 'Reasoning effort level for GPT-5 models' },
verbosity: { type: 'string', description: 'Verbosity level for GPT-5 models' },
thinkingLevel: { type: 'string', description: 'Thinking level for Gemini 3 models' },
tools: { type: 'json', description: 'Available tools configuration' },
},
outputs: {
Expand Down
68 changes: 41 additions & 27 deletions apps/sim/executor/handlers/agent/agent-handler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { db } from '@sim/db'
import { mcpServers } from '@sim/db/schema'
import { account, mcpServers } from '@sim/db/schema'
import { and, eq, inArray, isNull } from 'drizzle-orm'
import { createLogger } from '@/lib/logs/console/logger'
import { createMcpToolId } from '@/lib/mcp/utils'
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { getAllBlocks } from '@/blocks'
import type { BlockOutput } from '@/blocks/types'
import { AGENT, BlockType, DEFAULTS, HTTP } from '@/executor/constants'
Expand Down Expand Up @@ -919,6 +920,7 @@ export class AgentBlockHandler implements BlockHandler {
azureApiVersion: inputs.azureApiVersion,
vertexProject: inputs.vertexProject,
vertexLocation: inputs.vertexLocation,
vertexCredential: inputs.vertexCredential,
responseFormat,
workflowId: ctx.workflowId,
workspaceId: ctx.workspaceId,
Expand Down Expand Up @@ -997,7 +999,17 @@ export class AgentBlockHandler implements BlockHandler {
responseFormat: any,
providerStartTime: number
) {
const finalApiKey = this.getApiKey(providerId, model, providerRequest.apiKey)
let finalApiKey: string

// For Vertex AI, resolve OAuth credential to access token
if (providerId === 'vertex' && providerRequest.vertexCredential) {
finalApiKey = await this.resolveVertexCredential(
providerRequest.vertexCredential,
ctx.workflowId
)
} else {
finalApiKey = this.getApiKey(providerId, model, providerRequest.apiKey)
}

const { blockData, blockNameMapping } = collectBlockData(ctx)

Expand All @@ -1024,7 +1036,6 @@ export class AgentBlockHandler implements BlockHandler {
blockNameMapping,
})

this.logExecutionSuccess(providerId, model, ctx, block, providerStartTime, response)
return this.processProviderResponse(response, block, responseFormat)
}

Expand All @@ -1049,15 +1060,6 @@ export class AgentBlockHandler implements BlockHandler {
throw new Error(errorMessage)
}

this.logExecutionSuccess(
providerRequest.provider,
providerRequest.model,
ctx,
block,
providerStartTime,
'HTTP response'
)

const contentType = response.headers.get('Content-Type')
if (contentType?.includes(HTTP.CONTENT_TYPE.EVENT_STREAM)) {
return this.handleStreamingResponse(response, block, ctx, inputs)
Expand Down Expand Up @@ -1117,21 +1119,33 @@ export class AgentBlockHandler implements BlockHandler {
}
}

private logExecutionSuccess(
provider: string,
model: string,
ctx: ExecutionContext,
block: SerializedBlock,
startTime: number,
response: any
) {
const executionTime = Date.now() - startTime
const responseType =
response instanceof ReadableStream
? 'stream'
: response && typeof response === 'object' && 'stream' in response
? 'streaming-execution'
: 'json'
/**
* Resolves a Vertex AI OAuth credential to an access token
*/
private async resolveVertexCredential(credentialId: string, workflowId: string): Promise<string> {
const requestId = `vertex-${Date.now()}`

logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`)

// Get the credential - we need to find the owner
// Since we're in a workflow context, we can query the credential directly
const credential = await db.query.account.findFirst({
where: eq(account.id, credentialId),
})

if (!credential) {
throw new Error(`Vertex AI credential not found: ${credentialId}`)
}

// Refresh the token if needed
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)

if (!accessToken) {
throw new Error('Failed to get Vertex AI access token')
}

logger.info(`[${requestId}] Successfully resolved Vertex AI credential`)
return accessToken
}

private handleExecutionError(
Expand Down
1 change: 1 addition & 0 deletions apps/sim/executor/handlers/agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface AgentInputs {
azureApiVersion?: string
vertexProject?: string
vertexLocation?: string
vertexCredential?: string
reasoningEffort?: string
verbosity?: string
}
Expand Down
15 changes: 15 additions & 0 deletions apps/sim/lib/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,21 @@ export const auth = betterAuth({
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/google-groups`,
},

{
providerId: 'vertex-ai',
clientId: env.GOOGLE_CLIENT_ID as string,
clientSecret: env.GOOGLE_CLIENT_SECRET as string,
discoveryUrl: 'https://accounts.google.com/.well-known/openid-configuration',
accessType: 'offline',
scopes: [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/cloud-platform',
],
prompt: 'consent',
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/vertex-ai`,
},

{
providerId: 'microsoft-teams',
clientId: env.MICROSOFT_CLIENT_ID as string,
Expand Down
30 changes: 22 additions & 8 deletions apps/sim/lib/core/utils/display-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ function filterUserFile(data: any): any {
const DISPLAY_FILTERS = [filterUserFile]

export function filterForDisplay(data: any): any {
const seen = new WeakSet()
const seen = new Set<object>()
return filterForDisplayInternal(data, seen, 0)
}

function getObjectType(data: unknown): string {
return Object.prototype.toString.call(data).slice(8, -1)
}

function filterForDisplayInternal(data: any, seen: WeakSet<object>, depth: number): any {
function filterForDisplayInternal(data: any, seen: Set<object>, depth: number): any {
try {
if (data === null || data === undefined) {
return data
Expand Down Expand Up @@ -93,6 +93,7 @@ function filterForDisplayInternal(data: any, seen: WeakSet<object>, depth: numbe
return '[Unknown Type]'
}

// True circular reference: object is an ancestor in the current path
if (seen.has(data)) {
return '[Circular Reference]'
}
Expand Down Expand Up @@ -131,18 +132,24 @@ function filterForDisplayInternal(data: any, seen: WeakSet<object>, depth: numbe
return `[ArrayBuffer: ${(data as ArrayBuffer).byteLength} bytes]`

case 'Map': {
seen.add(data)
const obj: Record<string, any> = {}
for (const [key, value] of (data as Map<any, any>).entries()) {
const keyStr = typeof key === 'string' ? key : String(key)
obj[keyStr] = filterForDisplayInternal(value, seen, depth + 1)
}
seen.delete(data)
return obj
}

case 'Set':
return Array.from(data as Set<any>).map((item) =>
case 'Set': {
seen.add(data)
const result = Array.from(data as Set<any>).map((item) =>
filterForDisplayInternal(item, seen, depth + 1)
)
seen.delete(data)
return result
}

case 'WeakMap':
return '[WeakMap]'
Expand All @@ -161,17 +168,22 @@ function filterForDisplayInternal(data: any, seen: WeakSet<object>, depth: numbe
return `[${objectType}: ${(data as ArrayBufferView).byteLength} bytes]`
}

// Add to current path before processing children
seen.add(data)

for (const filterFn of DISPLAY_FILTERS) {
const result = filterFn(data)
if (result !== data) {
return filterForDisplayInternal(result, seen, depth + 1)
const filtered = filterFn(data)
if (filtered !== data) {
const result = filterForDisplayInternal(filtered, seen, depth + 1)
seen.delete(data)
return result
}
}

if (Array.isArray(data)) {
return data.map((item) => filterForDisplayInternal(item, seen, depth + 1))
const result = data.map((item) => filterForDisplayInternal(item, seen, depth + 1))
seen.delete(data)
return result
}

const result: Record<string, any> = {}
Expand All @@ -182,6 +194,8 @@ function filterForDisplayInternal(data: any, seen: WeakSet<object>, depth: numbe
result[key] = '[Error accessing property]'
}
}
// Remove from current path after processing children
seen.delete(data)
return result
} catch {
return '[Unserializable]'
Expand Down
Loading
Loading