@@ -29,47 +29,61 @@ import {
2929 getToolAsync ,
3030 validateRequiredParametersAfterMerge ,
3131} from '@/tools/utils'
32+ import { PlatformEvents } from '@/lib/core/telemetry'
3233
3334const logger = createLogger ( 'Tools' )
3435
36+ /** Result from hosted key lookup */
37+ interface HostedKeyResult {
38+ key : string
39+ envVarName : string
40+ }
41+
3542/**
3643 * Get a hosted API key from environment variables
3744 * Supports rotation when multiple keys are configured
45+ * Returns both the key and which env var it came from
3846 */
39- function getHostedKeyFromEnv ( envKeys : string [ ] ) : string | null {
40- const keys = envKeys
41- . map ( ( key ) => env [ key as keyof typeof env ] )
42- . filter ( ( value ) : value is string => Boolean ( value ) )
47+ function getHostedKeyFromEnv ( envKeys : string [ ] ) : HostedKeyResult | null {
48+ const keysWithNames = envKeys
49+ . map ( ( envVarName ) => ( { envVarName , key : env [ envVarName as keyof typeof env ] } ) )
50+ . filter ( ( item ) : item is { envVarName : string ; key : string } => Boolean ( item . key ) )
4351
44- if ( keys . length === 0 ) return null
52+ if ( keysWithNames . length === 0 ) return null
4553
4654 // Round-robin rotation based on current minute
4755 const currentMinute = Math . floor ( Date . now ( ) / 60000 )
48- const keyIndex = currentMinute % keys . length
56+ const keyIndex = currentMinute % keysWithNames . length
57+
58+ return keysWithNames [ keyIndex ]
59+ }
4960
50- return keys [ keyIndex ]
61+ /** Result from hosted key injection */
62+ interface HostedKeyInjectionResult {
63+ isUsingHostedKey : boolean
64+ envVarName ?: string
5165}
5266
5367/**
5468 * Inject hosted API key if tool supports it and user didn't provide one.
5569 * Checks BYOK workspace keys first, then falls back to hosted env keys.
56- * Returns whether a hosted (billable) key was injected.
70+ * Returns whether a hosted (billable) key was injected and which env var it came from .
5771 */
5872async function injectHostedKeyIfNeeded (
5973 tool : ToolConfig ,
6074 params : Record < string , unknown > ,
6175 executionContext : ExecutionContext | undefined ,
6276 requestId : string
63- ) : Promise < boolean > {
64- if ( ! tool . hosting ) return false
65- if ( ! isHosted ) return false
77+ ) : Promise < HostedKeyInjectionResult > {
78+ if ( ! tool . hosting ) return { isUsingHostedKey : false }
79+ if ( ! isHosted ) return { isUsingHostedKey : false }
6680
6781 const { envKeys, apiKeyParam, byokProviderId } = tool . hosting
6882 const userProvidedKey = params [ apiKeyParam ]
6983
7084 if ( userProvidedKey ) {
7185 logger . debug ( `[${ requestId } ] User provided API key for ${ tool . id } , skipping hosted key` )
72- return false
86+ return { isUsingHostedKey : false }
7387 }
7488
7589 // Check BYOK workspace key first
@@ -82,7 +96,7 @@ async function injectHostedKeyIfNeeded(
8296 if ( byokResult ) {
8397 params [ apiKeyParam ] = byokResult . apiKey
8498 logger . info ( `[${ requestId } ] Using BYOK key for ${ tool . id } ` )
85- return false // Don't bill - user's own key
99+ return { isUsingHostedKey : false } // Don't bill - user's own key
86100 }
87101 } catch ( error ) {
88102 logger . error ( `[${ requestId } ] Failed to get BYOK key for ${ tool . id } :` , error )
@@ -91,15 +105,15 @@ async function injectHostedKeyIfNeeded(
91105 }
92106
93107 // Fall back to hosted env key
94- const hostedKey = getHostedKeyFromEnv ( envKeys )
95- if ( ! hostedKey ) {
108+ const hostedKeyResult = getHostedKeyFromEnv ( envKeys )
109+ if ( ! hostedKeyResult ) {
96110 logger . debug ( `[${ requestId } ] No hosted key available for ${ tool . id } ` )
97- return false
111+ return { isUsingHostedKey : false }
98112 }
99113
100- params [ apiKeyParam ] = hostedKey
101- logger . info ( `[${ requestId } ] Using hosted key for ${ tool . id } ` )
102- return true // Bill the user
114+ params [ apiKeyParam ] = hostedKeyResult . key
115+ logger . info ( `[${ requestId } ] Using hosted key for ${ tool . id } ( ${ hostedKeyResult . envVarName } ) ` )
116+ return { isUsingHostedKey : true , envVarName : hostedKeyResult . envVarName }
103117}
104118
105119/**
@@ -114,17 +128,25 @@ function isRateLimitError(error: unknown): boolean {
114128 return false
115129}
116130
131+ /** Context for retry with throttle tracking */
132+ interface RetryContext {
133+ requestId : string
134+ toolId : string
135+ envVarName : string
136+ executionContext ?: ExecutionContext
137+ }
138+
117139/**
118140 * Execute a function with exponential backoff retry for rate limiting errors.
119- * Only used for hosted key requests.
141+ * Only used for hosted key requests. Tracks throttling events via telemetry.
120142 */
121143async function executeWithRetry < T > (
122144 fn : ( ) => Promise < T > ,
123- requestId : string ,
124- toolId : string ,
145+ context : RetryContext ,
125146 maxRetries = 3 ,
126147 baseDelayMs = 1000
127148) : Promise < T > {
149+ const { requestId, toolId, envVarName, executionContext } = context
128150 let lastError : unknown
129151
130152 for ( let attempt = 0 ; attempt <= maxRetries ; attempt ++ ) {
@@ -138,7 +160,20 @@ async function executeWithRetry<T>(
138160 }
139161
140162 const delayMs = baseDelayMs * Math . pow ( 2 , attempt )
141- logger . warn ( `[${ requestId } ] Rate limited for ${ toolId } , retrying in ${ delayMs } ms (attempt ${ attempt + 1 } /${ maxRetries } )` )
163+
164+ // Track throttling event via telemetry
165+ PlatformEvents . hostedKeyThrottled ( {
166+ toolId,
167+ envVarName,
168+ attempt : attempt + 1 ,
169+ maxRetries,
170+ delayMs,
171+ userId : executionContext ?. userId ,
172+ workspaceId : executionContext ?. workspaceId ,
173+ workflowId : executionContext ?. workflowId ,
174+ } )
175+
176+ logger . warn ( `[${ requestId } ] Rate limited for ${ toolId } (${ envVarName } ), retrying in ${ delayMs } ms (attempt ${ attempt + 1 } /${ maxRetries } )` )
142177 await new Promise ( ( resolve ) => setTimeout ( resolve , delayMs ) )
143178 }
144179 }
@@ -480,7 +515,7 @@ export async function executeTool(
480515 }
481516
482517 // Inject hosted API key if tool supports it and user didn't provide one
483- const isUsingHostedKey = await injectHostedKeyIfNeeded (
518+ const hostedKeyInfo = await injectHostedKeyIfNeeded (
484519 tool ,
485520 contextParams ,
486521 executionContext ,
@@ -596,7 +631,7 @@ export async function executeTool(
596631 finalResult = await processFileOutputs ( finalResult , tool , executionContext )
597632
598633 // Log usage for hosted key if execution was successful
599- if ( isUsingHostedKey && finalResult . success ) {
634+ if ( hostedKeyInfo . isUsingHostedKey && finalResult . success ) {
600635 await logHostedToolUsage ( tool , contextParams , finalResult . output , executionContext , requestId )
601636 }
602637
@@ -616,11 +651,15 @@ export async function executeTool(
616651
617652 // Execute the tool request directly (internal routes use regular fetch, external use SSRF-protected fetch)
618653 // Wrap with retry logic for hosted keys to handle rate limiting due to higher usage
619- const result = isUsingHostedKey
654+ const result = hostedKeyInfo . isUsingHostedKey
620655 ? await executeWithRetry (
621656 ( ) => executeToolRequest ( toolId , tool , contextParams ) ,
622- requestId ,
623- toolId
657+ {
658+ requestId,
659+ toolId,
660+ envVarName : hostedKeyInfo . envVarName ! ,
661+ executionContext,
662+ }
624663 )
625664 : await executeToolRequest ( toolId , tool , contextParams )
626665
@@ -641,7 +680,7 @@ export async function executeTool(
641680 finalResult = await processFileOutputs ( finalResult , tool , executionContext )
642681
643682 // Log usage for hosted key if execution was successful
644- if ( isUsingHostedKey && finalResult . success ) {
683+ if ( hostedKeyInfo . isUsingHostedKey && finalResult . success ) {
645684 await logHostedToolUsage ( tool , contextParams , finalResult . output , executionContext , requestId )
646685 }
647686
0 commit comments