3636import  org .springframework .http .HttpStatus ;
3737import  org .springframework .http .MediaType ;
3838import  org .springframework .http .ResponseEntity ;
39+ import  org .springframework .util .LinkedMultiValueMap ;
40+ import  org .springframework .util .MultiValueMap ;
3941import  org .springframework .web .client .ResponseErrorHandler ;
4042import  org .springframework .web .client .RestClient ;
4143import  org .springframework .web .reactive .function .client .WebClient ;
4244
4345import  okhttp3 .mockwebserver .MockResponse ;
4446import  okhttp3 .mockwebserver .MockWebServer ;
4547import  okhttp3 .mockwebserver .RecordedRequest ;
48+ import  org .opentest4j .AssertionFailedError ;
4649
4750public  class  AnthropicApiBuilderTests  {
4851
@@ -191,6 +194,50 @@ void dynamicApiKeyRestClient() throws InterruptedException {
191194			assertThat (recordedRequest .getHeader ("x-api-key" )).isEqualTo ("key2" );
192195		}
193196
197+ 		@ Test 
198+ 		void  dynamicApiKeyRestClientWithAdditionalApiKeyHeader () throws  InterruptedException  {
199+ 			AnthropicApi  api  = AnthropicApi .builder ()
200+ 					.apiKey (() -> {
201+ 						throw  new  AssertionFailedError ("Should not be called, API key is provided in headers" );
202+ 					})
203+ 				.baseUrl (mockWebServer .url ("/" ).toString ())
204+ 				.build ();
205+ 
206+ 			MockResponse  mockResponse  = new  MockResponse ().setResponseCode (200 )
207+ 				.addHeader (HttpHeaders .CONTENT_TYPE , MediaType .APPLICATION_JSON_VALUE )
208+ 				.setBody (""" 
209+ 						{ 
210+ 							"id": "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", 
211+ 						 	"type": "message", 
212+ 						 	"role": "assistant", 
213+ 						 	"content": [], 
214+ 						 	"model": "claude-opus-3-latest", 
215+ 						 	"stop_reason": null, 
216+ 						 	"stop_sequence": null, 
217+ 							 "usage": { 
218+ 						     	"input_tokens": 25, 
219+ 						     	"output_tokens": 1 
220+ 							} 
221+ 						} 
222+ 						""" );
223+ 			mockWebServer .enqueue (mockResponse );
224+ 
225+ 			AnthropicApi .AnthropicMessage  chatCompletionMessage  = new  AnthropicApi .AnthropicMessage (
226+ 					List .of (new  AnthropicApi .ContentBlock ("Hello world" )), AnthropicApi .Role .USER );
227+ 			AnthropicApi .ChatCompletionRequest  request  = AnthropicApi .ChatCompletionRequest .builder ()
228+ 				.model (AnthropicApi .ChatModel .CLAUDE_3_OPUS )
229+ 				.temperature (0.8 )
230+ 				.messages (List .of (chatCompletionMessage ))
231+ 				.build ();
232+ 			MultiValueMap <String , String > additionalHeaders  = new  LinkedMultiValueMap <>();
233+ 			additionalHeaders .add ("x-api-key" , "additional-key" );
234+ 			ResponseEntity <AnthropicApi .ChatCompletionResponse > response  = api .chatCompletionEntity (request , additionalHeaders );
235+ 			assertThat (response .getStatusCode ()).isEqualTo (HttpStatus .OK );
236+ 			RecordedRequest  recordedRequest  = mockWebServer .takeRequest ();
237+ 			assertThat (recordedRequest .getHeader (HttpHeaders .AUTHORIZATION )).isNull ();
238+ 			assertThat (recordedRequest .getHeader ("x-api-key" )).isEqualTo ("additional-key" );
239+ 		}
240+ 
194241		@ Test 
195242		void  dynamicApiKeyWebClient () throws  InterruptedException  {
196243			Queue <ApiKey > apiKeys  = new  LinkedList <>(List .of (new  SimpleApiKey ("key1" ), new  SimpleApiKey ("key2" )));
@@ -203,8 +250,23 @@ void dynamicApiKeyWebClient() throws InterruptedException {
203250				.addHeader (HttpHeaders .CONTENT_TYPE , MediaType .TEXT_EVENT_STREAM_VALUE )
204251				.setBody (
205252						""" 
206- 									 {"type": "message_start", "message": {"id": "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", "type": "message", "role": "assistant", "content": [], "model": "claude-opus-4-20250514", "stop_reason": null, "stop_sequence": null, "usage": {"input_tokens": 25, "output_tokens": 1}}} 
207- 								""" );
253+ 						{ 
254+ 							"type": "message_start", 
255+ 							"message": { 
256+ 								"id": "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", 
257+ 								"type": "message", 
258+ 								"role": "assistant", 
259+ 								"content": [], 
260+ 								"model": "claude-opus-4-20250514", 
261+ 								"stop_reason": null, 
262+ 								"stop_sequence": null, 
263+ 								"usage": { 
264+ 									"input_tokens": 25, 
265+ 									"output_tokens": 1 
266+ 								} 
267+ 							} 
268+ 						} 
269+ 						""" .replace ("\n " , "" ));
208270			mockWebServer .enqueue (mockResponse );
209271			mockWebServer .enqueue (mockResponse );
210272
@@ -216,20 +278,70 @@ void dynamicApiKeyWebClient() throws InterruptedException {
216278				.messages (List .of (chatCompletionMessage ))
217279				.stream (true )
218280				.build ();
219- 			List < AnthropicApi . ChatCompletionResponse >  response  =  api .chatCompletionStream (request )
281+ 			api .chatCompletionStream (request )
220282				.collectList ()
221283				.block ();
222284			RecordedRequest  recordedRequest  = mockWebServer .takeRequest ();
223285			assertThat (recordedRequest .getHeader (HttpHeaders .AUTHORIZATION )).isNull ();
224286			assertThat (recordedRequest .getHeader ("x-api-key" )).isEqualTo ("key1" );
225287
226- 			response  =  api .chatCompletionStream (request ).collectList ().block ();
288+ 			api .chatCompletionStream (request ).collectList ().block ();
227289
228290			recordedRequest  = mockWebServer .takeRequest ();
229291			assertThat (recordedRequest .getHeader (HttpHeaders .AUTHORIZATION )).isNull ();
230292			assertThat (recordedRequest .getHeader ("x-api-key" )).isEqualTo ("key2" );
231293		}
232294
295+ 		@ Test 
296+ 		void  dynamicApiKeyWebClientWithAdditionalApiKey () throws  InterruptedException  {
297+ 			Queue <ApiKey > apiKeys  = new  LinkedList <>(List .of (new  SimpleApiKey ("key1" ), new  SimpleApiKey ("key2" )));
298+ 			AnthropicApi  api  = AnthropicApi .builder ()
299+ 				.apiKey (() -> Objects .requireNonNull (apiKeys .poll ()).getValue ())
300+ 				.baseUrl (mockWebServer .url ("/" ).toString ())
301+ 				.build ();
302+ 
303+ 			MockResponse  mockResponse  = new  MockResponse ().setResponseCode (200 )
304+ 					.addHeader (HttpHeaders .CONTENT_TYPE , MediaType .TEXT_EVENT_STREAM_VALUE )
305+ 					.setBody (
306+ 							""" 
307+ 							{ 
308+ 								"type": "message_start", 
309+ 								"message": { 
310+ 									"id": "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", 
311+ 									"type": "message", 
312+ 									"role": "assistant", 
313+ 									"content": [], 
314+ 									"model": "claude-opus-4-20250514", 
315+ 									"stop_reason": null, 
316+ 									"stop_sequence": null, 
317+ 									"usage": { 
318+ 										"input_tokens": 25, 
319+ 										"output_tokens": 1 
320+ 									} 
321+ 								} 
322+ 							} 
323+ 							""" .replace ("\n " , "" ));
324+ 			mockWebServer .enqueue (mockResponse );
325+ 
326+ 			AnthropicApi .AnthropicMessage  chatCompletionMessage  = new  AnthropicApi .AnthropicMessage (
327+ 					List .of (new  AnthropicApi .ContentBlock ("Hello world" )), AnthropicApi .Role .USER );
328+ 			AnthropicApi .ChatCompletionRequest  request  = AnthropicApi .ChatCompletionRequest .builder ()
329+ 				.model (AnthropicApi .ChatModel .CLAUDE_3_OPUS )
330+ 				.temperature (0.8 )
331+ 				.messages (List .of (chatCompletionMessage ))
332+ 				.stream (true )
333+ 				.build ();
334+ 			MultiValueMap <String , String > additionalHeaders  = new  LinkedMultiValueMap <>();
335+ 			additionalHeaders .add ("x-api-key" , "additional-key" );
336+ 
337+ 			api .chatCompletionStream (request , additionalHeaders )
338+ 				.collectList ()
339+ 				.block ();
340+ 			RecordedRequest  recordedRequest  = mockWebServer .takeRequest ();
341+ 			assertThat (recordedRequest .getHeader (HttpHeaders .AUTHORIZATION )).isNull ();
342+ 			assertThat (recordedRequest .getHeader ("x-api-key" )).isEqualTo ("additional-key" );
343+ 		}
344+ 
233345	}
234346
235347}
0 commit comments