@@ -1131,6 +1131,122 @@ func TestErrorHandling(t *testing.T) {
11311131 }
11321132}
11331133
1134+ // TestStableRequestEncoding validates that a given intercepted request and a
1135+ // given set of injected tools should result identical payloads.
1136+ //
1137+ // Should the payload vary, it may subvert any caching mechanisms the provider may have.
1138+ func TestStableRequestEncoding (t * testing.T ) {
1139+ t .Parallel ()
1140+
1141+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : false }).Leveled (slog .LevelDebug )
1142+
1143+ cases := []struct {
1144+ name string
1145+ fixture []byte
1146+ createRequestFunc createRequestFunc
1147+ configureFunc configureFunc
1148+ }{
1149+ {
1150+ name : aibridge .ProviderAnthropic ,
1151+ fixture : antSimple ,
1152+ createRequestFunc : createAnthropicMessagesReq ,
1153+ configureFunc : func (addr string , client aibridge.Recorder , srvProxyMgr * mcp.ServerProxyManager ) (* aibridge.RequestBridge , error ) {
1154+ return aibridge .NewRequestBridge (t .Context (), []aibridge.Provider {aibridge .NewAnthropicProvider (anthropicCfg (addr , apiKey ), nil )}, logger , client , srvProxyMgr )
1155+ },
1156+ },
1157+ {
1158+ name : aibridge .ProviderOpenAI ,
1159+ fixture : oaiSimple ,
1160+ createRequestFunc : createOpenAIChatCompletionsReq ,
1161+ configureFunc : func (addr string , client aibridge.Recorder , srvProxyMgr * mcp.ServerProxyManager ) (* aibridge.RequestBridge , error ) {
1162+ return aibridge .NewRequestBridge (t .Context (), []aibridge.Provider {aibridge .NewOpenAIProvider (aibridge .OpenAIConfig (anthropicCfg (addr , apiKey )))}, logger , client , srvProxyMgr )
1163+ },
1164+ },
1165+ }
1166+
1167+ for _ , tc := range cases {
1168+ t .Run (tc .name , func (t * testing.T ) {
1169+ t .Parallel ()
1170+
1171+ ctx , cancel := context .WithTimeout (t .Context (), time .Second * 30 )
1172+ t .Cleanup (cancel )
1173+
1174+ // Setup MCP tools.
1175+ tools := setupMCPServerProxiesForTest (t )
1176+
1177+ // Configure the bridge with injected tools.
1178+ mcpMgr := mcp .NewServerProxyManager (tools )
1179+ require .NoError (t , mcpMgr .Init (ctx ))
1180+
1181+ arc := txtar .Parse (tc .fixture )
1182+ t .Logf ("%s: %s" , t .Name (), arc .Comment )
1183+
1184+ files := filesMap (arc )
1185+ require .Contains (t , files , fixtureRequest )
1186+ require .Contains (t , files , fixtureNonStreamingResponse )
1187+
1188+ var (
1189+ reference []byte
1190+ reqCount atomic.Int32
1191+ )
1192+
1193+ // Create a mock server that captures and compares request bodies.
1194+ mockSrv := httptest .NewUnstartedServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
1195+ reqCount .Add (1 )
1196+
1197+ // Capture the raw request body.
1198+ raw , err := io .ReadAll (r .Body )
1199+ defer r .Body .Close ()
1200+ require .NoError (t , err )
1201+ require .NotEmpty (t , raw )
1202+
1203+ // Store the first instance as the reference value.
1204+ if reference == nil {
1205+ reference = raw
1206+ } else {
1207+ // Compare all subsequent requests to the reference.
1208+ assert .JSONEq (t , string (reference ), string (raw ))
1209+ }
1210+
1211+ // Return a valid API response.
1212+ w .Header ().Set ("Content-Type" , "application/json" )
1213+ w .WriteHeader (http .StatusOK )
1214+ _ , _ = w .Write (files [fixtureNonStreamingResponse ])
1215+ }))
1216+ mockSrv .Config .BaseContext = func (_ net.Listener ) context.Context {
1217+ return ctx
1218+ }
1219+ mockSrv .Start ()
1220+ t .Cleanup (mockSrv .Close )
1221+
1222+ recorder := & mockRecorderClient {}
1223+ bridge , err := tc .configureFunc (mockSrv .URL , recorder , mcpMgr )
1224+ require .NoError (t , err )
1225+
1226+ // Invoke request to mocked API via aibridge.
1227+ bridgeSrv := httptest .NewUnstartedServer (bridge )
1228+ bridgeSrv .Config .BaseContext = func (_ net.Listener ) context.Context {
1229+ return aibridge .AsActor (ctx , userID , nil )
1230+ }
1231+ bridgeSrv .Start ()
1232+ t .Cleanup (bridgeSrv .Close )
1233+
1234+ // Make multiple requests and verify they all have identical payloads.
1235+ count := 10
1236+ for range count {
1237+ req := tc .createRequestFunc (t , bridgeSrv .URL , files [fixtureRequest ])
1238+ client := & http.Client {}
1239+ resp , err := client .Do (req )
1240+ require .NoError (t , err )
1241+ require .Equal (t , http .StatusOK , resp .StatusCode )
1242+ _ = resp .Body .Close ()
1243+ }
1244+
1245+ require .EqualValues (t , count , reqCount .Load ())
1246+ })
1247+ }
1248+ }
1249+
11341250func calculateTotalInputTokens (in []* aibridge.TokenUsageRecord ) int64 {
11351251 var total int64
11361252 for _ , el := range in {
@@ -1340,12 +1456,14 @@ func createMockMCPSrv(t *testing.T) http.Handler {
13401456 server .WithToolCapabilities (true ),
13411457 )
13421458
1343- tool := mcplib .NewTool (mockToolName ,
1344- mcplib .WithDescription (fmt .Sprintf ("Mock of the %s tool" , mockToolName )),
1345- )
1346- s .AddTool (tool , func (ctx context.Context , request mcplib.CallToolRequest ) (* mcplib.CallToolResult , error ) {
1347- return mcplib .NewToolResultText ("mock" ), nil
1348- })
1459+ for _ , name := range []string {mockToolName , "coder_list_templates" , "coder_template_version_parameters" , "coder_get_authenticated_user" , "coder_create_workspace_build" } {
1460+ tool := mcplib .NewTool (name ,
1461+ mcplib .WithDescription (fmt .Sprintf ("Mock of the %s tool" , name )),
1462+ )
1463+ s .AddTool (tool , func (ctx context.Context , request mcplib.CallToolRequest ) (* mcplib.CallToolResult , error ) {
1464+ return mcplib .NewToolResultText ("mock" ), nil
1465+ })
1466+ }
13491467
13501468 return server .NewStreamableHTTPServer (s )
13511469}
0 commit comments