13
13
14
14
from openai .types .chat .chat_completion_chunk import Choice as OpenAIStreamingChoice
15
15
16
+ from litellm .types .utils import StreamingChoices
17
+
16
18
from litellm .types .llms .anthropic import (
17
19
AllAnthropicToolsValues ,
18
20
AnthopicMessagesAssistantMessageParam ,
22
24
AnthropicMessagesUserMessageParam ,
23
25
AnthropicResponseContentBlockText ,
24
26
AnthropicResponseContentBlockToolUse ,
27
+ AnthropicResponseContentBlockThinking ,
28
+ AnthropicResponseContentBlockRedactedThinking ,
25
29
ContentBlockDelta ,
26
30
ContentJsonBlockDelta ,
27
31
ContentTextBlockDelta ,
32
+ ContentThinkingBlockDelta ,
33
+ ContentThinkingSignatureBlockDelta ,
28
34
MessageBlockDelta ,
29
35
MessageDelta ,
30
36
UsageDelta ,
42
48
ChatCompletionRequest ,
43
49
ChatCompletionSystemMessage ,
44
50
ChatCompletionTextObject ,
51
+ ChatCompletionThinkingBlock ,
52
+ ChatCompletionRedactedThinkingBlock ,
45
53
ChatCompletionToolCallFunctionChunk ,
46
54
ChatCompletionToolChoiceFunctionParam ,
47
55
ChatCompletionToolChoiceObjectParam ,
@@ -227,6 +235,7 @@ def translate_anthropic_messages_to_openai( # noqa: PLR0915
227
235
## ASSISTANT MESSAGE ##
228
236
assistant_message_str : Optional [str ] = None
229
237
tool_calls : List [ChatCompletionAssistantToolCall ] = []
238
+ thinking_blocks : List [Union [ChatCompletionThinkingBlock , ChatCompletionRedactedThinkingBlock ]] = []
230
239
if m ["role" ] == "assistant" :
231
240
if isinstance (m .get ("content" ), str ):
232
241
assistant_message_str = str (m .get ("content" , "" ))
@@ -253,11 +262,28 @@ def translate_anthropic_messages_to_openai( # noqa: PLR0915
253
262
function = function_chunk ,
254
263
)
255
264
)
265
+ elif content .get ("type" ) == "thinking" :
266
+ thinking_block = ChatCompletionThinkingBlock (
267
+ type = "thinking" ,
268
+ thinking = content .get ("thinking" ) or "" ,
269
+ signature = content .get ("signature" ) or "" ,
270
+ cache_control = content .get ("cache_control" , {})
271
+ )
272
+ thinking_blocks .append (thinking_block )
273
+ elif content .get ("type" ) == "redacted_thinking" :
274
+ redacted_thinking_block = ChatCompletionRedactedThinkingBlock (
275
+ type = "redacted_thinking" ,
276
+ data = content .get ("data" ) or "" ,
277
+ cache_control = content .get ("cache_control" , {})
278
+ )
279
+ thinking_blocks .append (redacted_thinking_block )
280
+
256
281
257
282
if assistant_message_str is not None or len (tool_calls ) > 0 :
258
283
assistant_message = ChatCompletionAssistantMessage (
259
284
role = "assistant" ,
260
285
content = assistant_message_str ,
286
+ thinking_blocks = thinking_blocks if len (thinking_blocks ) > 0 else None ,
261
287
)
262
288
if len (tool_calls ) > 0 :
263
289
assistant_message ["tool_calls" ] = tool_calls
@@ -383,11 +409,11 @@ def translate_anthropic_to_openai(
383
409
def _translate_openai_content_to_anthropic (
384
410
self , choices : List [Choices ]
385
411
) -> List [
386
- Union [AnthropicResponseContentBlockText , AnthropicResponseContentBlockToolUse ]
412
+ Union [AnthropicResponseContentBlockText , AnthropicResponseContentBlockToolUse , AnthropicResponseContentBlockThinking , AnthropicResponseContentBlockRedactedThinking ]
387
413
]:
388
414
new_content : List [
389
415
Union [
390
- AnthropicResponseContentBlockText , AnthropicResponseContentBlockToolUse
416
+ AnthropicResponseContentBlockText , AnthropicResponseContentBlockToolUse , AnthropicResponseContentBlockThinking , AnthropicResponseContentBlockRedactedThinking
391
417
]
392
418
] = []
393
419
for choice in choices :
@@ -410,6 +436,24 @@ def _translate_openai_content_to_anthropic(
410
436
type = "text" , text = choice .message .content
411
437
)
412
438
)
439
+ elif choice .message .thinking_blocks is not None :
440
+ for thinking_block in choice .message .thinking_blocks :
441
+ if "thinking" in thinking_block and "signature" in thinking_block :
442
+ new_content .append (
443
+ AnthropicResponseContentBlockThinking (
444
+ type = "thinking" ,
445
+ thinking = thinking_block .get ("thinking" ) or "" ,
446
+ signature = thinking_block .get ("signature" ) or "" ,
447
+ )
448
+ )
449
+ elif "data" in thinking_block :
450
+ new_content .append (
451
+ AnthropicResponseContentBlockRedactedThinking (
452
+ type = "redacted_thinking" ,
453
+ data = thinking_block .get ("data" , "" ),
454
+ )
455
+ )
456
+
413
457
414
458
return new_content
415
459
@@ -453,9 +497,9 @@ def translate_openai_response_to_anthropic(
453
497
return translated_obj
454
498
455
499
def _translate_streaming_openai_chunk_to_anthropic_content_block (
456
- self , choices : List [OpenAIStreamingChoice ]
500
+ self , choices : List [Union [ OpenAIStreamingChoice , StreamingChoices ] ]
457
501
) -> Tuple [
458
- Literal ["text" , "tool_use" ],
502
+ Literal ["text" , "tool_use" , "thinking" ],
459
503
"ContentBlockContentBlockDict" ,
460
504
]:
461
505
from litellm ._uuid import uuid
@@ -476,17 +520,35 @@ def _translate_streaming_openai_chunk_to_anthropic_content_block(
476
520
name = choice .delta .tool_calls [0 ].function .name or "" ,
477
521
input = {},
478
522
)
523
+ elif (
524
+ isinstance (choice , StreamingChoices ) and hasattr (choice .delta , "thinking_blocks" )
525
+ ):
526
+ thinking_blocks = choice .delta .thinking_blocks or []
527
+ if len (thinking_blocks ) > 0 :
528
+ thinking = thinking_blocks [0 ].get ("thinking" ) or ""
529
+ signature = thinking_blocks [0 ].get ("signature" ) or ""
530
+
531
+ if thinking and signature :
532
+ raise ValueError ("Both `thinking` and `signature` in a single streaming chunk isn't supported." )
533
+ return "thinking" , ChatCompletionThinkingBlock (
534
+ type = "thinking" ,
535
+ thinking = thinking ,
536
+ signature = signature
537
+ )
538
+
479
539
480
540
return "text" , TextBlock (type = "text" , text = "" )
481
541
482
542
def _translate_streaming_openai_chunk_to_anthropic (
483
- self , choices : List [OpenAIStreamingChoice ]
543
+ self , choices : List [Union [ OpenAIStreamingChoice , StreamingChoices ] ]
484
544
) -> Tuple [
485
- Literal ["text_delta" , "input_json_delta" ],
486
- Union [ContentTextBlockDelta , ContentJsonBlockDelta ],
545
+ Literal ["text_delta" , "input_json_delta" , "thinking_delta" , "signature_delta" ],
546
+ Union [ContentTextBlockDelta , ContentJsonBlockDelta , ContentThinkingBlockDelta , ContentThinkingSignatureBlockDelta ],
487
547
]:
488
548
489
549
text : str = ""
550
+ reasoning_content : str = ""
551
+ reasoning_signature : str = ""
490
552
partial_json : Optional [str ] = None
491
553
for choice in choices :
492
554
if choice .delta .content is not None and len (choice .delta .content ) > 0 :
@@ -499,10 +561,24 @@ def _translate_streaming_openai_chunk_to_anthropic(
499
561
and tool .function .arguments is not None
500
562
):
501
563
partial_json += tool .function .arguments
564
+ elif isinstance (choice , StreamingChoices ) and hasattr (choice .delta , "thinking_blocks" ):
565
+ thinking_blocks = choice .delta .thinking_blocks or []
566
+ if len (thinking_blocks ) > 0 :
567
+ reasoning_content += thinking_blocks [0 ].get ("thinking" ) or ""
568
+ reasoning_signature += thinking_blocks [0 ].get ("signature" ) or ""
569
+
570
+ if reasoning_content and reasoning_signature :
571
+ raise ValueError ("Both `reasoning` and `signature` in a single streaming chunk isn't supported." )
572
+
573
+
502
574
if partial_json is not None :
503
575
return "input_json_delta" , ContentJsonBlockDelta (
504
576
type = "input_json_delta" , partial_json = partial_json
505
577
)
578
+ elif reasoning_content :
579
+ return "thinking_delta" , ContentThinkingBlockDelta (type = "thinking_delta" , thinking = reasoning_content )
580
+ elif reasoning_signature :
581
+ return "signature_delta" , ContentThinkingSignatureBlockDelta (type = "signature_delta" , signature = reasoning_signature )
506
582
else :
507
583
return "text_delta" , ContentTextBlockDelta (type = "text_delta" , text = text )
508
584
0 commit comments