mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
Merge pull request #41892 from BerriAI/litellm_gemini_contentless_candidate_finish_reason
Some checks failed
CI Coverage / assert-ci-coverage (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Helm unit test / unit-test (push) Waiting to run
Publish basedpyright base counts / publish (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Code Quality Checks / code-quality (push) Waiting to run
Code Quality Checks / python-310-import-smoke (push) Waiting to run
UI Unit Tests / ui-unit-tests (push) Waiting to run
Postgres Tests / proxy-security (push) Waiting to run
Postgres Tests / schema-migration (push) Waiting to run
Postgres Tests / proxy-behavior (push) Waiting to run
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
LiteLLM Rust / rust-wheel (push) Waiting to run
Unit Tests: Documentation Validation / documentation (push) Waiting to run
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests / misc (push) Waiting to run
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests / caching-local (push) Waiting to run
Unit Tests / core-utils (push) Waiting to run
Unit Tests / enterprise-package (push) Waiting to run
Unit Tests / enterprise-routing (push) Waiting to run
Unit Tests / integrations (push) Waiting to run
Unit Tests / All Other Providers (push) Waiting to run
Unit Tests / Vertex AI (push) Waiting to run
Unit Tests / proxy-auth (push) Waiting to run
Unit Tests / proxy-endpoints (push) Waiting to run
Unit Tests / proxy-extras (push) Waiting to run
Unit Tests / proxy-infra (push) Waiting to run
Unit Tests / proxy-server (push) Waiting to run
Unit Tests / responses-caching-types (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
Issue label sync / sync-issue-labels-tests (push) Has been cancelled
Issue label sync / sync-issue-labels (push) Has been cancelled
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
Terraform Modules / fmt, validate, test (gcp) (push) Has been cancelled
VS Code Extension / vscode-extension (push) Has been cancelled
Some checks failed
CI Coverage / assert-ci-coverage (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Helm unit test / unit-test (push) Waiting to run
Publish basedpyright base counts / publish (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Code Quality Checks / code-quality (push) Waiting to run
Code Quality Checks / python-310-import-smoke (push) Waiting to run
UI Unit Tests / ui-unit-tests (push) Waiting to run
Postgres Tests / proxy-security (push) Waiting to run
Postgres Tests / schema-migration (push) Waiting to run
Postgres Tests / proxy-behavior (push) Waiting to run
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
LiteLLM Rust / rust-wheel (push) Waiting to run
Unit Tests: Documentation Validation / documentation (push) Waiting to run
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests / misc (push) Waiting to run
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests / caching-local (push) Waiting to run
Unit Tests / core-utils (push) Waiting to run
Unit Tests / enterprise-package (push) Waiting to run
Unit Tests / enterprise-routing (push) Waiting to run
Unit Tests / integrations (push) Waiting to run
Unit Tests / All Other Providers (push) Waiting to run
Unit Tests / Vertex AI (push) Waiting to run
Unit Tests / proxy-auth (push) Waiting to run
Unit Tests / proxy-endpoints (push) Waiting to run
Unit Tests / proxy-extras (push) Waiting to run
Unit Tests / proxy-infra (push) Waiting to run
Unit Tests / proxy-server (push) Waiting to run
Unit Tests / responses-caching-types (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
Issue label sync / sync-issue-labels-tests (push) Has been cancelled
Issue label sync / sync-issue-labels (push) Has been cancelled
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
Terraform Modules / fmt, validate, test (gcp) (push) Has been cancelled
VS Code Extension / vscode-extension (push) Has been cancelled
fix(gemini): preserve candidates with finishReason and no content (#40477)
This commit is contained in:
commit
ff7dc86947
9 changed files with 408 additions and 54 deletions
|
|
@ -224,6 +224,12 @@ _FINISH_REASON_MAP: Final[dict[str, OpenAIChatCompletionFinishReason]] = {
|
|||
"IMAGE_PROHIBITED_CONTENT": "content_filter",
|
||||
"TOO_MANY_TOOL_CALLS": "stop",
|
||||
"MALFORMED_RESPONSE": "stop",
|
||||
"NO_IMAGE": "content_filter",
|
||||
"IMAGE_RECITATION": "content_filter",
|
||||
"IMAGE_OTHER": "content_filter",
|
||||
"ESCALATION": "content_filter",
|
||||
"UNEXPECTED_TOOL_CALL": "stop",
|
||||
"MISSING_THOUGHT_SIGNATURE": "stop",
|
||||
# Zhipu GLM
|
||||
"network_error": "stop",
|
||||
"sensitive": "content_filter",
|
||||
|
|
|
|||
|
|
@ -1410,6 +1410,8 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
return "max_tokens"
|
||||
elif openai_finish_reason == "tool_calls":
|
||||
return "tool_use"
|
||||
elif openai_finish_reason in ["content_filter", "refusal"]:
|
||||
return "refusal"
|
||||
return "end_turn"
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import time
|
|||
from collections.abc import Callable, Mapping, Sequence
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, Optional, Union, cast
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, Optional, Union, cast, get_args
|
||||
|
||||
import httpx
|
||||
|
||||
|
|
@ -57,6 +57,7 @@ from litellm.types.llms.vertex_ai import (
|
|||
ContentType,
|
||||
FunctionCallingConfig,
|
||||
FunctionDeclaration,
|
||||
GeminiFinishReason,
|
||||
GeminiThinkingConfig,
|
||||
GenerateContentResponseBody,
|
||||
HttpxPartType,
|
||||
|
|
@ -1330,25 +1331,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
"IMAGE_PROHIBITED_CONTENT": "The token generation was stopped as the response was flagged for prohibited image content.",
|
||||
}
|
||||
|
||||
_GEMINI_FINISH_REASON_KEYS = frozenset(
|
||||
{
|
||||
"STOP",
|
||||
"MAX_TOKENS",
|
||||
"SAFETY",
|
||||
"RECITATION",
|
||||
"FINISH_REASON_UNSPECIFIED",
|
||||
"MALFORMED_FUNCTION_CALL",
|
||||
"LANGUAGE",
|
||||
"OTHER",
|
||||
"BLOCKLIST",
|
||||
"PROHIBITED_CONTENT",
|
||||
"SPII",
|
||||
"IMAGE_SAFETY",
|
||||
"IMAGE_PROHIBITED_CONTENT",
|
||||
"TOO_MANY_TOOL_CALLS",
|
||||
"MALFORMED_RESPONSE",
|
||||
}
|
||||
)
|
||||
_GEMINI_FINISH_REASON_KEYS: Final[frozenset[str]] = frozenset(get_args(GeminiFinishReason))
|
||||
|
||||
@staticmethod
|
||||
def get_finish_reason_mapping() -> dict[str, OpenAIChatCompletionFinishReason]:
|
||||
|
|
@ -2232,22 +2215,23 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
|
||||
grounding_metadata: Final[list[dict]] = []
|
||||
url_context_metadata: Final[list[dict]] = []
|
||||
image_response: list[ImageURLListItem] | None = None
|
||||
safety_ratings: Final[list] = []
|
||||
citation_metadata: Final[list] = []
|
||||
chat_completion_message: Final[ChatCompletionResponseMessage] = {"role": "assistant"}
|
||||
chat_completion_logprobs: ChoiceLogprobs | None = None
|
||||
tools: list[ChatCompletionToolCallChunk] | None = []
|
||||
functions: ChatCompletionToolCallFunctionChunk | None = None
|
||||
thinking_blocks: list[ChatCompletionThinkingBlock] | None = None
|
||||
reasoning_content: str | None = None
|
||||
thought_signatures: Sequence[str] | None = None
|
||||
server_side_tool_invocations: list[dict[str, object]] | None = None
|
||||
|
||||
for idx, candidate in enumerate(_candidates):
|
||||
if "content" not in candidate:
|
||||
if "content" not in candidate and "finishReason" not in candidate:
|
||||
continue
|
||||
|
||||
image_response: list[ImageURLListItem] | None = None
|
||||
chat_completion_message: ChatCompletionResponseMessage = {"role": "assistant"}
|
||||
chat_completion_logprobs: ChoiceLogprobs | None = None
|
||||
tools: list[ChatCompletionToolCallChunk] | None = None
|
||||
functions: ChatCompletionToolCallFunctionChunk | None = None
|
||||
thinking_blocks: list[ChatCompletionThinkingBlock] | None = None
|
||||
reasoning_content: str | None = None
|
||||
thought_signatures: Sequence[str] | None = None
|
||||
server_side_tool_invocations: list[dict[str, object]] | None = None
|
||||
|
||||
# Extract metadata using helper function
|
||||
(
|
||||
candidate_grounding_metadata,
|
||||
|
|
@ -2261,7 +2245,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
safety_ratings.extend(candidate_safety_ratings)
|
||||
citation_metadata.extend(candidate_citation_metadata)
|
||||
|
||||
if "parts" in candidate["content"]:
|
||||
if "content" in candidate and candidate["content"] and "parts" in candidate["content"]:
|
||||
(
|
||||
content,
|
||||
reasoning_content,
|
||||
|
|
@ -2368,14 +2352,18 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
)
|
||||
model_response.choices.append(choice)
|
||||
elif isinstance(model_response, ModelResponse):
|
||||
native_finish_reason = candidate.get("finishReason")
|
||||
choice = litellm.Choices(
|
||||
finish_reason=VertexGeminiConfig._check_finish_reason(
|
||||
chat_completion_message, candidate.get("finishReason")
|
||||
chat_completion_message, native_finish_reason
|
||||
),
|
||||
index=candidate.get("index", idx),
|
||||
message=chat_completion_message,
|
||||
logprobs=chat_completion_logprobs,
|
||||
enhancements=None,
|
||||
provider_specific_fields=(
|
||||
{"native_finish_reason": native_finish_reason} if native_finish_reason is not None else None
|
||||
),
|
||||
)
|
||||
model_response.choices.append(choice)
|
||||
|
||||
|
|
@ -3173,12 +3161,10 @@ class ModelResponseIterator:
|
|||
self.has_seen_tool_calls = True
|
||||
break
|
||||
|
||||
# _process_candidates skips candidates without a "content" part, so a
|
||||
# content-less chunk leaves choices empty and the downstream streaming
|
||||
# handler hits IndexError on choices[0]. This covers the final chunk
|
||||
# (finishReason, no content) and mid-stream metadata-only chunks
|
||||
# (grounding/web-search/thought, no content and no finishReason — seen
|
||||
# with web_search + reasoning) by emitting an empty-delta choice.
|
||||
# _process_candidates skips candidates with neither "content" nor
|
||||
# "finishReason", so a metadata-only chunk (grounding/web-search/thought,
|
||||
# seen with web_search + reasoning) leaves choices empty and the downstream
|
||||
# streaming handler hits IndexError on choices[0]. Emit an empty-delta choice.
|
||||
if not model_response.choices and _candidates:
|
||||
from litellm.types.utils import Delta, StreamingChoices
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ from litellm.types.llms.openai import (
|
|||
ChatCompletionToolParamFunctionChunk,
|
||||
ChatCompletionUserMessage,
|
||||
GenericChatCompletionMessage,
|
||||
IncompleteDetails,
|
||||
InputTokensDetails,
|
||||
OpenAIChatCompletionTextObject,
|
||||
OpenAIMcpServerTool,
|
||||
|
|
@ -111,6 +112,9 @@ ResponseTools: TypeAlias = Sequence[Mapping[str, object]] | None
|
|||
ChatToolParam: TypeAlias = ChatCompletionToolParam | OpenAIMcpServerTool
|
||||
NAMESPACE_DESCRIPTION_SEPARATOR: Final = "\n\n"
|
||||
NAMESPACE_MEMBER_TYPES_WITH_CHAT_TOOLS: Final = frozenset({"function", "custom"})
|
||||
_INCOMPLETE_REASON_BY_FINISH_REASON: Final[Mapping[str, Literal["max_output_tokens", "content_filter"]]] = (
|
||||
MappingProxyType({"length": "max_output_tokens", "content_filter": "content_filter", "refusal": "content_filter"})
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -2299,6 +2303,18 @@ class LiteLLMCompletionResponsesConfig:
|
|||
# Default to completed for unknown finish reasons
|
||||
return "completed"
|
||||
|
||||
@staticmethod
|
||||
def _incomplete_details_for_finish_reason(
|
||||
finish_reason: str | None,
|
||||
existing: IncompleteDetails | None,
|
||||
) -> IncompleteDetails | None:
|
||||
if existing is not None:
|
||||
return existing
|
||||
if finish_reason is None:
|
||||
return None
|
||||
reason: Final = _INCOMPLETE_REASON_BY_FINISH_REASON.get(finish_reason)
|
||||
return IncompleteDetails(reason=reason) if reason is not None else None
|
||||
|
||||
@staticmethod
|
||||
def _tool_call_id_from_responses_item(item_id: str | None, call_id: str | None) -> str:
|
||||
"""Bedrock Mantle returns a non-unique, index-based ``call_id`` (``call_0``,
|
||||
|
|
@ -2415,13 +2431,18 @@ class LiteLLMCompletionResponsesConfig:
|
|||
if choices and len(choices) > 0:
|
||||
finish_reason = choices[0].finish_reason
|
||||
|
||||
incomplete_details: Final = LiteLLMCompletionResponsesConfig._incomplete_details_for_finish_reason(
|
||||
finish_reason=finish_reason,
|
||||
existing=getattr(chat_completion_response, "incomplete_details", None),
|
||||
)
|
||||
|
||||
responses_api_response: Final[ResponsesAPIResponse] = ResponsesAPIResponse(
|
||||
id=chat_completion_response.id,
|
||||
created_at=chat_completion_response.created,
|
||||
model=chat_completion_response.model,
|
||||
object="response",
|
||||
error=getattr(chat_completion_response, "error", None),
|
||||
incomplete_details=getattr(chat_completion_response, "incomplete_details", None),
|
||||
incomplete_details=incomplete_details,
|
||||
instructions=getattr(chat_completion_response, "instructions", None),
|
||||
metadata=getattr(chat_completion_response, "metadata", {}),
|
||||
output=LiteLLMCompletionResponsesConfig._transform_chat_completion_choices_to_responses_output(
|
||||
|
|
|
|||
|
|
@ -425,22 +425,35 @@ class UrlContextMetadata(TypedDict, total=False):
|
|||
urlMetadata: list[UrlMetadata]
|
||||
|
||||
|
||||
GeminiFinishReason = Literal[
|
||||
"FINISH_REASON_UNSPECIFIED",
|
||||
"STOP",
|
||||
"MAX_TOKENS",
|
||||
"SAFETY",
|
||||
"RECITATION",
|
||||
"LANGUAGE",
|
||||
"OTHER",
|
||||
"BLOCKLIST",
|
||||
"PROHIBITED_CONTENT",
|
||||
"SPII",
|
||||
"MALFORMED_FUNCTION_CALL",
|
||||
"IMAGE_SAFETY",
|
||||
"IMAGE_PROHIBITED_CONTENT",
|
||||
"TOO_MANY_TOOL_CALLS",
|
||||
"MALFORMED_RESPONSE",
|
||||
"NO_IMAGE",
|
||||
"IMAGE_RECITATION",
|
||||
"IMAGE_OTHER",
|
||||
"ESCALATION",
|
||||
"UNEXPECTED_TOOL_CALL",
|
||||
"MISSING_THOUGHT_SIGNATURE",
|
||||
]
|
||||
|
||||
|
||||
class Candidates(TypedDict, total=False):
|
||||
index: int
|
||||
content: HttpxContentType
|
||||
finishReason: Literal[
|
||||
"FINISH_REASON_UNSPECIFIED",
|
||||
"STOP",
|
||||
"MAX_TOKENS",
|
||||
"SAFETY",
|
||||
"RECITATION",
|
||||
"OTHER",
|
||||
"BLOCKLIST",
|
||||
"PROHIBITED_CONTENT",
|
||||
"SPII",
|
||||
"MALFORMED_FUNCTION_CALL",
|
||||
"IMAGE_SAFETY",
|
||||
]
|
||||
finishReason: GeminiFinishReason
|
||||
safetyRatings: list[SafetyRatings]
|
||||
citationMetadata: CitationMetadata
|
||||
groundingMetadata: GroundingMetadata
|
||||
|
|
|
|||
|
|
@ -151,6 +151,12 @@ class TestMapFinishReasonGemini:
|
|||
("IMAGE_PROHIBITED_CONTENT", "content_filter"),
|
||||
("TOO_MANY_TOOL_CALLS", "stop"),
|
||||
("MALFORMED_RESPONSE", "stop"),
|
||||
("NO_IMAGE", "content_filter"),
|
||||
("IMAGE_RECITATION", "content_filter"),
|
||||
("IMAGE_OTHER", "content_filter"),
|
||||
("ESCALATION", "content_filter"),
|
||||
("UNEXPECTED_TOOL_CALL", "stop"),
|
||||
("MISSING_THOUGHT_SIGNATURE", "stop"),
|
||||
],
|
||||
)
|
||||
def test_gemini_finish_reasons(self, gemini_reason, expected):
|
||||
|
|
|
|||
|
|
@ -105,6 +105,46 @@ def test_translate_chat_length_takes_precedence_over_refusal():
|
|||
assert result.get("stop_details") is None
|
||||
|
||||
|
||||
def test_translate_chat_content_filter_to_anthropic_response():
|
||||
response = ModelResponse(
|
||||
id="chatcmpl-content-filter",
|
||||
model="openai-model",
|
||||
choices=[
|
||||
Choices(
|
||||
index=0,
|
||||
finish_reason="content_filter",
|
||||
message=Message(content=None, role="assistant"),
|
||||
)
|
||||
],
|
||||
usage=Usage(prompt_tokens=1, completion_tokens=0, total_tokens=1),
|
||||
)
|
||||
|
||||
result = LiteLLMAnthropicMessagesAdapter().translate_openai_response_to_anthropic(response)
|
||||
|
||||
assert result["content"] == []
|
||||
assert result["stop_reason"] == "refusal"
|
||||
|
||||
|
||||
def test_translate_chat_refusal_finish_reason_to_anthropic_response():
|
||||
response = ModelResponse(
|
||||
id="chatcmpl-refusal-reason",
|
||||
model="openai-model",
|
||||
choices=[
|
||||
Choices(
|
||||
index=0,
|
||||
finish_reason="refusal",
|
||||
message=Message(content=None, role="assistant"),
|
||||
)
|
||||
],
|
||||
usage=Usage(prompt_tokens=1, completion_tokens=0, total_tokens=1),
|
||||
)
|
||||
|
||||
result = LiteLLMAnthropicMessagesAdapter().translate_openai_response_to_anthropic(response)
|
||||
|
||||
assert result["content"] == []
|
||||
assert result["stop_reason"] == "refusal"
|
||||
|
||||
|
||||
def test_translate_streaming_openai_chunk_to_anthropic_content_block():
|
||||
choices = [
|
||||
StreamingChoices(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import asyncio
|
|||
import json
|
||||
import re
|
||||
from copy import deepcopy
|
||||
from typing import Final, List, cast
|
||||
from typing import Final, List, cast, get_args
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import httpx
|
||||
|
|
@ -18,7 +18,7 @@ from litellm.llms.vertex_ai.common_utils import VertexAIError
|
|||
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
|
||||
VertexGeminiConfig,
|
||||
)
|
||||
from litellm.types.llms.vertex_ai import UsageMetadata
|
||||
from litellm.types.llms.vertex_ai import GeminiFinishReason, UsageMetadata
|
||||
from litellm.types.utils import ChoiceLogprobs, Usage
|
||||
from litellm.utils import CustomStreamWrapper
|
||||
|
||||
|
|
@ -940,6 +940,11 @@ def test_check_finish_reason():
|
|||
)
|
||||
|
||||
|
||||
def test_every_documented_gemini_finish_reason_has_an_explicit_mapping():
|
||||
documented: Final = frozenset(get_args(GeminiFinishReason))
|
||||
assert set(VertexGeminiConfig.get_finish_reason_mapping()) == documented
|
||||
|
||||
|
||||
def test_finish_reason_unspecified_and_malformed_function_call():
|
||||
"""
|
||||
Test that FINISH_REASON_UNSPECIFIED and MALFORMED_FUNCTION_CALL
|
||||
|
|
@ -968,6 +973,12 @@ def test_finish_reason_unspecified_and_malformed_function_call():
|
|||
# Test new Gemini finish reasons
|
||||
assert finish_reason_mappings["TOO_MANY_TOOL_CALLS"] == "stop"
|
||||
assert finish_reason_mappings["MALFORMED_RESPONSE"] == "stop"
|
||||
assert finish_reason_mappings["NO_IMAGE"] == "content_filter"
|
||||
assert finish_reason_mappings["IMAGE_RECITATION"] == "content_filter"
|
||||
assert finish_reason_mappings["IMAGE_OTHER"] == "content_filter"
|
||||
assert finish_reason_mappings["ESCALATION"] == "content_filter"
|
||||
assert finish_reason_mappings["UNEXPECTED_TOOL_CALL"] == "stop"
|
||||
assert finish_reason_mappings["MISSING_THOUGHT_SIGNATURE"] == "stop"
|
||||
|
||||
|
||||
def test_vertex_ai_usage_metadata_response_token_count():
|
||||
|
|
@ -6074,3 +6085,210 @@ def test_prompt_blocked_chunk_keeps_served_model_version():
|
|||
|
||||
assert streaming_chunk.model == "gemini-3.8-flash-001"
|
||||
assert streaming_chunk.choices[0].finish_reason == "content_filter"
|
||||
|
||||
|
||||
def test_gemini_candidate_with_finish_reason_no_content_chat_completion():
|
||||
config = VertexGeminiConfig()
|
||||
completion_response = {
|
||||
"candidates": [
|
||||
{
|
||||
"finishReason": "NO_IMAGE",
|
||||
"index": 0,
|
||||
}
|
||||
],
|
||||
"usageMetadata": {
|
||||
"promptTokenCount": 19,
|
||||
"candidatesTokenCount": 0,
|
||||
"totalTokenCount": 19,
|
||||
},
|
||||
}
|
||||
model_response = ModelResponse()
|
||||
logging_obj = MagicMock()
|
||||
raw_response = MagicMock()
|
||||
raw_response.headers = {}
|
||||
|
||||
resp = config._transform_google_generate_content_to_openai_model_response(
|
||||
completion_response=completion_response,
|
||||
model_response=model_response,
|
||||
model="gemini-2.5-flash-image",
|
||||
logging_obj=logging_obj,
|
||||
raw_response=raw_response,
|
||||
)
|
||||
assert len(resp.choices) == 1
|
||||
assert resp.choices[0].finish_reason == "content_filter"
|
||||
assert resp.choices[0].message.content is None
|
||||
assert resp.choices[0].provider_specific_fields["native_finish_reason"] == "NO_IMAGE"
|
||||
|
||||
|
||||
def test_gemini_candidate_with_finish_reason_no_content_anthropic_messages():
|
||||
from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import (
|
||||
LiteLLMAnthropicMessagesAdapter,
|
||||
)
|
||||
|
||||
config = VertexGeminiConfig()
|
||||
completion_response = {
|
||||
"candidates": [
|
||||
{
|
||||
"finishReason": "NO_IMAGE",
|
||||
"index": 0,
|
||||
}
|
||||
],
|
||||
"usageMetadata": {
|
||||
"promptTokenCount": 19,
|
||||
"candidatesTokenCount": 0,
|
||||
"totalTokenCount": 19,
|
||||
},
|
||||
}
|
||||
resp = config._transform_google_generate_content_to_openai_model_response(
|
||||
completion_response=completion_response,
|
||||
model_response=ModelResponse(),
|
||||
model="gemini-2.5-flash-image",
|
||||
logging_obj=MagicMock(),
|
||||
raw_response=MagicMock(headers={}),
|
||||
)
|
||||
|
||||
adapter = LiteLLMAnthropicMessagesAdapter()
|
||||
anthropic_resp = adapter.translate_openai_response_to_anthropic(
|
||||
response=resp,
|
||||
tool_name_mapping={},
|
||||
)
|
||||
assert anthropic_resp["stop_reason"] == "refusal"
|
||||
assert anthropic_resp["content"] == []
|
||||
|
||||
|
||||
def test_gemini_candidate_with_finish_reason_no_content_responses_api():
|
||||
from litellm.responses.litellm_completion_transformation.transformation import (
|
||||
LiteLLMCompletionResponsesConfig,
|
||||
)
|
||||
|
||||
config = VertexGeminiConfig()
|
||||
completion_response = {
|
||||
"candidates": [
|
||||
{
|
||||
"finishReason": "NO_IMAGE",
|
||||
"index": 0,
|
||||
}
|
||||
],
|
||||
"usageMetadata": {
|
||||
"promptTokenCount": 19,
|
||||
"candidatesTokenCount": 0,
|
||||
"totalTokenCount": 19,
|
||||
},
|
||||
}
|
||||
resp = config._transform_google_generate_content_to_openai_model_response(
|
||||
completion_response=completion_response,
|
||||
model_response=ModelResponse(),
|
||||
model="gemini-2.5-flash-image",
|
||||
logging_obj=MagicMock(),
|
||||
raw_response=MagicMock(headers={}),
|
||||
)
|
||||
|
||||
responses_resp = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response(
|
||||
request_input="Generate picture",
|
||||
responses_api_request={},
|
||||
chat_completion_response=resp,
|
||||
)
|
||||
assert responses_resp.status == "incomplete"
|
||||
assert responses_resp.incomplete_details is not None
|
||||
assert responses_resp.incomplete_details.reason == "content_filter"
|
||||
|
||||
|
||||
def test_gemini_candidate_other_finish_reasons_no_content():
|
||||
from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import (
|
||||
LiteLLMAnthropicMessagesAdapter,
|
||||
)
|
||||
from litellm.responses.litellm_completion_transformation.transformation import (
|
||||
LiteLLMCompletionResponsesConfig,
|
||||
)
|
||||
|
||||
config = VertexGeminiConfig()
|
||||
max_tokens_response = {
|
||||
"candidates": [{"finishReason": "MAX_TOKENS", "index": 0}],
|
||||
"usageMetadata": {"promptTokenCount": 10, "candidatesTokenCount": 50, "totalTokenCount": 60},
|
||||
}
|
||||
resp_length = config._transform_google_generate_content_to_openai_model_response(
|
||||
completion_response=max_tokens_response,
|
||||
model_response=ModelResponse(),
|
||||
model="gemini-2.5-flash",
|
||||
logging_obj=MagicMock(),
|
||||
raw_response=MagicMock(headers={}),
|
||||
)
|
||||
assert len(resp_length.choices) == 1
|
||||
assert resp_length.choices[0].finish_reason == "length"
|
||||
assert resp_length.choices[0].provider_specific_fields["native_finish_reason"] == "MAX_TOKENS"
|
||||
|
||||
anthropic_length = LiteLLMAnthropicMessagesAdapter().translate_openai_response_to_anthropic(
|
||||
response=resp_length,
|
||||
tool_name_mapping={},
|
||||
)
|
||||
assert anthropic_length["stop_reason"] == "max_tokens"
|
||||
|
||||
responses_length = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response(
|
||||
request_input="thinking request",
|
||||
responses_api_request={},
|
||||
chat_completion_response=resp_length,
|
||||
)
|
||||
assert responses_length.status == "incomplete"
|
||||
assert responses_length.incomplete_details.reason == "max_output_tokens"
|
||||
|
||||
|
||||
def test_gemini_candidate_with_finish_reason_no_content_streaming_chunk():
|
||||
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
|
||||
ModelResponseIterator,
|
||||
)
|
||||
|
||||
chunk: Final = {
|
||||
"candidates": [{"finishReason": "NO_IMAGE", "index": 0}],
|
||||
"usageMetadata": {"promptTokenCount": 19, "candidatesTokenCount": 0, "totalTokenCount": 19},
|
||||
}
|
||||
iterator: Final = ModelResponseIterator(streaming_response=[], sync_stream=True, logging_obj=MagicMock())
|
||||
|
||||
streaming_chunk: Final = iterator.chunk_parser(chunk)
|
||||
|
||||
assert len(streaming_chunk.choices) == 1
|
||||
assert streaming_chunk.choices[0].finish_reason == "content_filter"
|
||||
assert streaming_chunk.choices[0].delta.content is None
|
||||
assert streaming_chunk.choices[0].delta.tool_calls is None
|
||||
|
||||
|
||||
def test_gemini_multi_candidate_messages_do_not_share_state():
|
||||
config: Final = VertexGeminiConfig()
|
||||
completion_response: Final = {
|
||||
"candidates": [
|
||||
{
|
||||
"content": {
|
||||
"role": "model",
|
||||
"parts": [
|
||||
{"text": "Let me check the weather.", "thought": True},
|
||||
{"functionCall": {"name": "get_weather", "args": {"city": "Paris"}}},
|
||||
],
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
"index": 0,
|
||||
},
|
||||
{
|
||||
"content": {"role": "model", "parts": [{"text": "It is sunny in Paris."}]},
|
||||
"finishReason": "STOP",
|
||||
"index": 1,
|
||||
},
|
||||
],
|
||||
"usageMetadata": {"promptTokenCount": 10, "candidatesTokenCount": 20, "totalTokenCount": 30},
|
||||
}
|
||||
|
||||
resp: Final = config._transform_google_generate_content_to_openai_model_response(
|
||||
completion_response=completion_response,
|
||||
model_response=ModelResponse(),
|
||||
model="gemini-2.5-flash",
|
||||
logging_obj=MagicMock(),
|
||||
raw_response=MagicMock(headers={}),
|
||||
)
|
||||
|
||||
assert len(resp.choices) == 2
|
||||
assert resp.choices[0].finish_reason == "tool_calls"
|
||||
assert resp.choices[0].message.tool_calls[0].function.name == "get_weather"
|
||||
assert resp.choices[0].message.reasoning_content == "Let me check the weather."
|
||||
assert resp.choices[1].finish_reason == "stop"
|
||||
assert resp.choices[1].message.content == "It is sunny in Paris."
|
||||
assert resp.choices[1].message.tool_calls is None
|
||||
assert getattr(resp.choices[1].message, "reasoning_content", None) is None
|
||||
assert resp.choices[1].provider_specific_fields["native_finish_reason"] == "STOP"
|
||||
|
|
|
|||
|
|
@ -4938,3 +4938,65 @@ class TestStreamingSnapshotItemIds:
|
|||
reasoning_items = _bridged_output_items(completed_event.response, "reasoning")
|
||||
assert len(reasoning_items) == 1
|
||||
assert reasoning_items[0].id == streamed_event.item_id
|
||||
|
||||
|
||||
def test_transform_chat_completion_response_incomplete_details():
|
||||
from litellm.types.llms.openai import IncompleteDetails
|
||||
|
||||
resp_length = ModelResponse(
|
||||
id="resp-length",
|
||||
choices=[Choices(index=0, finish_reason="length", message=Message(content="cutoff", role="assistant"))],
|
||||
model="gpt-4o",
|
||||
)
|
||||
result_length = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response(
|
||||
request_input="test prompt",
|
||||
responses_api_request={},
|
||||
chat_completion_response=resp_length,
|
||||
)
|
||||
assert result_length.status == "incomplete"
|
||||
assert result_length.incomplete_details is not None
|
||||
assert result_length.incomplete_details.reason == "max_output_tokens"
|
||||
|
||||
resp_filter = ModelResponse(
|
||||
id="resp-filter",
|
||||
choices=[Choices(index=0, finish_reason="content_filter", message=Message(content=None, role="assistant"))],
|
||||
model="gpt-4o",
|
||||
)
|
||||
result_filter = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response(
|
||||
request_input="test prompt",
|
||||
responses_api_request={},
|
||||
chat_completion_response=resp_filter,
|
||||
)
|
||||
assert result_filter.status == "incomplete"
|
||||
assert result_filter.incomplete_details is not None
|
||||
assert result_filter.incomplete_details.reason == "content_filter"
|
||||
|
||||
resp_refusal = ModelResponse(
|
||||
id="resp-refusal",
|
||||
choices=[Choices(index=0, finish_reason="refusal", message=Message(content=None, role="assistant"))],
|
||||
model="gpt-4o",
|
||||
)
|
||||
result_refusal = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response(
|
||||
request_input="test prompt",
|
||||
responses_api_request={},
|
||||
chat_completion_response=resp_refusal,
|
||||
)
|
||||
assert result_refusal.status == "incomplete"
|
||||
assert result_refusal.incomplete_details is not None
|
||||
assert result_refusal.incomplete_details.reason == "content_filter"
|
||||
|
||||
existing_details = IncompleteDetails(reason="content_filter")
|
||||
resp_existing = ModelResponse(
|
||||
id="resp-existing",
|
||||
choices=[Choices(index=0, finish_reason="length", message=Message(content="cutoff", role="assistant"))],
|
||||
model="gpt-4o",
|
||||
)
|
||||
resp_existing.incomplete_details = existing_details
|
||||
result_existing = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response(
|
||||
request_input="test prompt",
|
||||
responses_api_request={},
|
||||
chat_completion_response=resp_existing,
|
||||
)
|
||||
assert result_existing.status == "incomplete"
|
||||
assert result_existing.incomplete_details == existing_details
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue