@@ -245,6 +245,157 @@ public void testFromLlmRequest_basic() throws Exception {
245245 assertThat (request .messages .get (0 ).content .getValue ()).isEqualTo ("Hello" );
246246 }
247247
248+ @ Test
249+ public void testFromLlmRequest_withRefusal () throws Exception {
250+ LlmRequest llmRequest =
251+ LlmRequest .builder ()
252+ .model ("gemini-1.5-pro" )
253+ .contents (
254+ ImmutableList .of (
255+ Content .builder ()
256+ .role ("model" )
257+ .parts (
258+ ImmutableList .of (
259+ Part .fromText ("Regular text response" ),
260+ Part .fromText (
261+ ChatCompletionsCommon .REFUSAL_PREFIX + "I cannot do that." )))
262+ .build ()))
263+ .build ();
264+
265+ ChatCompletionsRequest request = ChatCompletionsRequest .fromLlmRequest (llmRequest , false );
266+
267+ assertThat (request .messages ).hasSize (1 );
268+ ChatCompletionsRequest .Message message = request .messages .get (0 );
269+ assertThat (message .role ).isEqualTo ("assistant" );
270+ assertThat (message .refusal ).isEqualTo ("I cannot do that." );
271+ assertThat (message .content .getValue ()).isEqualTo ("Regular text response" );
272+ }
273+
274+ @ Test
275+ public void testFromLlmRequest_withRefusalEmbeddedAfterNewline () throws Exception {
276+ // A single Part containing both content and refusal, separated by "\n[[REFUSAL]]: ".
277+ LlmRequest llmRequest =
278+ LlmRequest .builder ()
279+ .model ("gemini-1.5-pro" )
280+ .contents (
281+ ImmutableList .of (
282+ Content .builder ()
283+ .role ("model" )
284+ .parts (
285+ ImmutableList .of (
286+ Part .fromText (
287+ "Partial text answer\n "
288+ + ChatCompletionsCommon .REFUSAL_PREFIX
289+ + "System error or refusal" )))
290+ .build ()))
291+ .build ();
292+
293+ ChatCompletionsRequest request = ChatCompletionsRequest .fromLlmRequest (llmRequest , false );
294+
295+ assertThat (request .messages ).hasSize (1 );
296+ ChatCompletionsRequest .Message message = request .messages .get (0 );
297+ assertThat (message .role ).isEqualTo ("assistant" );
298+ assertThat (message .content .getValue ()).isEqualTo ("Partial text answer" );
299+ assertThat (message .refusal ).isEqualTo ("System error or refusal" );
300+ }
301+
302+ @ Test
303+ public void testFromLlmRequest_withMultipleRefusalsJoinedWithNewline () throws Exception {
304+ LlmRequest llmRequest =
305+ LlmRequest .builder ()
306+ .model ("gemini-1.5-pro" )
307+ .contents (
308+ ImmutableList .of (
309+ Content .builder ()
310+ .role ("model" )
311+ .parts (
312+ ImmutableList .of (
313+ Part .fromText (ChatCompletionsCommon .REFUSAL_PREFIX + "First" ),
314+ Part .fromText (ChatCompletionsCommon .REFUSAL_PREFIX + "Second" )))
315+ .build ()))
316+ .build ();
317+
318+ ChatCompletionsRequest request = ChatCompletionsRequest .fromLlmRequest (llmRequest , false );
319+
320+ assertThat (request .messages ).hasSize (1 );
321+ ChatCompletionsRequest .Message message = request .messages .get (0 );
322+ assertThat (message .role ).isEqualTo ("assistant" );
323+ assertThat (message .refusal ).isEqualTo ("First\n Second" );
324+ assertThat (message .content ).isNull ();
325+ }
326+
327+ @ Test
328+ public void testFromLlmRequest_withRefusalOnlyHasNullContent () throws Exception {
329+ LlmRequest llmRequest =
330+ LlmRequest .builder ()
331+ .model ("gemini-1.5-pro" )
332+ .contents (
333+ ImmutableList .of (
334+ Content .builder ()
335+ .role ("model" )
336+ .parts (
337+ ImmutableList .of (
338+ Part .fromText (
339+ ChatCompletionsCommon .REFUSAL_PREFIX + "Only a refusal" )))
340+ .build ()))
341+ .build ();
342+
343+ ChatCompletionsRequest request = ChatCompletionsRequest .fromLlmRequest (llmRequest , false );
344+
345+ assertThat (request .messages ).hasSize (1 );
346+ ChatCompletionsRequest .Message message = request .messages .get (0 );
347+ assertThat (message .role ).isEqualTo ("assistant" );
348+ assertThat (message .refusal ).isEqualTo ("Only a refusal" );
349+ assertThat (message .content ).isNull ();
350+ }
351+
352+ @ Test
353+ public void testFromLlmRequest_withRefusalPrefixAfterEmptyContentLine () throws Exception {
354+ // Edge case: text begins with "\n[[REFUSAL]]: ..." -- empty content before the prefix.
355+ // Expectation: no content part, refusal populated.
356+ String text = "\n " + ChatCompletionsCommon .REFUSAL_PREFIX + "Refusal only" ;
357+ LlmRequest llmRequest =
358+ LlmRequest .builder ()
359+ .model ("gemini-1.5-pro" )
360+ .contents (
361+ ImmutableList .of (
362+ Content .builder ()
363+ .role ("model" )
364+ .parts (ImmutableList .of (Part .fromText (text )))
365+ .build ()))
366+ .build ();
367+
368+ ChatCompletionsRequest request = ChatCompletionsRequest .fromLlmRequest (llmRequest , false );
369+
370+ assertThat (request .messages ).hasSize (1 );
371+ ChatCompletionsRequest .Message message = request .messages .get (0 );
372+ assertThat (message .refusal ).isEqualTo ("Refusal only" );
373+ assertThat (message .content ).isNull ();
374+ }
375+
376+ @ Test
377+ public void testFromLlmRequest_withRefusalPrefixMidLineIsNotSplit () throws Exception {
378+ // The prefix is intentionally NOT recognized mid-line without a preceding newline.
379+ String inlineText = "foo " + ChatCompletionsCommon .REFUSAL_PREFIX + "bar" ;
380+ LlmRequest llmRequest =
381+ LlmRequest .builder ()
382+ .model ("gemini-1.5-pro" )
383+ .contents (
384+ ImmutableList .of (
385+ Content .builder ()
386+ .role ("model" )
387+ .parts (ImmutableList .of (Part .fromText (inlineText )))
388+ .build ()))
389+ .build ();
390+
391+ ChatCompletionsRequest request = ChatCompletionsRequest .fromLlmRequest (llmRequest , false );
392+
393+ assertThat (request .messages ).hasSize (1 );
394+ ChatCompletionsRequest .Message message = request .messages .get (0 );
395+ assertThat (message .refusal ).isNull ();
396+ assertThat (message .content .getValue ()).isEqualTo (inlineText );
397+ }
398+
248399 @ Test
249400 public void testFromLlmRequest_withSystemInstruction () throws Exception {
250401 LlmRequest llmRequest =
0 commit comments