@@ -205,3 +205,229 @@ describe("OpenBot Mastra listen port configuration", () => {
205205 } ) ;
206206 }
207207} ) ;
208+
209+ describe ( "OpenBot Mastra provider requests" , ( ) => {
210+ const choices : {
211+ provider : string ;
212+ base : string ;
213+ model : string ;
214+ url : string ;
215+ apiKey ?: string ;
216+ error ?: string ;
217+ } [ ] = [
218+ {
219+ provider : "anthropic" ,
220+ base : "" ,
221+ model : "claude-sonnet-4-5" ,
222+ url : "https://api.anthropic.com/v1/messages" ,
223+ apiKey : "test-anthropic" ,
224+ } ,
225+ {
226+ provider : "anthropic" ,
227+ base : "http://anthropic.test/v1" ,
228+ model : "claude-sonnet-4-5" ,
229+ url : "http://anthropic.test/v1/messages" ,
230+ apiKey : "test-anthropic" ,
231+ } ,
232+ {
233+ provider : "" ,
234+ base : "" ,
235+ model : "gpt-5.5" ,
236+ url : "https://api.openai.com/v1/responses" ,
237+ apiKey : "test-openai" ,
238+ } ,
239+ {
240+ provider : "openai" ,
241+ base : " https://api.openai.com/v1/ " ,
242+ model : "gpt-5.5" ,
243+ url : "https://api.openai.com/v1/responses" ,
244+ apiKey : "test-openai" ,
245+ } ,
246+ {
247+ provider : "" ,
248+ base : "http://compatible.test/v1" ,
249+ model : "llama3.1:8b" ,
250+ url : "http://compatible.test/v1/chat/completions" ,
251+ apiKey : "test-openai" ,
252+ } ,
253+ ...[
254+ "http://anthropic.test" ,
255+ " http://anthropic.test/proxy/ " ,
256+ "http://anthropic.test/proxy/v1/" ,
257+ ] . map ( ( base ) => ( {
258+ provider : "anthropic" ,
259+ base,
260+ model : "claude-sonnet-4-5" ,
261+ apiKey : "test-anthropic" ,
262+ url : base . includes ( "proxy" )
263+ ? "http://anthropic.test/proxy/v1/messages"
264+ : "http://anthropic.test/v1/messages" ,
265+ } ) ) ,
266+ ...[ undefined , "" ] . map ( ( apiKey ) => ( {
267+ provider : "openai" ,
268+ base : "http://compatible.test/v1" ,
269+ model : "llama3.1:8b" ,
270+ apiKey,
271+ url : "http://compatible.test/v1/chat/completions" ,
272+ } ) ) ,
273+ {
274+ provider : "openai" ,
275+ base : "" ,
276+ model : "gpt-5.5" ,
277+ url : "https://api.openai.com/v1/responses" ,
278+ error : "OpenAI API key is missing" ,
279+ } ,
280+ ] ;
281+
282+ for ( const choice of choices ) {
283+ test ( `uses ${ choice . base || "the default endpoint" } with ${ choice . apiKey === undefined ? "no" : choice . apiKey || "an empty" } API key` , async ( ) => {
284+ const seen : { url : string | null ; key : string | null ; model : string } [ ] =
285+ [ ] ;
286+ const provider = Bun . serve ( {
287+ hostname : "127.0.0.1" ,
288+ port : 0 ,
289+ async fetch ( request ) {
290+ const body = await request . json ( ) ;
291+ seen . push ( {
292+ url : request . headers . get ( "x-test-original-url" ) ,
293+ key : request . headers . get (
294+ choice . provider === "anthropic" ? "x-api-key" : "authorization" ,
295+ ) ,
296+ model : body . model ,
297+ } ) ;
298+ const path = new URL ( request . url ) . pathname ;
299+ if ( path . endsWith ( "/v1/messages" ) ) {
300+ return Response . json ( {
301+ id : "msg" ,
302+ type : "message" ,
303+ role : "assistant" ,
304+ model : body . model ,
305+ content : [ { type : "text" , text : "hello" } ] ,
306+ stop_reason : "end_turn" ,
307+ stop_sequence : null ,
308+ usage : { input_tokens : 1 , output_tokens : 1 } ,
309+ } ) ;
310+ }
311+ if ( path === "/v1/chat/completions" ) {
312+ return Response . json ( {
313+ id : "chat" ,
314+ object : "chat.completion" ,
315+ created : 0 ,
316+ model : body . model ,
317+ choices : [
318+ {
319+ index : 0 ,
320+ finish_reason : "stop" ,
321+ message : { role : "assistant" , content : "hello" } ,
322+ } ,
323+ ] ,
324+ usage : {
325+ prompt_tokens : 1 ,
326+ completion_tokens : 1 ,
327+ total_tokens : 2 ,
328+ } ,
329+ } ) ;
330+ }
331+ if ( path === "/v1/responses" ) {
332+ return Response . json ( {
333+ id : "resp" ,
334+ object : "response" ,
335+ created_at : 0 ,
336+ model : body . model ,
337+ status : "completed" ,
338+ output : [
339+ {
340+ id : "msg" ,
341+ type : "message" ,
342+ role : "assistant" ,
343+ status : "completed" ,
344+ content : [
345+ { type : "output_text" , text : "hello" , annotations : [ ] } ,
346+ ] ,
347+ } ,
348+ ] ,
349+ usage : {
350+ input_tokens : 1 ,
351+ output_tokens : 1 ,
352+ input_tokens_details : { cached_tokens : 0 } ,
353+ output_tokens_details : { reasoning_tokens : 0 } ,
354+ } ,
355+ } ) ;
356+ }
357+ return new Response ( "unexpected provider route" , { status : 404 } ) ;
358+ } ,
359+ } ) ;
360+ const child = Bun . spawn (
361+ [
362+ Bun . argv [ 0 ] ,
363+ "-e" ,
364+ [
365+ // Only HTTP is replaced: the real Mastra Agent and provider SDK build the request.
366+ "const networkFetch = globalThis.fetch;" ,
367+ "globalThis.fetch = (input, init) => {" ,
368+ " const request = new Request(input, init);" ,
369+ " const url = new URL(request.url);" ,
370+ ' request.headers.set("x-test-original-url", request.url);' ,
371+ ` return networkFetch(new Request(${ JSON . stringify ( provider . url . toString ( ) ) } + url.pathname.slice(1) + url.search, request));` ,
372+ "};" ,
373+ 'const { mastra } = await import("./agent-mastra/src/mastra/index.ts");' ,
374+ 'const result = await mastra.getAgent("openbot").generate("Say hello");' ,
375+ "console.log(JSON.stringify({ text: result.text }));" ,
376+ ] . join ( "\n" ) ,
377+ ] ,
378+ {
379+ env : {
380+ PATH : process . env . PATH ?? "/opt/homebrew/bin:/usr/bin:/bin" ,
381+ MASTRA_TELEMETRY_DISABLED : "true" ,
382+ DO_NOT_TRACK : "1" ,
383+ NODE_ENV : "test" ,
384+ BOT_PROVIDER : choice . provider ,
385+ BOT_MODEL : choice . model ,
386+ ANTHROPIC_API_KEY :
387+ choice . provider === "anthropic" ? choice . apiKey : "" ,
388+ OPENAI_API_KEY :
389+ choice . provider === "anthropic" ? "" : choice . apiKey ,
390+ ANTHROPIC_BASE_URL :
391+ choice . provider === "anthropic" ? choice . base : "" ,
392+ OPENAI_BASE_URL : choice . provider === "anthropic" ? "" : choice . base ,
393+ } ,
394+ stdout : "pipe" ,
395+ stderr : "pipe" ,
396+ } ,
397+ ) ;
398+ const timeout = setTimeout ( ( ) => child . kill ( ) , 10_000 ) ;
399+ try {
400+ const [ stdout , stderr , exitCode ] = await Promise . all ( [
401+ new Response ( child . stdout ) . text ( ) ,
402+ new Response ( child . stderr ) . text ( ) ,
403+ child . exited ,
404+ ] ) ;
405+ if ( choice . error ) {
406+ expect ( exitCode ) . not . toBe ( 0 ) ;
407+ expect ( stderr ) . toContain ( choice . error ) ;
408+ expect ( seen ) . toEqual ( [ ] ) ;
409+ return ;
410+ }
411+ if ( exitCode !== 0 )
412+ throw new Error (
413+ `provider probe exited ${ exitCode } \n${ stdout } \n${ stderr } ` ,
414+ ) ;
415+ expect ( stdout ) . toContain ( '"text":"hello"' ) ;
416+ expect ( seen ) . toEqual ( [
417+ {
418+ url : choice . url ,
419+ key :
420+ choice . provider === "anthropic"
421+ ? ( choice . apiKey ?? null )
422+ : `Bearer ${ choice . apiKey ?. trim ( ) || "no-key-needed" } ` ,
423+ model : choice . model ,
424+ } ,
425+ ] ) ;
426+ } finally {
427+ clearTimeout ( timeout ) ;
428+ child . kill ( ) ;
429+ await provider . stop ( true ) ;
430+ }
431+ } , 15_000 ) ;
432+ }
433+ } ) ;
0 commit comments