mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-11 14:22:48 +00:00
@@ -218,6 +218,7 @@ class OCIChatConfig(BaseConfig):
|
||||
"parallel_tool_calls": False,
|
||||
"audio": False,
|
||||
"web_search_options": False,
|
||||
"response_format": "responseFormat",
|
||||
}
|
||||
|
||||
# Cohere and Gemini use the same parameter mapping as GENERIC
|
||||
@@ -269,6 +270,9 @@ class OCIChatConfig(BaseConfig):
|
||||
|
||||
adapted_params[alias] = value
|
||||
|
||||
if alias == "responseFormat":
|
||||
adapted_params["response_format"] = value
|
||||
|
||||
return adapted_params
|
||||
|
||||
def _sign_with_oci_signer(
|
||||
@@ -673,6 +677,36 @@ class OCIChatConfig(BaseConfig):
|
||||
selected_params["tools"] = adapt_tool_definition_to_oci_standard( # type: ignore[assignment]
|
||||
selected_params["tools"], vendor # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
# Transform response_format type to OCI uppercase format
|
||||
if "responseFormat" in selected_params:
|
||||
rf = selected_params["responseFormat"]
|
||||
if isinstance(rf, dict) and "type" in rf:
|
||||
rf_payload = dict(rf)
|
||||
selected_params["responseFormat"] = rf_payload
|
||||
|
||||
response_type = rf_payload["type"]
|
||||
schema_payload: Optional[Any] = None
|
||||
|
||||
if "json_schema" in rf_payload:
|
||||
raw_schema_payload = rf_payload.pop("json_schema")
|
||||
if isinstance(raw_schema_payload, dict):
|
||||
schema_payload = dict(raw_schema_payload)
|
||||
else:
|
||||
schema_payload = raw_schema_payload
|
||||
|
||||
if schema_payload is not None:
|
||||
rf_payload["jsonSchema"] = schema_payload
|
||||
|
||||
if vendor == OCIVendors.COHERE:
|
||||
# Cohere expects lower-case type values
|
||||
rf_payload["type"] = response_type
|
||||
else:
|
||||
format_type = response_type.upper()
|
||||
if format_type == "JSON":
|
||||
format_type = "JSON_OBJECT"
|
||||
rf_payload["type"] = format_type
|
||||
|
||||
return selected_params
|
||||
|
||||
def adapt_messages_to_cohere_standard(self, messages: List[AllMessageValues]) -> List[CohereMessage]:
|
||||
@@ -806,11 +840,12 @@ class OCIChatConfig(BaseConfig):
|
||||
|
||||
|
||||
# Create Cohere-specific chat request
|
||||
optional_cohere_params = self._get_optional_params(OCIVendors.COHERE, optional_params)
|
||||
chat_request = CohereChatRequest(
|
||||
apiFormat="COHERE",
|
||||
message=self._extract_text_content(user_messages[-1]["content"]),
|
||||
chatHistory=self.adapt_messages_to_cohere_standard(messages),
|
||||
**self._get_optional_params(OCIVendors.COHERE, optional_params)
|
||||
**optional_cohere_params
|
||||
)
|
||||
|
||||
data = OCICompletionPayload(
|
||||
|
||||
+32
-26
@@ -102,6 +102,7 @@ class OCIChatRequestPayload(BaseModel):
|
||||
seed: Optional[int] = None
|
||||
frequencyPenalty: Optional[float] = None
|
||||
presencePenalty: Optional[float] = None
|
||||
responseFormat: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class OCIServingMode(BaseModel):
|
||||
@@ -125,14 +126,14 @@ class OCICompletionPayload(BaseModel):
|
||||
class OCICompletionTokenDetails(BaseModel):
|
||||
"""Completion token details in the OCI response."""
|
||||
|
||||
acceptedPredictionTokens: int
|
||||
reasoningTokens: int
|
||||
acceptedPredictionTokens: Optional[int] = None
|
||||
reasoningTokens: Optional[int] = None
|
||||
|
||||
|
||||
class OCIPromptTokensDetails(BaseModel):
|
||||
"""Prompt token details in the OCI response."""
|
||||
|
||||
cachedTokens: int
|
||||
cachedTokens: Optional[int] = None
|
||||
|
||||
|
||||
class OCIResponseUsage(BaseModel):
|
||||
@@ -205,40 +206,40 @@ class CohereStreamChunk(BaseModel):
|
||||
|
||||
class CohereMessage(BaseModel):
|
||||
"""Base model for Cohere messages."""
|
||||
|
||||
|
||||
role: str
|
||||
message: str
|
||||
message: Optional[str] = None
|
||||
toolCalls: Optional[List[CohereToolCall]] = None
|
||||
|
||||
|
||||
class CohereUserMessage(CohereMessage):
|
||||
"""User message in Cohere chat."""
|
||||
|
||||
|
||||
role: Literal["USER"] = "USER"
|
||||
|
||||
|
||||
class CohereChatBotMessage(CohereMessage):
|
||||
"""Chatbot message in Cohere chat."""
|
||||
|
||||
|
||||
role: Literal["CHATBOT"] = "CHATBOT"
|
||||
|
||||
|
||||
class CohereSystemMessage(CohereMessage):
|
||||
"""System message in Cohere chat."""
|
||||
|
||||
|
||||
role: Literal["SYSTEM"] = "SYSTEM"
|
||||
|
||||
|
||||
class CohereToolMessage(CohereMessage):
|
||||
"""Tool message in Cohere chat."""
|
||||
|
||||
|
||||
role: Literal["TOOL"] = "TOOL"
|
||||
toolCallId: str
|
||||
|
||||
|
||||
class CohereParameterDefinition(BaseModel):
|
||||
"""Parameter definition for Cohere tools."""
|
||||
|
||||
|
||||
description: str
|
||||
type: str
|
||||
isRequired: bool = False
|
||||
@@ -246,7 +247,7 @@ class CohereParameterDefinition(BaseModel):
|
||||
|
||||
class CohereTool(BaseModel):
|
||||
"""Tool definition for Cohere."""
|
||||
|
||||
|
||||
name: str
|
||||
description: str
|
||||
parameterDefinitions: Dict[str, CohereParameterDefinition]
|
||||
@@ -254,38 +255,44 @@ class CohereTool(BaseModel):
|
||||
|
||||
class CohereToolCall(BaseModel):
|
||||
"""Tool call made by Cohere model."""
|
||||
|
||||
|
||||
name: str
|
||||
parameters: Dict[str, Any]
|
||||
|
||||
|
||||
class CohereToolResult(BaseModel):
|
||||
"""Result of a tool call."""
|
||||
|
||||
|
||||
callId: str
|
||||
result: str
|
||||
|
||||
|
||||
class CohereResponseFormat(BaseModel):
|
||||
"""Response format for Cohere."""
|
||||
|
||||
|
||||
type: str
|
||||
|
||||
|
||||
class CohereResponseTextFormat(CohereResponseFormat):
|
||||
"""Text response format for Cohere."""
|
||||
|
||||
|
||||
type: Literal["text"] = "text"
|
||||
|
||||
|
||||
class CohereResponseJSONSchemaFormat(CohereResponseFormat):
|
||||
"""JSON schema response format for Cohere."""
|
||||
|
||||
type: Literal["json_schema"] = "json_schema"
|
||||
jsonSchema: Dict[str, Any]
|
||||
|
||||
|
||||
class CohereChatRequest(BaseModel):
|
||||
"""Cohere chat request model."""
|
||||
|
||||
|
||||
# Required fields
|
||||
message: str
|
||||
apiFormat: Literal["COHERE"] = "COHERE"
|
||||
|
||||
|
||||
# Optional fields
|
||||
chatHistory: Optional[List[CohereMessage]] = None
|
||||
maxTokens: Optional[int] = None
|
||||
@@ -298,7 +305,7 @@ class CohereChatRequest(BaseModel):
|
||||
seed: Optional[int] = None
|
||||
tools: Optional[List[CohereTool]] = None
|
||||
toolChoice: Optional[Union[str, Dict[str, Any]]] = None
|
||||
responseFormat: Optional[CohereResponseFormat] = None
|
||||
responseFormat: Optional[Union[CohereResponseTextFormat, CohereResponseJSONSchemaFormat, CohereResponseFormat]] = None
|
||||
preambleOverride: Optional[str] = None
|
||||
documents: Optional[List[Dict[str, Any]]] = None
|
||||
searchQueriesOnly: Optional[bool] = None
|
||||
@@ -318,7 +325,7 @@ class CohereChatRequest(BaseModel):
|
||||
|
||||
class CohereUsage(BaseModel):
|
||||
"""Usage information for Cohere response."""
|
||||
|
||||
|
||||
promptTokens: int
|
||||
completionTokens: int
|
||||
totalTokens: int
|
||||
@@ -328,7 +335,7 @@ class CohereUsage(BaseModel):
|
||||
|
||||
class CohereCitation(BaseModel):
|
||||
"""Citation in Cohere response."""
|
||||
|
||||
|
||||
start: int
|
||||
end: int
|
||||
text: str
|
||||
@@ -337,19 +344,19 @@ class CohereCitation(BaseModel):
|
||||
|
||||
class CohereSearchQuery(BaseModel):
|
||||
"""Search query generated by Cohere."""
|
||||
|
||||
|
||||
text: str
|
||||
generation_id: str
|
||||
|
||||
|
||||
class CohereChatResponse(BaseModel):
|
||||
"""Cohere chat response model."""
|
||||
|
||||
|
||||
# Required fields
|
||||
text: str
|
||||
apiFormat: Literal["COHERE"] = "COHERE"
|
||||
finishReason: Literal["COMPLETE", "ERROR_TOXIC", "ERROR_LIMIT", "ERROR", "USER_CANCEL", "MAX_TOKENS"]
|
||||
|
||||
|
||||
# Optional fields
|
||||
chatHistory: Optional[List[CohereMessage]] = None
|
||||
citations: Optional[List[CohereCitation]] = None
|
||||
@@ -364,7 +371,7 @@ class CohereChatResponse(BaseModel):
|
||||
|
||||
class CohereChatDetails(BaseModel):
|
||||
"""Chat details for Cohere request."""
|
||||
|
||||
|
||||
compartmentId: str
|
||||
servingMode: OCIServingMode
|
||||
chatRequest: CohereChatRequest
|
||||
@@ -372,8 +379,7 @@ class CohereChatDetails(BaseModel):
|
||||
|
||||
class CohereChatResult(BaseModel):
|
||||
"""Complete Cohere chat result."""
|
||||
|
||||
|
||||
modelId: str
|
||||
modelVersion: str
|
||||
chatResponse: CohereChatResponse
|
||||
|
||||
|
||||
@@ -287,6 +287,114 @@ class TestOCIChatConfig:
|
||||
# Verify the message content
|
||||
assert transformed_request["chatRequest"]["message"] == "What is quantum computing?"
|
||||
|
||||
def test_transform_request_response_format_json_object(self):
|
||||
"""
|
||||
Tests that response_format type 'json_object' is uppercased to 'JSON_OBJECT' for generic OCI models.
|
||||
"""
|
||||
config = OCIChatConfig()
|
||||
optional_params = {
|
||||
"oci_compartment_id": TEST_COMPARTMENT_ID,
|
||||
"response_format": {"type": "json_object"},
|
||||
}
|
||||
transformed_request = config.transform_request(
|
||||
model=TEST_MODEL_NAME,
|
||||
messages=TEST_MESSAGES, # type: ignore
|
||||
optional_params=optional_params,
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
rf = transformed_request["chatRequest"]["responseFormat"]
|
||||
assert rf["type"] == "JSON_OBJECT"
|
||||
|
||||
def test_transform_request_response_format_text(self):
|
||||
"""
|
||||
Tests that response_format type 'text' is uppercased to 'TEXT' for generic OCI models.
|
||||
"""
|
||||
config = OCIChatConfig()
|
||||
optional_params = {
|
||||
"oci_compartment_id": TEST_COMPARTMENT_ID,
|
||||
"response_format": {"type": "text"},
|
||||
}
|
||||
transformed_request = config.transform_request(
|
||||
model=TEST_MODEL_NAME,
|
||||
messages=TEST_MESSAGES, # type: ignore
|
||||
optional_params=optional_params,
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
rf = transformed_request["chatRequest"]["responseFormat"]
|
||||
assert rf["type"] == "TEXT"
|
||||
|
||||
def test_transform_request_response_format_json_shorthand(self):
|
||||
"""
|
||||
Tests that response_format type 'json' is mapped to 'JSON_OBJECT' for generic OCI models.
|
||||
"""
|
||||
config = OCIChatConfig()
|
||||
optional_params = {
|
||||
"oci_compartment_id": TEST_COMPARTMENT_ID,
|
||||
"response_format": {"type": "json"},
|
||||
}
|
||||
transformed_request = config.transform_request(
|
||||
model=TEST_MODEL_NAME,
|
||||
messages=TEST_MESSAGES, # type: ignore
|
||||
optional_params=optional_params,
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
rf = transformed_request["chatRequest"]["responseFormat"]
|
||||
assert rf["type"] == "JSON_OBJECT"
|
||||
|
||||
def test_transform_response_without_token_details(self):
|
||||
"""
|
||||
Tests that responses missing completionTokensDetails and promptTokensDetails
|
||||
are handled correctly (fields are optional).
|
||||
"""
|
||||
config = OCIChatConfig()
|
||||
created_time = datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
mock_oci_response = {
|
||||
"modelId": TEST_MODEL_NAME,
|
||||
"modelVersion": "1.0",
|
||||
"chatResponse": {
|
||||
"apiFormat": "GENERIC",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "ASSISTANT",
|
||||
"content": [{"type": "TEXT", "text": "Hello!"}],
|
||||
},
|
||||
"finishReason": "STOP",
|
||||
}
|
||||
],
|
||||
"timeCreated": created_time,
|
||||
"usage": {
|
||||
"promptTokens": 5,
|
||||
"completionTokens": 10,
|
||||
"totalTokens": 15,
|
||||
},
|
||||
},
|
||||
}
|
||||
response = httpx.Response(
|
||||
status_code=200, json=mock_oci_response, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
result = config.transform_response(
|
||||
model=TEST_MODEL_NAME,
|
||||
raw_response=response,
|
||||
model_response=ModelResponse(),
|
||||
logging_obj={}, # type: ignore
|
||||
request_data={},
|
||||
messages=[],
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
encoding={},
|
||||
)
|
||||
|
||||
assert isinstance(result, ModelResponse)
|
||||
assert result.choices[0].message.content == "Hello!"
|
||||
assert result.usage.prompt_tokens == 5 # type: ignore
|
||||
assert result.usage.completion_tokens == 10 # type: ignore
|
||||
assert result.usage.total_tokens == 15 # type: ignore
|
||||
|
||||
def test_transform_response_simple_text(self):
|
||||
"""
|
||||
Tests if a simple text response is transformed correctly.
|
||||
|
||||
@@ -239,6 +239,110 @@ class TestOCICohereToolCalls:
|
||||
assert result.usage.completion_tokens == 22
|
||||
assert result.usage.total_tokens == 48
|
||||
|
||||
def test_cohere_request_preserves_json_schema_response_format(self):
|
||||
"""Ensure Cohere requests retain JSON schema payloads in responseFormat."""
|
||||
config = OCIChatConfig()
|
||||
messages = [{"role": "user", "content": "Return structured info"}]
|
||||
response_format = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "test_schema",
|
||||
"strict": True,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"foo": {"type": "string"}
|
||||
},
|
||||
"required": ["foo"]
|
||||
}
|
||||
}
|
||||
}
|
||||
optional_params = {
|
||||
"oci_compartment_id": TEST_COMPARTMENT_ID,
|
||||
"response_format": response_format,
|
||||
}
|
||||
|
||||
transformed_request = config.transform_request(
|
||||
model="cohere.command-rplus",
|
||||
messages=messages, # type: ignore[arg-type]
|
||||
optional_params=optional_params,
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
chat_request = transformed_request["chatRequest"]
|
||||
assert chat_request["apiFormat"] == "COHERE"
|
||||
assert "responseFormat" in chat_request
|
||||
|
||||
cohere_response_format = chat_request["responseFormat"]
|
||||
assert cohere_response_format["type"] == "json_schema"
|
||||
assert "json_schema" not in cohere_response_format
|
||||
assert "jsonSchema" in cohere_response_format
|
||||
assert cohere_response_format["jsonSchema"] == response_format["json_schema"]
|
||||
|
||||
def test_cohere_request_response_format_text_stays_lowercase(self):
|
||||
"""Ensure Cohere keeps response_format type lowercase (e.g. 'text' not 'TEXT')."""
|
||||
config = OCIChatConfig()
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
optional_params = {
|
||||
"oci_compartment_id": TEST_COMPARTMENT_ID,
|
||||
"response_format": {"type": "text"},
|
||||
}
|
||||
|
||||
transformed_request = config.transform_request(
|
||||
model="cohere.command-latest",
|
||||
messages=messages, # type: ignore
|
||||
optional_params=optional_params,
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
chat_request = transformed_request["chatRequest"]
|
||||
assert chat_request["apiFormat"] == "COHERE"
|
||||
assert "responseFormat" in chat_request
|
||||
assert chat_request["responseFormat"]["type"] == "text"
|
||||
|
||||
def test_cohere_tool_call_only_message_no_text(self):
|
||||
"""Test chat history with an assistant message that has tool calls but no text content."""
|
||||
config = OCIChatConfig()
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "What's the weather?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": '{"location": "Paris"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "Sunny, 25C",
|
||||
"tool_call_id": "call_1",
|
||||
},
|
||||
]
|
||||
|
||||
chat_history = config.adapt_messages_to_cohere_standard(messages)
|
||||
|
||||
# First message is the user message
|
||||
assert chat_history[0].role == "USER"
|
||||
assert chat_history[0].message == "What's the weather?"
|
||||
|
||||
# Second message is the assistant with tool calls and no text
|
||||
assistant_msg = chat_history[1]
|
||||
assert assistant_msg.role == "CHATBOT"
|
||||
assert assistant_msg.message is None or assistant_msg.message == ""
|
||||
assert assistant_msg.toolCalls is not None
|
||||
assert len(assistant_msg.toolCalls) == 1
|
||||
assert assistant_msg.toolCalls[0].name == "get_weather"
|
||||
|
||||
def test_cohere_chat_history_with_tool_calls(self):
|
||||
"""Test chat history transformation with tool calls"""
|
||||
config = OCIChatConfig()
|
||||
|
||||
Reference in New Issue
Block a user