This commit is contained in:
John Lathouwers
2026-02-07 22:58:59 -08:00
committed by GitHub
parent 1fecae0399
commit 8dcd183013
4 changed files with 280 additions and 27 deletions
+36 -1
View File
@@ -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
View File
@@ -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()