@@ -9,12 +9,14 @@ const {
99 mockGetApiKeyWithBYOK,
1010 mockExecuteRequest,
1111 mockFilterModelSafeWorkspaceFileAttachments,
12+ mockExecuteTool,
1213 mockUploadLargeFilesToProvider,
1314} = vi . hoisted ( ( ) => ( {
1415 mockAttachLargeFileRemoteUrls : vi . fn ( ) ,
1516 mockGetApiKeyWithBYOK : vi . fn ( ) ,
1617 mockExecuteRequest : vi . fn ( ) ,
1718 mockFilterModelSafeWorkspaceFileAttachments : vi . fn ( async ( attachments : unknown [ ] ) => attachments ) ,
19+ mockExecuteTool : vi . fn ( async ( ) => ( { success : true , output : { } } ) ) ,
1820 mockUploadLargeFilesToProvider : vi . fn ( ) ,
1921} ) )
2022
@@ -39,9 +41,16 @@ vi.mock('@/lib/uploads/contexts/workspace/workspace-file-secret-provenance', ()
3941 mockFilterModelSafeWorkspaceFileAttachments ( ...args ) ,
4042} ) )
4143
44+ vi . mock ( '@/tools' , ( ) => ( {
45+ executeTool : ( ...args : unknown [ ] ) => mockExecuteTool ( ...args ) ,
46+ } ) )
47+
48+ import type { NormalizedBlockOutput , StreamingExecution } from '@/executor/types'
4249import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
4350import { executeProviderRequest } from '@/providers'
44- import type { ProviderResponse } from '@/providers/types'
51+ import { executeProviderTool } from '@/providers/runtime-context'
52+ import type { AgentStreamEvent } from '@/providers/stream-events'
53+ import type { ProviderResponse , ProviderToolConfig } from '@/providers/types'
4554
4655const HOSTED_RATE_INPUT_COST = 0.340285
4756const HOSTED_RATE_OUTPUT_COST = 0.0387
@@ -91,6 +100,102 @@ function makeAnthropicResponse(): ProviderResponse {
91100 }
92101}
93102
103+ function makeProviderTool ( id : string , credential : string ) : ProviderToolConfig {
104+ return {
105+ id,
106+ name : id ,
107+ description : id ,
108+ params : { oauthCredential : credential } ,
109+ parameters : { type : 'object' , properties : { } , required : [ ] } ,
110+ }
111+ }
112+
113+ describe ( 'executeProviderRequest — tool identities' , ( ) => {
114+ beforeEach ( ( ) => {
115+ vi . clearAllMocks ( )
116+ } )
117+
118+ it ( 'sends unique opaque ids and projects provider aliases out of the response' , async ( ) => {
119+ const tools = [
120+ makeProviderTool ( 'gmail_send' , 'credential-a' ) ,
121+ makeProviderTool ( 'gmail_send' , 'credential-b' ) ,
122+ ]
123+ mockExecuteRequest . mockImplementationOnce ( async ( request ) => {
124+ const alias = request . tools [ 1 ] . id
125+ expect ( request . tools . map ( ( tool : ProviderToolConfig ) => tool . id ) ) . toEqual ( [
126+ 'gmail_send' ,
127+ 'gmail_send__sim_2' ,
128+ ] )
129+ expect ( alias ) . not . toContain ( 'credential-b' )
130+ return {
131+ content : 'sent' ,
132+ model : 'test-model' ,
133+ toolCalls : [ { name : alias , arguments : { } } ] ,
134+ timing : {
135+ startTime : 'start' ,
136+ endTime : 'end' ,
137+ duration : 1 ,
138+ timeSegments : [ { type : 'tool' , name : alias , startTime : 0 , endTime : 1 , duration : 1 } ] ,
139+ } ,
140+ }
141+ } )
142+
143+ const response = ( await executeProviderRequest ( 'anthropic' , {
144+ model : 'test-model' ,
145+ tools,
146+ } ) ) as ProviderResponse
147+
148+ expect ( response . toolCalls ?. [ 0 ] . name ) . toBe ( 'gmail_send' )
149+ expect ( response . timing ?. timeSegments ?. [ 0 ] . name ) . toBe ( 'gmail_send' )
150+ expect ( tools [ 1 ] . params . oauthCredential ) . toBe ( 'credential-b' )
151+ } )
152+
153+ it ( 'keeps the alias map active while a streaming provider executes the selected instance' , async ( ) => {
154+ const tools = [
155+ makeProviderTool ( 'gmail_send' , 'credential-a' ) ,
156+ makeProviderTool ( 'gmail_send' , 'credential-b' ) ,
157+ ]
158+ mockExecuteRequest . mockImplementationOnce ( async ( request ) => {
159+ const selected = request . tools [ 1 ] as ProviderToolConfig
160+ const output : NormalizedBlockOutput = {
161+ toolCalls : { list : [ ] , count : 0 } ,
162+ providerTiming : { startTime : 'start' , endTime : 'end' , duration : 0 , timeSegments : [ ] } ,
163+ }
164+ return {
165+ streamFormat : 'agent-events-v1' ,
166+ stream : new ReadableStream < AgentStreamEvent > ( {
167+ async pull ( controller ) {
168+ await executeProviderTool ( selected . id , selected . params )
169+ output . toolCalls = { list : [ { name : selected . id } ] , count : 1 }
170+ controller . enqueue ( { type : 'tool_call_start' , id : 'call-1' , name : selected . id } )
171+ controller . close ( )
172+ } ,
173+ } ) ,
174+ execution : { success : true , output } ,
175+ }
176+ } )
177+
178+ const response = await executeProviderRequest ( 'anthropic' , {
179+ model : 'test-model' ,
180+ tools,
181+ } )
182+ expect ( response ) . not . toBeInstanceOf ( ReadableStream )
183+ expect ( response ) . toHaveProperty ( 'stream' )
184+ const streaming = response as StreamingExecution
185+ const reader = ( streaming . stream as ReadableStream < AgentStreamEvent > ) . getReader ( )
186+ const event = await reader . read ( )
187+ await reader . read ( )
188+
189+ expect ( mockExecuteTool ) . toHaveBeenCalledWith (
190+ 'gmail_send' ,
191+ { oauthCredential : 'credential-b' } ,
192+ expect . any ( Object )
193+ )
194+ expect ( event . value ) . toEqual ( { type : 'tool_call_start' , id : 'call-1' , name : 'gmail_send' } )
195+ expect ( streaming . execution . output . toolCalls ?. list [ 0 ] . name ) . toBe ( 'gmail_send' )
196+ } )
197+ } )
198+
94199describe ( 'executeProviderRequest — BYOK regression' , ( ) => {
95200 beforeEach ( ( ) => {
96201 vi . clearAllMocks ( )
0 commit comments