Litellm dev bedrock anthropic 3 7 v2 (#8843)

* feat(bedrock/converse/transformation.py): support claude-3-7-sonnet reasoning_Content transformation

Closes https://github.com/BerriAI/litellm/issues/8777

* fix(bedrock/): support returning `reasoning_content` on streaming for claude-3-7

Resolves https://github.com/BerriAI/litellm/issues/8777

* feat(bedrock/): unify converse reasoning content blocks for consistency across anthropic and bedrock

* fix(anthropic/chat/transformation.py): handle deepseek-style 'reasoning_content' extraction within transformation.py

simpler logic

* feat(bedrock/): fix streaming to return blocks in consistent format

* fix: fix linting error

* test: fix test

* feat(factory.py): fix bedrock thinking block translation on tool calling

allows passing the thinking blocks back to bedrock for tool calling

* fix(types/utils.py): don't exclude provider_specific_fields on model dump

ensures consistent responses

* fix: fix linting errors

* fix(convert_dict_to_response.py): pass reasoning_content on root

* fix: test

* fix(streaming_handler.py): add helper util for setting model id

* fix(streaming_handler.py): fix setting model id on model response stream chunk

* fix(streaming_handler.py): fix linting error

* fix(streaming_handler.py): fix linting error

* fix(types/utils.py): add provider_specific_fields to model stream response

* fix(streaming_handler.py): copy provider specific fields and add them to the root of the streaming response

* fix(streaming_handler.py): fix check

* fix: fix test

* fix(types/utils.py): ensure messages content is always openai compatible

* fix(types/utils.py): fix delta object to always be openai compatible

only introduce new params if variable exists

* test: fix bedrock nova tests

* test: skip flaky test

* test: skip flaky test in ci/cd
This commit is contained in:
Krish Dholakia 2025-02-26 16:05:33 -08:00 committed by GitHub
parent f3ef6c92a3
commit 05a973bf19
20 changed files with 447 additions and 149 deletions

View file

@ -66,6 +66,22 @@ class ToolUseBlock(TypedDict):
toolUseId: str
class BedrockConverseReasoningTextBlock(TypedDict, total=False):
text: Required[str]
signature: str
class BedrockConverseReasoningContentBlock(TypedDict, total=False):
reasoningText: BedrockConverseReasoningTextBlock
redactedContent: str
class BedrockConverseReasoningContentBlockDelta(TypedDict, total=False):
signature: str
redactedContent: str
text: str
class ContentBlock(TypedDict, total=False):
text: str
image: ImageBlock
@ -73,6 +89,7 @@ class ContentBlock(TypedDict, total=False):
toolResult: ToolResultBlock
toolUse: ToolUseBlock
cachePoint: CachePointBlock
reasoningContent: BedrockConverseReasoningContentBlock
class MessageBlock(TypedDict):
@ -167,6 +184,7 @@ class ContentBlockDeltaEvent(TypedDict, total=False):
text: str
toolUse: ToolBlockDeltaEvent
reasoningContent: BedrockConverseReasoningContentBlockDelta
class CommonRequestObject(

View file

@ -596,6 +596,9 @@ class ChatCompletionResponseMessage(TypedDict, total=False):
tool_calls: Optional[List[ChatCompletionToolCallChunk]]
role: Literal["assistant"]
function_call: Optional[ChatCompletionToolCallFunctionChunk]
provider_specific_fields: Optional[dict]
reasoning_content: Optional[str]
thinking_blocks: Optional[List[ChatCompletionThinkingBlock]]
class ChatCompletionUsageBlock(TypedDict):

View file

@ -24,6 +24,7 @@ from typing_extensions import Callable, Dict, Required, TypedDict, override
from ..litellm_core_utils.core_helpers import map_finish_reason
from .guardrails import GuardrailEventHooks
from .llms.openai import (
ChatCompletionThinkingBlock,
ChatCompletionToolCallChunk,
ChatCompletionUsageBlock,
OpenAIChatCompletionChunk,
@ -457,29 +458,6 @@ Reference:
ChatCompletionMessage(content='This is a test', role='assistant', function_call=None, tool_calls=None))
"""
REASONING_CONTENT_COMPATIBLE_PARAMS = [
"thinking_blocks",
"reasoning_content",
]
def map_reasoning_content(provider_specific_fields: Dict[str, Any]) -> str:
"""
Extract reasoning_content from provider_specific_fields
"""
reasoning_content: str = ""
for k, v in provider_specific_fields.items():
if k == "thinking_blocks" and isinstance(v, list):
_reasoning_content = ""
for block in v:
if block.get("type") == "thinking":
_reasoning_content += block.get("thinking", "")
reasoning_content = _reasoning_content
elif k == "reasoning_content":
reasoning_content = v
return reasoning_content
def add_provider_specific_fields(
object: BaseModel, provider_specific_fields: Optional[Dict[str, Any]]
@ -487,12 +465,6 @@ def add_provider_specific_fields(
if not provider_specific_fields: # set if provider_specific_fields is not empty
return
setattr(object, "provider_specific_fields", provider_specific_fields)
for k, v in provider_specific_fields.items():
if v is not None:
setattr(object, k, v)
if k in REASONING_CONTENT_COMPATIBLE_PARAMS and k != "reasoning_content":
reasoning_content = map_reasoning_content({k: v})
setattr(object, "reasoning_content", reasoning_content)
class Message(OpenAIObject):
@ -501,6 +473,8 @@ class Message(OpenAIObject):
tool_calls: Optional[List[ChatCompletionMessageToolCall]]
function_call: Optional[FunctionCall]
audio: Optional[ChatCompletionAudioResponse] = None
reasoning_content: Optional[str] = None
thinking_blocks: Optional[List[ChatCompletionThinkingBlock]] = None
provider_specific_fields: Optional[Dict[str, Any]] = Field(
default=None, exclude=True
)
@ -513,6 +487,8 @@ class Message(OpenAIObject):
tool_calls: Optional[list] = None,
audio: Optional[ChatCompletionAudioResponse] = None,
provider_specific_fields: Optional[Dict[str, Any]] = None,
reasoning_content: Optional[str] = None,
thinking_blocks: Optional[List[ChatCompletionThinkingBlock]] = None,
**params,
):
init_values: Dict[str, Any] = {
@ -538,6 +514,12 @@ class Message(OpenAIObject):
if audio is not None:
init_values["audio"] = audio
if thinking_blocks is not None:
init_values["thinking_blocks"] = thinking_blocks
if reasoning_content is not None:
init_values["reasoning_content"] = reasoning_content
super(Message, self).__init__(
**init_values, # type: ignore
**params,
@ -548,6 +530,14 @@ class Message(OpenAIObject):
# OpenAI compatible APIs like mistral API will raise an error if audio is passed in
del self.audio
if reasoning_content is None:
# ensure default response matches OpenAI spec
del self.reasoning_content
if thinking_blocks is None:
# ensure default response matches OpenAI spec
del self.thinking_blocks
add_provider_specific_fields(self, provider_specific_fields)
def get(self, key, default=None):
@ -571,9 +561,9 @@ class Message(OpenAIObject):
class Delta(OpenAIObject):
provider_specific_fields: Optional[Dict[str, Any]] = Field(
default=None, exclude=True
)
reasoning_content: Optional[str] = None
thinking_blocks: Optional[List[ChatCompletionThinkingBlock]] = None
provider_specific_fields: Optional[Dict[str, Any]] = Field(default=None)
def __init__(
self,
@ -582,6 +572,8 @@ class Delta(OpenAIObject):
function_call=None,
tool_calls=None,
audio: Optional[ChatCompletionAudioResponse] = None,
reasoning_content: Optional[str] = None,
thinking_blocks: Optional[List[ChatCompletionThinkingBlock]] = None,
**params,
):
super(Delta, self).__init__(**params)
@ -593,6 +585,18 @@ class Delta(OpenAIObject):
self.tool_calls: Optional[List[Union[ChatCompletionDeltaToolCall, Any]]] = None
self.audio: Optional[ChatCompletionAudioResponse] = None
if reasoning_content is not None:
self.reasoning_content = reasoning_content
else:
# ensure default response matches OpenAI spec
del self.reasoning_content
if thinking_blocks is not None:
self.thinking_blocks = thinking_blocks
else:
# ensure default response matches OpenAI spec
del self.thinking_blocks
if function_call is not None and isinstance(function_call, dict):
self.function_call = FunctionCall(**function_call)
else:
@ -894,12 +898,14 @@ class ModelResponseBase(OpenAIObject):
class ModelResponseStream(ModelResponseBase):
choices: List[StreamingChoices]
provider_specific_fields: Optional[Dict[str, Any]] = Field(default=None)
def __init__(
self,
choices: Optional[List[Union[StreamingChoices, dict, BaseModel]]] = None,
id: Optional[str] = None,
created: Optional[int] = None,
provider_specific_fields: Optional[Dict[str, Any]] = None,
**kwargs,
):
if choices is not None and isinstance(choices, list):
@ -936,6 +942,7 @@ class ModelResponseStream(ModelResponseBase):
kwargs["id"] = id
kwargs["created"] = created
kwargs["object"] = "chat.completion.chunk"
kwargs["provider_specific_fields"] = provider_specific_fields
super().__init__(**kwargs)