Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/sim/app/api/tools/image/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ const MAX_IMAGE_BYTES = 25 * 1024 * 1024
const MAX_IMAGE_JSON_BYTES = Math.ceil((MAX_IMAGE_BYTES * 4) / 3) + 256 * 1024

export const dynamic = 'force-dynamic'
export const maxDuration = 600
/**
* Mirrors the maximum plan execution timeout (enterprise async, 90 minutes) used by
* `getMaxExecutionTimeout()` for the provider polling loop below. Next.js requires a
* static literal for `maxDuration`, so this value must be kept in sync with that source.
*/
export const maxDuration = 5400

type ImageProvider = (typeof imageProviders)[number]

Expand Down
11 changes: 8 additions & 3 deletions apps/sim/app/api/tools/stt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { sttToolContract } from '@/lib/api/contracts/tools/media/stt'
import { getValidationErrorMessage, parseRequest, validationErrorResponse } from '@/lib/api/server'
import { extractAudioFromVideo, isVideoFile } from '@/lib/audio/extractor'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
import {
secureFetchWithPinnedIP,
validateUrlWithDNS,
Expand All @@ -25,7 +25,12 @@ const logger = createLogger('SttProxyAPI')
const ELEVENLABS_STT_MODEL = 'scribe_v2'

export const dynamic = 'force-dynamic'
export const maxDuration = 300 // 5 minutes for large files
/**
* Mirrors the maximum plan execution timeout (enterprise async, 90 minutes) used by
* `getMaxExecutionTimeout()` for the transcript polling loop below. Next.js requires a
* static literal for `maxDuration`, so this value must be kept in sync with that source.
*/
Comment thread
icecrasher321 marked this conversation as resolved.
export const maxDuration = 5400

export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId()
Expand Down Expand Up @@ -629,7 +634,7 @@ async function transcribeWithAssemblyAI(
let transcript: any
let attempts = 0
const pollIntervalMs = 5000
const maxAttempts = Math.ceil(DEFAULT_EXECUTION_TIMEOUT_MS / pollIntervalMs)
const maxAttempts = Math.ceil(getMaxExecutionTimeout() / pollIntervalMs)

while (attempts < maxAttempts) {
const statusResponse = await fetch(`https://api.assemblyai.com/v2/transcript/${id}`, {
Expand Down
11 changes: 8 additions & 3 deletions apps/sim/app/api/tools/textract/parse/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { textractParseContract } from '@/lib/api/contracts/tools/media/document-parse'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
import { validateS3BucketName } from '@/lib/core/security/input-validation'
import {
secureFetchWithPinnedIP,
Expand All @@ -22,7 +22,12 @@ import {
import { assertToolFileAccess } from '@/app/api/files/authorization'

export const dynamic = 'force-dynamic'
export const maxDuration = 300 // 5 minutes for large multi-page PDF processing
/**
* Mirrors the maximum plan execution timeout (enterprise async, 90 minutes) used by
* `getMaxExecutionTimeout()` for the job polling loop below. Next.js requires a static
* literal for `maxDuration`, so this value must be kept in sync with that source.
*/
export const maxDuration = 5400

const logger = createLogger('TextractParseAPI')

Expand Down Expand Up @@ -184,7 +189,7 @@ async function pollForJobCompletion(
requestId: string
): Promise<Record<string, unknown>> {
const pollIntervalMs = 5000
const maxPollTimeMs = DEFAULT_EXECUTION_TIMEOUT_MS
const maxPollTimeMs = getMaxExecutionTimeout()
const maxAttempts = Math.ceil(maxPollTimeMs / pollIntervalMs)

const getTarget = useAnalyzeDocument
Expand Down
7 changes: 6 additions & 1 deletion apps/sim/app/api/tools/video/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ const MAX_VIDEO_REFERENCE_IMAGE_BYTES = 25 * 1024 * 1024
const MAX_VIDEO_JSON_BYTES = 2 * 1024 * 1024

export const dynamic = 'force-dynamic'
export const maxDuration = 600 // 10 minutes for video generation
/**
* Mirrors the maximum plan execution timeout (enterprise async, 90 minutes) used by
* `getMaxExecutionTimeout()` for the provider polling loops below. Next.js requires a
* static literal for `maxDuration`, so this value must be kept in sync with that source.
*/
export const maxDuration = 5400

async function readVideoResponseBuffer(response: Response, label: string): Promise<Buffer> {
return readResponseToBufferWithLimit(response, {
Expand Down
50 changes: 30 additions & 20 deletions apps/sim/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { randomFloat } from '@sim/utils/random'
import { getBYOKKey } from '@/lib/api-key/byok'
import { generateInternalToken } from '@/lib/auth/internal'
import { isHosted } from '@/lib/core/config/feature-flags'
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
import { DEFAULT_EXECUTION_TIMEOUT_MS, getMaxExecutionTimeout } from '@/lib/core/execution-limits'
import { getHostedKeyRateLimiter } from '@/lib/core/rate-limiter'
import {
secureFetchWithPinnedIP,
Expand Down Expand Up @@ -879,6 +879,9 @@ export async function executeTool(
options: ExecuteToolOptions = {}
): Promise<ToolResponse> {
const { skipPostProcess = false, executionContext, signal } = options
// Fall back to the workflow execution's abort signal so plan-based execution timeouts
// and cancellation propagate to tool fetches when the caller passes no explicit signal.
const effectiveSignal = signal ?? executionContext?.abortSignal
// Capture start time for precise timing
const startTime = new Date()
const startTimeISO = startTime.toISOString()
Expand Down Expand Up @@ -972,7 +975,7 @@ export async function executeTool(
executionContext,
requestId,
startTimeISO,
signal
effectiveSignal
)
} else {
// For built-in tools, use the synchronous version
Expand Down Expand Up @@ -1169,23 +1172,26 @@ export async function executeTool(
// Execute the tool request directly (internal routes use regular fetch, external use SSRF-protected fetch)
// Wrap with retry logic for hosted keys to handle rate limiting due to higher usage
const result = hostedKeyInfo.isUsingHostedKey
? await executeWithRetry(() => executeToolRequest(toolId, tool, contextParams, signal), {
requestId,
toolId,
envVarName: hostedKeyInfo.envVarName!,
executionContext,
reacquireAfterRetriesExhausted: async () => {
const reacquired = await reacquireHostedKey(
tool,
contextParams,
executionContext,
requestId
)
if (!reacquired) return null
return () => executeToolRequest(toolId, tool, contextParams)
},
})
: await executeToolRequest(toolId, tool, contextParams, signal)
? await executeWithRetry(
() => executeToolRequest(toolId, tool, contextParams, effectiveSignal),
{
requestId,
toolId,
envVarName: hostedKeyInfo.envVarName!,
executionContext,
reacquireAfterRetriesExhausted: async () => {
const reacquired = await reacquireHostedKey(
tool,
contextParams,
executionContext,
requestId
)
if (!reacquired) return null
return () => executeToolRequest(toolId, tool, contextParams, effectiveSignal)
},
}
)
: await executeToolRequest(toolId, tool, contextParams, effectiveSignal)

// Apply post-processing if available and not skipped
let finalResult = result
Expand Down Expand Up @@ -1576,7 +1582,11 @@ async function executeToolRequest(
try {
if (isInternalRoute) {
const controller = new AbortController()
const timeout = requestParams.timeout || DEFAULT_EXECUTION_TIMEOUT_MS
// With a caller/execution abort signal present, the plan-based timeout bounds the call and
// this only acts as a ceiling; without one, keep the tighter default as the hang safety net.
const timeout =
requestParams.timeout ||
(signal ? getMaxExecutionTimeout() : DEFAULT_EXECUTION_TIMEOUT_MS)
const timeoutId = setTimeout(
() => controller.abort(`timeout:internal_tool_fetch:${timeout}ms`),
timeout
Expand Down
Loading