Skip to content

Commit d5ea6ab

Browse files
committed
fix(polling-tools): pass plan execution timeout to internal polling tool routes
1 parent e761043 commit d5ea6ab

5 files changed

Lines changed: 54 additions & 28 deletions

File tree

apps/sim/app/api/tools/image/route.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ const MAX_IMAGE_BYTES = 25 * 1024 * 1024
3939
const MAX_IMAGE_JSON_BYTES = Math.ceil((MAX_IMAGE_BYTES * 4) / 3) + 256 * 1024
4040

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

4449
type ImageProvider = (typeof imageProviders)[number]
4550

apps/sim/app/api/tools/stt/route.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { sttToolContract } from '@/lib/api/contracts/tools/media/stt'
77
import { getValidationErrorMessage, parseRequest, validationErrorResponse } from '@/lib/api/server'
88
import { extractAudioFromVideo, isVideoFile } from '@/lib/audio/extractor'
99
import { checkInternalAuth } from '@/lib/auth/hybrid'
10-
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
10+
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
1111
import {
1212
secureFetchWithPinnedIP,
1313
validateUrlWithDNS,
@@ -25,7 +25,12 @@ const logger = createLogger('SttProxyAPI')
2525
const ELEVENLABS_STT_MODEL = 'scribe_v2'
2626

2727
export const dynamic = 'force-dynamic'
28-
export const maxDuration = 300 // 5 minutes for large files
28+
/**
29+
* Mirrors the maximum plan execution timeout (enterprise async, 90 minutes) used by
30+
* `getMaxExecutionTimeout()` for the transcript polling loop below. Next.js requires a
31+
* static literal for `maxDuration`, so this value must be kept in sync with that source.
32+
*/
33+
export const maxDuration = 5400
2934

3035
export const POST = withRouteHandler(async (request: NextRequest) => {
3136
const requestId = generateId()
@@ -629,7 +634,7 @@ async function transcribeWithAssemblyAI(
629634
let transcript: any
630635
let attempts = 0
631636
const pollIntervalMs = 5000
632-
const maxAttempts = Math.ceil(DEFAULT_EXECUTION_TIMEOUT_MS / pollIntervalMs)
637+
const maxAttempts = Math.ceil(getMaxExecutionTimeout() / pollIntervalMs)
633638

634639
while (attempts < maxAttempts) {
635640
const statusResponse = await fetch(`https://api.assemblyai.com/v2/transcript/${id}`, {

apps/sim/app/api/tools/textract/parse/route.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
66
import { textractParseContract } from '@/lib/api/contracts/tools/media/document-parse'
77
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
88
import { checkInternalAuth } from '@/lib/auth/hybrid'
9-
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
9+
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
1010
import { validateS3BucketName } from '@/lib/core/security/input-validation'
1111
import {
1212
secureFetchWithPinnedIP,
@@ -22,7 +22,12 @@ import {
2222
import { assertToolFileAccess } from '@/app/api/files/authorization'
2323

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

2732
const logger = createLogger('TextractParseAPI')
2833

@@ -184,7 +189,7 @@ async function pollForJobCompletion(
184189
requestId: string
185190
): Promise<Record<string, unknown>> {
186191
const pollIntervalMs = 5000
187-
const maxPollTimeMs = DEFAULT_EXECUTION_TIMEOUT_MS
192+
const maxPollTimeMs = getMaxExecutionTimeout()
188193
const maxAttempts = Math.ceil(maxPollTimeMs / pollIntervalMs)
189194

190195
const getTarget = useAnalyzeDocument

apps/sim/app/api/tools/video/route.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ const MAX_VIDEO_REFERENCE_IMAGE_BYTES = 25 * 1024 * 1024
2828
const MAX_VIDEO_JSON_BYTES = 2 * 1024 * 1024
2929

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

3338
async function readVideoResponseBuffer(response: Response, label: string): Promise<Buffer> {
3439
return readResponseToBufferWithLimit(response, {

apps/sim/tools/index.ts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { randomFloat } from '@sim/utils/random'
55
import { getBYOKKey } from '@/lib/api-key/byok'
66
import { generateInternalToken } from '@/lib/auth/internal'
77
import { isHosted } from '@/lib/core/config/feature-flags'
8-
import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
8+
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
99
import { getHostedKeyRateLimiter } from '@/lib/core/rate-limiter'
1010
import {
1111
secureFetchWithPinnedIP,
@@ -879,6 +879,9 @@ export async function executeTool(
879879
options: ExecuteToolOptions = {}
880880
): Promise<ToolResponse> {
881881
const { skipPostProcess = false, executionContext, signal } = options
882+
// Fall back to the workflow execution's abort signal so plan-based execution timeouts
883+
// and cancellation propagate to tool fetches when the caller passes no explicit signal.
884+
const effectiveSignal = signal ?? executionContext?.abortSignal
882885
// Capture start time for precise timing
883886
const startTime = new Date()
884887
const startTimeISO = startTime.toISOString()
@@ -972,7 +975,7 @@ export async function executeTool(
972975
executionContext,
973976
requestId,
974977
startTimeISO,
975-
signal
978+
effectiveSignal
976979
)
977980
} else {
978981
// For built-in tools, use the synchronous version
@@ -1169,23 +1172,26 @@ export async function executeTool(
11691172
// Execute the tool request directly (internal routes use regular fetch, external use SSRF-protected fetch)
11701173
// Wrap with retry logic for hosted keys to handle rate limiting due to higher usage
11711174
const result = hostedKeyInfo.isUsingHostedKey
1172-
? await executeWithRetry(() => executeToolRequest(toolId, tool, contextParams, signal), {
1173-
requestId,
1174-
toolId,
1175-
envVarName: hostedKeyInfo.envVarName!,
1176-
executionContext,
1177-
reacquireAfterRetriesExhausted: async () => {
1178-
const reacquired = await reacquireHostedKey(
1179-
tool,
1180-
contextParams,
1181-
executionContext,
1182-
requestId
1183-
)
1184-
if (!reacquired) return null
1185-
return () => executeToolRequest(toolId, tool, contextParams)
1186-
},
1187-
})
1188-
: await executeToolRequest(toolId, tool, contextParams, signal)
1175+
? await executeWithRetry(
1176+
() => executeToolRequest(toolId, tool, contextParams, effectiveSignal),
1177+
{
1178+
requestId,
1179+
toolId,
1180+
envVarName: hostedKeyInfo.envVarName!,
1181+
executionContext,
1182+
reacquireAfterRetriesExhausted: async () => {
1183+
const reacquired = await reacquireHostedKey(
1184+
tool,
1185+
contextParams,
1186+
executionContext,
1187+
requestId
1188+
)
1189+
if (!reacquired) return null
1190+
return () => executeToolRequest(toolId, tool, contextParams, effectiveSignal)
1191+
},
1192+
}
1193+
)
1194+
: await executeToolRequest(toolId, tool, contextParams, effectiveSignal)
11891195

11901196
// Apply post-processing if available and not skipped
11911197
let finalResult = result
@@ -1576,7 +1582,7 @@ async function executeToolRequest(
15761582
try {
15771583
if (isInternalRoute) {
15781584
const controller = new AbortController()
1579-
const timeout = requestParams.timeout || DEFAULT_EXECUTION_TIMEOUT_MS
1585+
const timeout = requestParams.timeout || getMaxExecutionTimeout()
15801586
const timeoutId = setTimeout(
15811587
() => controller.abort(`timeout:internal_tool_fetch:${timeout}ms`),
15821588
timeout

0 commit comments

Comments
 (0)