mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-17 00:26:01 +00:00
Add thought signature support to v1/messages api (#16812)
* Add thought signature support to v1/messages api * update the thinking level handling logic * update the thinking level handling logic * Add streaming support * fix intalling litellm error
This commit is contained in:
@@ -1162,16 +1162,58 @@ def _gemini_tool_call_invoke_helper(
|
||||
return function_call
|
||||
|
||||
|
||||
def _get_thought_signature_from_tool(tool: dict) -> Optional[str]:
|
||||
"""Extract thought signature from tool call's provider_specific_fields"""
|
||||
def _get_thought_signature_from_tool(tool: dict, model: Optional[str] = None) -> Optional[str]:
|
||||
"""Extract thought signature from tool call's provider_specific_fields.
|
||||
|
||||
Checks both tool.provider_specific_fields and tool.function.provider_specific_fields.
|
||||
If no signature is found and model is gemini-3, returns a dummy signature.
|
||||
"""
|
||||
# First check tool's provider_specific_fields
|
||||
provider_fields = tool.get("provider_specific_fields") or {}
|
||||
if isinstance(provider_fields, dict):
|
||||
return provider_fields.get("thought_signature")
|
||||
signature = provider_fields.get("thought_signature")
|
||||
if signature:
|
||||
return signature
|
||||
|
||||
# Then check function's provider_specific_fields
|
||||
function = tool.get("function")
|
||||
if function:
|
||||
if isinstance(function, dict):
|
||||
func_provider_fields = function.get("provider_specific_fields") or {}
|
||||
if isinstance(func_provider_fields, dict):
|
||||
signature = func_provider_fields.get("thought_signature")
|
||||
if signature:
|
||||
return signature
|
||||
elif hasattr(function, "provider_specific_fields") and function.provider_specific_fields:
|
||||
if isinstance(function.provider_specific_fields, dict):
|
||||
signature = function.provider_specific_fields.get("thought_signature")
|
||||
if signature:
|
||||
return signature
|
||||
|
||||
# If no signature found and model is gemini-3, return dummy signature
|
||||
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import VertexGeminiConfig
|
||||
if model and VertexGeminiConfig._is_gemini_3_or_newer(model):
|
||||
return _get_dummy_thought_signature()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_dummy_thought_signature() -> str:
|
||||
"""Generate a dummy thought signature for models that require it.
|
||||
|
||||
This is used when transferring conversation history from older models
|
||||
(like gemini-2.5-flash) to gemini-3, which requires thought_signature
|
||||
for strict validation.
|
||||
"""
|
||||
# Return a base64-encoded dummy signature string
|
||||
# Below dummy signature is recommended by google - https://ai.google.dev/gemini-api/docs/thought-signatures#faqs
|
||||
dummy_data = b"skip_thought_signature_validator"
|
||||
return base64.b64encode(dummy_data).decode("utf-8")
|
||||
|
||||
|
||||
def convert_to_gemini_tool_call_invoke(
|
||||
message: ChatCompletionAssistantMessage,
|
||||
model: Optional[str] = None,
|
||||
) -> List[VertexPartType]:
|
||||
"""
|
||||
OpenAI tool invokes:
|
||||
@@ -1229,7 +1271,7 @@ def convert_to_gemini_tool_call_invoke(
|
||||
part_dict: VertexPartType = {
|
||||
"function_call": gemini_function_call
|
||||
}
|
||||
thought_signature = _get_thought_signature_from_tool(dict(tool))
|
||||
thought_signature = _get_thought_signature_from_tool(dict(tool), model=model)
|
||||
if thought_signature:
|
||||
part_dict["thoughtSignature"] = thought_signature
|
||||
|
||||
@@ -1250,11 +1292,18 @@ def convert_to_gemini_tool_call_invoke(
|
||||
}
|
||||
|
||||
# Extract thought signature from function_call's provider_specific_fields
|
||||
thought_signature = None
|
||||
provider_fields = function_call.get("provider_specific_fields") if isinstance(function_call, dict) else {}
|
||||
if isinstance(provider_fields, dict):
|
||||
thought_signature = provider_fields.get("thought_signature")
|
||||
if thought_signature:
|
||||
part_dict_function["thoughtSignature"] = thought_signature
|
||||
|
||||
# If no signature found and model is gemini-3, use dummy signature
|
||||
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import VertexGeminiConfig
|
||||
if not thought_signature and model and VertexGeminiConfig._is_gemini_3_or_newer(model):
|
||||
thought_signature = _get_dummy_thought_signature()
|
||||
|
||||
if thought_signature:
|
||||
part_dict_function["thoughtSignature"] = thought_signature
|
||||
|
||||
_parts_list.append(part_dict_function)
|
||||
else: # don't silently drop params. Make it clear to user what's happening.
|
||||
|
||||
@@ -137,6 +137,7 @@ class ChunkProcessor:
|
||||
"name": None,
|
||||
"type": None,
|
||||
"arguments": [],
|
||||
"provider_specific_fields": None,
|
||||
}
|
||||
|
||||
if hasattr(tool_call, "id") and tool_call.id:
|
||||
@@ -156,22 +157,48 @@ class ChunkProcessor:
|
||||
tool_call_map[index]["arguments"].append(
|
||||
tool_call.function.arguments
|
||||
)
|
||||
|
||||
# Preserve provider_specific_fields from streaming chunks
|
||||
provider_fields = None
|
||||
if hasattr(tool_call, "provider_specific_fields") and tool_call.provider_specific_fields:
|
||||
provider_fields = tool_call.provider_specific_fields
|
||||
elif hasattr(tool_call, "function") and hasattr(tool_call.function, "provider_specific_fields") and tool_call.function.provider_specific_fields:
|
||||
provider_fields = tool_call.function.provider_specific_fields
|
||||
|
||||
if provider_fields:
|
||||
# Merge provider_specific_fields if multiple chunks have them
|
||||
if tool_call_map[index]["provider_specific_fields"] is None:
|
||||
tool_call_map[index]["provider_specific_fields"] = {}
|
||||
if isinstance(provider_fields, dict):
|
||||
tool_call_map[index]["provider_specific_fields"].update(
|
||||
provider_fields
|
||||
)
|
||||
|
||||
# Convert the map to a list of tool calls
|
||||
for index in sorted(tool_call_map.keys()):
|
||||
tool_call_data = tool_call_map[index]
|
||||
if tool_call_data["id"] and tool_call_data["name"]:
|
||||
combined_arguments = "".join(tool_call_data["arguments"]) or "{}"
|
||||
tool_calls_list.append(
|
||||
ChatCompletionMessageToolCall(
|
||||
id=tool_call_data["id"],
|
||||
function=Function(
|
||||
arguments=combined_arguments,
|
||||
name=tool_call_data["name"],
|
||||
),
|
||||
type=tool_call_data["type"] or "function",
|
||||
)
|
||||
|
||||
# Build function - provider_specific_fields should be on tool_call level, not function level
|
||||
function = Function(
|
||||
arguments=combined_arguments,
|
||||
name=tool_call_data["name"],
|
||||
)
|
||||
|
||||
# Prepare params for ChatCompletionMessageToolCall
|
||||
tool_call_params = {
|
||||
"id": tool_call_data["id"],
|
||||
"function": function,
|
||||
"type": tool_call_data["type"] or "function",
|
||||
}
|
||||
|
||||
# Add provider_specific_fields if present (for thought signatures in Gemini 3)
|
||||
if tool_call_data.get("provider_specific_fields"):
|
||||
tool_call_params["provider_specific_fields"] = tool_call_data["provider_specific_fields"]
|
||||
|
||||
tool_call = ChatCompletionMessageToolCall(**tool_call_params)
|
||||
tool_calls_list.append(tool_call)
|
||||
|
||||
return tool_calls_list
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AsyncIterator,
|
||||
Dict,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
@@ -129,6 +130,39 @@ class LiteLLMAnthropicMessagesAdapter:
|
||||
|
||||
### FOR [BETA] `/v1/messages` endpoint support
|
||||
|
||||
def _extract_signature_from_tool_call(
|
||||
self, tool_call: Any
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Extract signature from a tool call's provider_specific_fields.
|
||||
Only checks provider_specific_fields, not thinking blocks.
|
||||
"""
|
||||
signature = None
|
||||
|
||||
if hasattr(tool_call, "provider_specific_fields") and tool_call.provider_specific_fields:
|
||||
if "thought_signature" in tool_call.provider_specific_fields:
|
||||
signature = tool_call.provider_specific_fields["thought_signature"]
|
||||
elif (
|
||||
hasattr(tool_call.function, "provider_specific_fields")
|
||||
and tool_call.function.provider_specific_fields
|
||||
):
|
||||
if "thought_signature" in tool_call.function.provider_specific_fields:
|
||||
signature = tool_call.function.provider_specific_fields["thought_signature"]
|
||||
|
||||
return signature
|
||||
|
||||
def _extract_signature_from_tool_use_content(
|
||||
self, content: Dict[str, Any]
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Extract signature from a tool_use content block's provider_specific_fields.
|
||||
"""
|
||||
provider_specific_fields = content.get("provider_specific_fields", {})
|
||||
if provider_specific_fields:
|
||||
return provider_specific_fields.get("signature")
|
||||
return None
|
||||
|
||||
|
||||
def translatable_anthropic_params(self) -> List:
|
||||
"""
|
||||
Which anthropic params, we need to translate to the openai format.
|
||||
@@ -263,10 +297,18 @@ class LiteLLMAnthropicMessagesAdapter:
|
||||
else:
|
||||
assistant_message_str += content.get("text", "")
|
||||
elif content.get("type") == "tool_use":
|
||||
function_chunk = ChatCompletionToolCallFunctionChunk(
|
||||
name=content.get("name", ""),
|
||||
arguments=json.dumps(content.get("input", {})),
|
||||
)
|
||||
function_chunk: ChatCompletionToolCallFunctionChunk = {
|
||||
"name": content.get("name", ""),
|
||||
"arguments": json.dumps(content.get("input", {})),
|
||||
}
|
||||
signature = self._extract_signature_from_tool_use_content(content)
|
||||
|
||||
if signature:
|
||||
provider_specific_fields: Dict[str, Any] = (
|
||||
function_chunk.get("provider_specific_fields") or {}
|
||||
)
|
||||
provider_specific_fields["thought_signature"] = signature
|
||||
function_chunk["provider_specific_fields"] = provider_specific_fields
|
||||
|
||||
tool_calls.append(
|
||||
ChatCompletionAssistantToolCall(
|
||||
@@ -512,18 +554,27 @@ class LiteLLMAnthropicMessagesAdapter:
|
||||
and len(choice.message.tool_calls) > 0
|
||||
):
|
||||
for tool_call in choice.message.tool_calls:
|
||||
new_content.append(
|
||||
AnthropicResponseContentBlockToolUse(
|
||||
type="tool_use",
|
||||
id=tool_call.id,
|
||||
name=tool_call.function.name or "",
|
||||
input=(
|
||||
json.loads(tool_call.function.arguments)
|
||||
if tool_call.function.arguments
|
||||
else {}
|
||||
),
|
||||
)
|
||||
# Extract signature from provider_specific_fields only
|
||||
signature = self._extract_signature_from_tool_call(tool_call)
|
||||
|
||||
provider_specific_fields = {}
|
||||
if signature:
|
||||
provider_specific_fields["signature"] = signature
|
||||
|
||||
tool_use_block = AnthropicResponseContentBlockToolUse(
|
||||
type="tool_use",
|
||||
id=tool_call.id,
|
||||
name=tool_call.function.name or "",
|
||||
input=(
|
||||
json.loads(tool_call.function.arguments)
|
||||
if tool_call.function.arguments
|
||||
else {}
|
||||
),
|
||||
)
|
||||
# Add provider_specific_fields if signature is present
|
||||
if provider_specific_fields:
|
||||
tool_use_block.provider_specific_fields = provider_specific_fields
|
||||
new_content.append(tool_use_block)
|
||||
# Handle text content
|
||||
elif choice.message.content is not None:
|
||||
new_content.append(
|
||||
|
||||
@@ -140,4 +140,4 @@ class GoogleAIStudioGeminiConfig(VertexGeminiConfig):
|
||||
except Exception:
|
||||
# If conversion fails, leave as is and let the API handle it
|
||||
pass
|
||||
return _gemini_convert_messages_with_history(messages=messages)
|
||||
return _gemini_convert_messages_with_history(messages=messages, model=model)
|
||||
|
||||
@@ -173,7 +173,7 @@ def transform_openai_messages_to_gemini_context_caching(
|
||||
supports_system_message=supports_system_message, messages=messages
|
||||
)
|
||||
|
||||
transformed_messages = _gemini_convert_messages_with_history(messages=new_messages)
|
||||
transformed_messages = _gemini_convert_messages_with_history(messages=new_messages, model=model)
|
||||
|
||||
model_name = "models/{}".format(model)
|
||||
|
||||
|
||||
@@ -195,6 +195,7 @@ def check_if_part_exists_in_parts(
|
||||
|
||||
def _gemini_convert_messages_with_history( # noqa: PLR0915
|
||||
messages: List[AllMessageValues],
|
||||
model: Optional[str] = None,
|
||||
) -> List[ContentType]:
|
||||
"""
|
||||
Converts given messages from OpenAI format to Gemini format
|
||||
@@ -379,7 +380,7 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915
|
||||
or assistant_msg.get("function_call") is not None
|
||||
): # support assistant tool invoke conversion
|
||||
gemini_tool_call_parts = convert_to_gemini_tool_call_invoke(
|
||||
assistant_msg
|
||||
assistant_msg, model=model
|
||||
)
|
||||
## check if gemini_tool_call already exists in assistant_content
|
||||
for gemini_tool_call_part in gemini_tool_call_parts:
|
||||
|
||||
@@ -898,11 +898,11 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
||||
if VertexGeminiConfig._is_gemini_3_or_newer(model):
|
||||
if "temperature" not in optional_params:
|
||||
optional_params["temperature"] = 1.0
|
||||
thinking_config = optional_params.get("thinkingConfig", {})
|
||||
if (
|
||||
"thinkingConfig" not in optional_params
|
||||
or "thinkingLevel" not in optional_params.get("thinkingConfig", {})
|
||||
"thinkingLevel" not in thinking_config
|
||||
and "thinkingBudget" not in thinking_config
|
||||
):
|
||||
thinking_config = optional_params.get("thinkingConfig", {})
|
||||
thinking_config["thinkingLevel"] = "low"
|
||||
optional_params["thinkingConfig"] = thinking_config
|
||||
|
||||
@@ -1892,7 +1892,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
||||
def _transform_messages(
|
||||
self, messages: List[AllMessageValues], model: Optional[str] = None
|
||||
) -> List[ContentType]:
|
||||
return _gemini_convert_messages_with_history(messages=messages)
|
||||
return _gemini_convert_messages_with_history(messages=messages, model=model)
|
||||
|
||||
def get_error_class(
|
||||
self, error_message: str, status_code: int, headers: Union[Dict, httpx.Headers]
|
||||
|
||||
@@ -11773,10 +11773,12 @@
|
||||
"supports_web_search": true
|
||||
},
|
||||
"gemini-3-pro-preview": {
|
||||
"cache_read_input_token_cost": 1.25e-07,
|
||||
"cache_read_input_token_cost": 2e-07,
|
||||
"cache_read_input_token_cost_above_200k_tokens": 4e-07,
|
||||
"cache_creation_input_token_cost_above_200k_tokens": 2.5e-07,
|
||||
"input_cost_per_token": 2e-06,
|
||||
"input_cost_per_token_above_200k_tokens": 4e-06,
|
||||
"input_cost_per_token_batches": 1e-06,
|
||||
"litellm_provider": "vertex_ai-language-models",
|
||||
"max_audio_length_hours": 8.4,
|
||||
"max_audio_per_prompt": 1,
|
||||
@@ -11790,10 +11792,60 @@
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.2e-05,
|
||||
"output_cost_per_token_above_200k_tokens": 1.8e-05,
|
||||
"output_cost_per_token_batches": 6e-06,
|
||||
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing",
|
||||
"supported_endpoints": [
|
||||
"/v1/chat/completions",
|
||||
"/v1/completions"
|
||||
"/v1/completions",
|
||||
"/v1/batch"
|
||||
],
|
||||
"supported_modalities": [
|
||||
"text",
|
||||
"image",
|
||||
"audio",
|
||||
"video"
|
||||
],
|
||||
"supported_output_modalities": [
|
||||
"text"
|
||||
],
|
||||
"supports_audio_input": true,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_video_input": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"vertex_ai/gemini-3-pro-preview": {
|
||||
"cache_read_input_token_cost": 2e-07,
|
||||
"cache_read_input_token_cost_above_200k_tokens": 4e-07,
|
||||
"cache_creation_input_token_cost_above_200k_tokens": 2.5e-07,
|
||||
"input_cost_per_token": 2e-06,
|
||||
"input_cost_per_token_above_200k_tokens": 4e-06,
|
||||
"input_cost_per_token_batches": 1e-06,
|
||||
"litellm_provider": "vertex_ai",
|
||||
"max_audio_length_hours": 8.4,
|
||||
"max_audio_per_prompt": 1,
|
||||
"max_images_per_prompt": 3000,
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 65535,
|
||||
"max_pdf_size_mb": 30,
|
||||
"max_tokens": 65535,
|
||||
"max_video_length": 1,
|
||||
"max_videos_per_prompt": 10,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.2e-05,
|
||||
"output_cost_per_token_above_200k_tokens": 1.8e-05,
|
||||
"output_cost_per_token_batches": 6e-06,
|
||||
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing",
|
||||
"supported_endpoints": [
|
||||
"/v1/chat/completions",
|
||||
"/v1/completions",
|
||||
"/v1/batch"
|
||||
],
|
||||
"supported_modalities": [
|
||||
"text",
|
||||
@@ -13476,9 +13528,11 @@
|
||||
"tpm": 800000
|
||||
},
|
||||
"gemini/gemini-3-pro-preview": {
|
||||
"cache_read_input_token_cost": 3.125e-07,
|
||||
"cache_read_input_token_cost": 2e-07,
|
||||
"cache_read_input_token_cost_above_200k_tokens": 4e-07,
|
||||
"input_cost_per_token": 2e-06,
|
||||
"input_cost_per_token_above_200k_tokens": 4e-06,
|
||||
"input_cost_per_token_batches": 1e-06,
|
||||
"litellm_provider": "gemini",
|
||||
"max_audio_length_hours": 8.4,
|
||||
"max_audio_per_prompt": 1,
|
||||
@@ -13492,11 +13546,13 @@
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.2e-05,
|
||||
"output_cost_per_token_above_200k_tokens": 1.8e-05,
|
||||
"output_cost_per_token_batches": 6e-06,
|
||||
"rpm": 2000,
|
||||
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing",
|
||||
"supported_endpoints": [
|
||||
"/v1/chat/completions",
|
||||
"/v1/completions"
|
||||
"/v1/completions",
|
||||
"/v1/batch"
|
||||
],
|
||||
"supported_modalities": [
|
||||
"text",
|
||||
|
||||
@@ -458,6 +458,10 @@ class AnthropicResponseContentBlockToolUse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
input: dict
|
||||
provider_specific_fields: Optional[Dict[str, Any]] = None
|
||||
|
||||
class Config:
|
||||
extra = "allow" # Allow provider_specific_fields
|
||||
|
||||
|
||||
class AnthropicResponseContentBlockThinking(BaseModel):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from enum import Enum
|
||||
from os import PathLike
|
||||
from typing import IO, Any, Iterable, List, Literal, Mapping, Optional, Tuple, Union
|
||||
from typing import IO, Any, Dict, Iterable, List, Literal, Mapping, Optional, Tuple, Union
|
||||
|
||||
import httpx
|
||||
from openai._legacy_response import (
|
||||
@@ -453,6 +453,7 @@ class ChatCompletionAudioDelta(TypedDict, total=False):
|
||||
class ChatCompletionToolCallFunctionChunk(TypedDict, total=False):
|
||||
name: Optional[str]
|
||||
arguments: str
|
||||
provider_specific_fields: Optional[Dict[str, Any]]
|
||||
|
||||
|
||||
class ChatCompletionAssistantToolCall(TypedDict):
|
||||
|
||||
@@ -11773,10 +11773,12 @@
|
||||
"supports_web_search": true
|
||||
},
|
||||
"gemini-3-pro-preview": {
|
||||
"cache_read_input_token_cost": 1.25e-07,
|
||||
"cache_read_input_token_cost": 2e-07,
|
||||
"cache_read_input_token_cost_above_200k_tokens": 4e-07,
|
||||
"cache_creation_input_token_cost_above_200k_tokens": 2.5e-07,
|
||||
"input_cost_per_token": 2e-06,
|
||||
"input_cost_per_token_above_200k_tokens": 4e-06,
|
||||
"input_cost_per_token_batches": 1e-06,
|
||||
"litellm_provider": "vertex_ai-language-models",
|
||||
"max_audio_length_hours": 8.4,
|
||||
"max_audio_per_prompt": 1,
|
||||
@@ -11790,10 +11792,60 @@
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.2e-05,
|
||||
"output_cost_per_token_above_200k_tokens": 1.8e-05,
|
||||
"output_cost_per_token_batches": 6e-06,
|
||||
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing",
|
||||
"supported_endpoints": [
|
||||
"/v1/chat/completions",
|
||||
"/v1/completions"
|
||||
"/v1/completions",
|
||||
"/v1/batch"
|
||||
],
|
||||
"supported_modalities": [
|
||||
"text",
|
||||
"image",
|
||||
"audio",
|
||||
"video"
|
||||
],
|
||||
"supported_output_modalities": [
|
||||
"text"
|
||||
],
|
||||
"supports_audio_input": true,
|
||||
"supports_function_calling": true,
|
||||
"supports_pdf_input": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_video_input": true,
|
||||
"supports_vision": true,
|
||||
"supports_web_search": true
|
||||
},
|
||||
"vertex_ai/gemini-3-pro-preview": {
|
||||
"cache_read_input_token_cost": 2e-07,
|
||||
"cache_read_input_token_cost_above_200k_tokens": 4e-07,
|
||||
"cache_creation_input_token_cost_above_200k_tokens": 2.5e-07,
|
||||
"input_cost_per_token": 2e-06,
|
||||
"input_cost_per_token_above_200k_tokens": 4e-06,
|
||||
"input_cost_per_token_batches": 1e-06,
|
||||
"litellm_provider": "vertex_ai",
|
||||
"max_audio_length_hours": 8.4,
|
||||
"max_audio_per_prompt": 1,
|
||||
"max_images_per_prompt": 3000,
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 65535,
|
||||
"max_pdf_size_mb": 30,
|
||||
"max_tokens": 65535,
|
||||
"max_video_length": 1,
|
||||
"max_videos_per_prompt": 10,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.2e-05,
|
||||
"output_cost_per_token_above_200k_tokens": 1.8e-05,
|
||||
"output_cost_per_token_batches": 6e-06,
|
||||
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing",
|
||||
"supported_endpoints": [
|
||||
"/v1/chat/completions",
|
||||
"/v1/completions",
|
||||
"/v1/batch"
|
||||
],
|
||||
"supported_modalities": [
|
||||
"text",
|
||||
@@ -13476,9 +13528,11 @@
|
||||
"tpm": 800000
|
||||
},
|
||||
"gemini/gemini-3-pro-preview": {
|
||||
"cache_read_input_token_cost": 3.125e-07,
|
||||
"cache_read_input_token_cost": 2e-07,
|
||||
"cache_read_input_token_cost_above_200k_tokens": 4e-07,
|
||||
"input_cost_per_token": 2e-06,
|
||||
"input_cost_per_token_above_200k_tokens": 4e-06,
|
||||
"input_cost_per_token_batches": 1e-06,
|
||||
"litellm_provider": "gemini",
|
||||
"max_audio_length_hours": 8.4,
|
||||
"max_audio_per_prompt": 1,
|
||||
@@ -13492,11 +13546,13 @@
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.2e-05,
|
||||
"output_cost_per_token_above_200k_tokens": 1.8e-05,
|
||||
"output_cost_per_token_batches": 6e-06,
|
||||
"rpm": 2000,
|
||||
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing",
|
||||
"supported_endpoints": [
|
||||
"/v1/chat/completions",
|
||||
"/v1/completions"
|
||||
"/v1/completions",
|
||||
"/v1/batch"
|
||||
],
|
||||
"supported_modalities": [
|
||||
"text",
|
||||
|
||||
+42
@@ -790,3 +790,45 @@ def test_translate_anthropic_messages_to_openai_mixed_content_with_image():
|
||||
# Check final text
|
||||
assert result[0]["content"][4]["type"] == "text"
|
||||
assert result[0]["content"][4]["text"] == "What's the difference?"
|
||||
|
||||
|
||||
def test_translate_anthropic_messages_to_openai_tool_use_with_signature():
|
||||
"""Test that thought signatures from tool_use blocks are correctly extracted and placed in provider_specific_fields."""
|
||||
|
||||
test_signature = "EpYECpMEAdHtim9iBECdK1l5uVIIXoZZmq+PUBH9nz3Q6EMeIdEqWwVb5GlxSNtxuSkFoseFco5U4zxN/lacJxD2WUjFvEyL2GOkbPgXFeCcgNBMEYVRg7UAr45KGeWJJmJMoheLHezKawI1L94vi2PsB9TDpWv4vyAx1vKG2PByiVmWWtd0rondsdbENNp2Rrz3ol1zha+XhOtyhTCdSWce8GVD/zElklL3C0h9HrsTQrnNyouaZa9KlXZJ72XDCIkIlV0m6EtxbzdMwbH4sLFOpifRlRn+AmzXjxvLovRtn2bXh/X3bUgPxqypaST57Dlpddlk1Mt0oJmGFtwB/FH1JmK21cIC06uXtlUc8lm/9cTQLd5hcEUX+XRrmTdzqxDgRttN8CRfVUAGE7Er+prN4yCIdNtEQdZm8zymEpHTkYplJ/hK7SMf9Iu1k+eCDFYCzvQuzLcJtNpRaGS1BbVA3va5JKrEu96G7a3Wl3DyzmrH8N3+RA+UIHvP6P5v93tI/eTyfMY54rKpLGkfFeeSMAr5aSoUZVYkvFI8xGEcIrqLWPDF91MclLZa7USSVql0wYu1G9KD10IkopeKkTIAl81WfoY5+Kw1o4CHo7bEQ6tfTuTB4IEywf1XKMBYHmsfAe5B9ferkLYtnAzzt1hoiK1m/2CjX8yQAknRLsnAuyeXfJZRZidVKYOKaSDftddbXJpIlJApC"
|
||||
|
||||
anthropic_messages = [
|
||||
AnthropicMessagesUserMessageParam(
|
||||
role="user",
|
||||
content=[{"type": "text", "text": "What's the weather like in London?"}],
|
||||
),
|
||||
AnthopicMessagesAssistantMessageParam(
|
||||
role="assistant",
|
||||
content=[
|
||||
{
|
||||
"type": "tool_use",
|
||||
"id": "call_386f67af31f9415781bc35071405",
|
||||
"name": "get_weather",
|
||||
"input": {"location": "London"},
|
||||
"provider_specific_fields": {
|
||||
"signature": test_signature,
|
||||
},
|
||||
}
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
adapter = LiteLLMAnthropicMessagesAdapter()
|
||||
result = adapter.translate_anthropic_messages_to_openai(messages=anthropic_messages)
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[1]["role"] == "assistant"
|
||||
assert "tool_calls" in result[1]
|
||||
assert len(result[1]["tool_calls"]) == 1
|
||||
|
||||
# Verify thought signature is extracted and placed in provider_specific_fields
|
||||
tool_call = result[1]["tool_calls"][0]
|
||||
assert tool_call["id"] == "call_386f67af31f9415781bc35071405"
|
||||
assert "function" in tool_call
|
||||
assert "provider_specific_fields" in tool_call["function"]
|
||||
assert tool_call["function"]["provider_specific_fields"]["thought_signature"] == test_signature
|
||||
|
||||
@@ -390,3 +390,151 @@ def test_thought_signature_with_function_call_mode():
|
||||
assert "provider_specific_fields" in function
|
||||
assert function["provider_specific_fields"]["thought_signature"] == test_signature
|
||||
assert tools is None
|
||||
|
||||
|
||||
def test_dummy_signature_added_for_gemini_3_conversation_history():
|
||||
"""Test that dummy signatures are added when transferring conversation history from older models (like gemini-2.5-flash) to gemini-3."""
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
convert_to_gemini_tool_call_invoke,
|
||||
)
|
||||
import base64
|
||||
|
||||
# Simulate conversation history from gemini-2.5-flash (no thought signature)
|
||||
assistant_message_from_older_model = {
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_current_temperature",
|
||||
"arguments": '{"location": "Paris"}',
|
||||
},
|
||||
"index": 0,
|
||||
# No provider_specific_fields - older model doesn't provide signatures
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
# Convert to Gemini format for gemini-3-pro-preview (should add dummy signature)
|
||||
gemini_parts = convert_to_gemini_tool_call_invoke(
|
||||
assistant_message_from_older_model, model="gemini-3-pro-preview"
|
||||
)
|
||||
|
||||
# Verify dummy signature is added
|
||||
assert len(gemini_parts) == 1
|
||||
assert "function_call" in gemini_parts[0]
|
||||
assert "thoughtSignature" in gemini_parts[0]
|
||||
|
||||
# Verify it's the expected dummy signature (base64 encoded "skip_thought_signature_validator")
|
||||
expected_dummy = base64.b64encode(b"skip_thought_signature_validator").decode("utf-8")
|
||||
assert gemini_parts[0]["thoughtSignature"] == expected_dummy
|
||||
|
||||
|
||||
def test_dummy_signature_not_added_for_gemini_2_5():
|
||||
"""Test that dummy signatures are NOT added when target model is not gemini-3."""
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
convert_to_gemini_tool_call_invoke,
|
||||
)
|
||||
|
||||
# Simulate conversation history from gemini-2.5-flash (no thought signature)
|
||||
assistant_message = {
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_current_temperature",
|
||||
"arguments": '{"location": "Paris"}',
|
||||
},
|
||||
"index": 0,
|
||||
# No provider_specific_fields
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
# Convert to Gemini format for gemini-2.5-flash (should NOT add dummy signature)
|
||||
gemini_parts = convert_to_gemini_tool_call_invoke(
|
||||
assistant_message, model="gemini-2.5-flash"
|
||||
)
|
||||
|
||||
# Verify no dummy signature is added for non-gemini-3 models
|
||||
assert len(gemini_parts) == 1
|
||||
assert "function_call" in gemini_parts[0]
|
||||
assert "thoughtSignature" not in gemini_parts[0]
|
||||
|
||||
|
||||
def test_dummy_signature_not_added_when_signature_exists():
|
||||
"""Test that dummy signatures are NOT added when a real signature already exists."""
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
convert_to_gemini_tool_call_invoke,
|
||||
)
|
||||
|
||||
real_signature = "Co4CAdHtim/rWgXbz2Ghp4tShzLeMASrPw6JJyYIC3cbVyZnKzU3uv8/wVzyS2sKRPL2m8QQHHXbNQhEEz500G7n/4ZMmksdTtfQcJMoT76S1DGwhnAiLwTgWCNXs3lEb4M19EVYoWFxhrH5Lr9YMIquoU9U4paydGwvZyIyigamIg4B6WnxrRsf0KZV12gJed0DZuKczvOFtHz3zUnmZRlOiTzd5gBVyQM+5jv1VI8m4WUKd6cN/5a5ZvaA0ggiO6kdVhlpIVs7GczSEVJD8KH4u02X7VSnb7CvykqDntZzV0y8rZFBEFGKrChmeHlWXP4D1IB3F9KQyhuLgWImMzg4BajKVxxMU737JGnNISy5"
|
||||
|
||||
# Assistant message with existing thought signature
|
||||
assistant_message_with_signature = {
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_current_temperature",
|
||||
"arguments": '{"location": "Paris"}',
|
||||
"provider_specific_fields": {
|
||||
"thought_signature": real_signature,
|
||||
},
|
||||
},
|
||||
"index": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
# Convert to Gemini format for gemini-3-pro-preview
|
||||
gemini_parts = convert_to_gemini_tool_call_invoke(
|
||||
assistant_message_with_signature, model="gemini-3-pro-preview"
|
||||
)
|
||||
|
||||
# Verify real signature is preserved, not replaced with dummy
|
||||
assert len(gemini_parts) == 1
|
||||
assert "function_call" in gemini_parts[0]
|
||||
assert "thoughtSignature" in gemini_parts[0]
|
||||
assert gemini_parts[0]["thoughtSignature"] == real_signature
|
||||
|
||||
|
||||
def test_dummy_signature_with_function_call_mode():
|
||||
"""Test that dummy signatures are added for function_call mode when converting to gemini-3."""
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
convert_to_gemini_tool_call_invoke,
|
||||
)
|
||||
import base64
|
||||
|
||||
# Assistant message with function_call (not tool_calls) and no signature
|
||||
assistant_message_function_call = {
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"function_call": {
|
||||
"name": "get_current_temperature",
|
||||
"arguments": '{"location": "Paris"}',
|
||||
# No provider_specific_fields
|
||||
},
|
||||
}
|
||||
|
||||
# Convert to Gemini format for gemini-3-pro-preview
|
||||
gemini_parts = convert_to_gemini_tool_call_invoke(
|
||||
assistant_message_function_call, model="gemini-3-pro-preview"
|
||||
)
|
||||
|
||||
# Verify dummy signature is added
|
||||
assert len(gemini_parts) == 1
|
||||
assert "function_call" in gemini_parts[0]
|
||||
assert "thoughtSignature" in gemini_parts[0]
|
||||
|
||||
# Verify it's the expected dummy signature
|
||||
expected_dummy = base64.b64encode(b"skip_thought_signature_validator").decode("utf-8")
|
||||
assert gemini_parts[0]["thoughtSignature"] == expected_dummy
|
||||
|
||||
Reference in New Issue
Block a user