From d666595906e3283e61a15946fa241c76cdb73bc6 Mon Sep 17 00:00:00 2001 From: apex-mochen <2756823972@qq.com> Date: Mon, 12 Oct 2026 21:29:02 -0700 Subject: [PATCH 1/6] fix(oci): support Cohere tool-result turns in chat requests The call that follows a tool call failed for OCI Cohere models in two independent ways: 1. Request: OCIChatConfig.transform_request put the last user message in the top-level ``message`` while trailing ``tool``-role results stayed at the end of ``chatHistory``. OCI rejects this with HTTP 400 ("cannot specify message if the last entry in chat history contains tool results"). When the OpenAI-format message array ends with a ``tool`` message, hoist the trailing TOOL history entries into the top-level ``toolResults`` and send ``message=""``, the shape Cohere's contract specifies for a tool-result turn. When a user message follows the tool results the previous shape is preserved. 2. Response: OCI writes citation document references in camelCase (``documentIds``), but CohereCitation only accepted ``document_ids``, so handle_cohere_response raised a validation error whenever the model cited tool output. Accept both names. Closes #46157 Signed-off-by: apex-mochen <2756823972@qq.com> --- litellm/llms/oci/chat/cohere.py | 22 ++ litellm/llms/oci/chat/transformation.py | 27 +- litellm/types/llms/oci.py | 20 +- .../chat/test_oci_cohere_tool_result_turn.py | 315 ++++++++++++++++++ 4 files changed, 379 insertions(+), 5 deletions(-) create mode 100644 tests/unit/llms/oci/chat/test_oci_cohere_tool_result_turn.py diff --git a/litellm/llms/oci/chat/cohere.py b/litellm/llms/oci/chat/cohere.py index 9163ddfa5de..9fb2d07eb33 100644 --- a/litellm/llms/oci/chat/cohere.py +++ b/litellm/llms/oci/chat/cohere.py @@ -176,6 +176,28 @@ def _resolved_oci_parameter_schema(raw_parameters: dict[str, JsonValue]) -> Json return sanitize_oci_schema(resolve_oci_schema_anyof(resolve_oci_schema_refs(raw_parameters))) +def split_trailing_tool_results( + chat_history: list[CohereMessage], +) -> tuple[list[CohereMessage], list[CohereToolResult]]: + """Hoist trailing TOOL-role history entries into a top-level ``toolResults`` list. + + OCI's Cohere API rejects a request whose last ``chatHistory`` entry is a + TOOL result while a non-empty top-level ``message`` is also set ("cannot + specify message if the last entry in chat history contains tool results"). + The accepted shape for a tool-result turn is the trailing results moved to + the request's top-level ``toolResults`` and ``message`` sent empty, matching + Cohere's own contract. + + Returns ``(remaining_history, trailing_results)``; when the last entry is + not a TOOL message, ``trailing_results`` is empty and the history is + returned unchanged. + """ + trailing: Final[list[CohereToolResult]] = [] + while chat_history and isinstance(chat_history[-1], CohereToolMessage): + trailing = chat_history.pop().toolResults + trailing + return chat_history, trailing + + def _cohere_parameter_definition(param_schema: dict[str, JsonValue], is_required: bool) -> CohereParameterDefinition: json_type: Final = _json_str(param_schema.get("type")) or "string" return CohereParameterDefinition( diff --git a/litellm/llms/oci/chat/transformation.py b/litellm/llms/oci/chat/transformation.py index 14ef6225666..738d7e4d0b1 100644 --- a/litellm/llms/oci/chat/transformation.py +++ b/litellm/llms/oci/chat/transformation.py @@ -33,6 +33,7 @@ from litellm.llms.oci.chat.cohere import ( extract_text_content, handle_cohere_response, handle_cohere_stream_chunk, + split_trailing_tool_results, ) from litellm.llms.oci.chat.generic import ( adapt_messages_to_generic_oci_standard, @@ -567,10 +568,32 @@ class OCIChatConfig(BaseConfig): if preamble: preamble_override = preamble + cohere_history: Final = adapt_messages_to_cohere_standard( + [m for m in messages if m.get("role") != "system"] + ) + # OCI's Cohere API rejects a non-empty top-level ``message`` when the + # last ``chatHistory`` entry is a TOOL result ("cannot specify message + # if the last entry in chat history contains tool results"). For a + # tool-result turn (agent loop continuation — the OpenAI-format + # message array ends with a ``tool`` message), hoist the trailing TOOL + # entries into the top-level ``toolResults`` and send an empty + # ``message`` — the shape Cohere itself specifies for that turn. + # When a user message follows the tool results, the turn is a normal + # one and the request keeps the previous shape (tool results stay in + # ``chatHistory``). + is_tool_result_turn: Final = bool(messages) and messages[-1].get("role") == "tool" + if is_tool_result_turn: + remaining_history, trailing_tool_results = split_trailing_tool_results(cohere_history) + chat_message: Final = "" + else: + remaining_history, trailing_tool_results = cohere_history, [] + chat_message = extract_text_content(user_messages[-1]["content"]) + chat_request: Final = CohereChatRequest( apiFormat="COHERE", - message=extract_text_content(user_messages[-1]["content"]), - chatHistory=adapt_messages_to_cohere_standard([m for m in messages if m.get("role") != "system"]), + message=chat_message, + chatHistory=remaining_history or None, + toolResults=trailing_tool_results or None, preambleOverride=preamble_override, **self._get_optional_params(OCIVendors.COHERE, optional_params, model), ) diff --git a/litellm/types/llms/oci.py b/litellm/types/llms/oci.py index bd3a0af1dca..aca02e6849c 100644 --- a/litellm/types/llms/oci.py +++ b/litellm/types/llms/oci.py @@ -3,7 +3,7 @@ from __future__ import annotations from enum import Enum from typing import Literal -from pydantic import SerializeAsAny +from pydantic import AliasChoices, ConfigDict, Field, SerializeAsAny from litellm.types.llms.base import LiteLLMBaseModel @@ -304,6 +304,10 @@ class CohereChatRequest(LiteLLMBaseModel): # Without it, Pydantic v2 would serialize each element using the declared # ``CohereMessage`` schema and silently drop subclass fields. chatHistory: list[SerializeAsAny[CohereMessage]] | None = None + # Tool results for a tool-result turn. OCI's Cohere API refuses a non-empty + # top-level ``message`` when the last ``chatHistory`` entry is a TOOL result, + # and requires the trailing results to be hoisted here instead. + toolResults: list[CohereToolResult] | None = None maxTokens: int | None = None temperature: float | None = None topP: float | None = None @@ -349,12 +353,22 @@ class CohereUsage(LiteLLMBaseModel): class CohereCitation(LiteLLMBaseModel): - """Citation in Cohere response.""" + """Citation in Cohere response. + + OCI writes citation document references in camelCase (``documentIds``), like + the rest of the OCI payload, so accept both the camelCase wire name and the + snake_case field name. + """ + + model_config = ConfigDict(populate_by_name=True, protected_namespaces=()) start: int end: int text: str - document_ids: list[str] + document_ids: list[str] = Field( + validation_alias=AliasChoices("documentIds", "document_ids"), + serialization_alias="documentIds", + ) class CohereSearchQuery(LiteLLMBaseModel): diff --git a/tests/unit/llms/oci/chat/test_oci_cohere_tool_result_turn.py b/tests/unit/llms/oci/chat/test_oci_cohere_tool_result_turn.py new file mode 100644 index 00000000000..46161456cf6 --- /dev/null +++ b/tests/unit/llms/oci/chat/test_oci_cohere_tool_result_turn.py @@ -0,0 +1,315 @@ +"""Regression tests for OCI Cohere tool-result turns (issue #46157). + +Two independent defects break the call *after* a tool call for OCI Cohere +models: + +1. ``OCIChatConfig.transform_request`` put the last user message in the + top-level ``message`` while trailing ``tool``-role results stayed at the end + of ``chatHistory``. OCI rejects this combination with HTTP 400 ("cannot + specify message if the last entry in chat history contains tool results"). + The accepted shape for a tool-result turn is the trailing TOOL entries moved + out of ``chatHistory`` into the top-level ``toolResults`` and ``message`` + sent as ``""``. + +2. OCI writes citation document references in camelCase (``documentIds``), but + ``CohereCitation`` only accepted ``document_ids``, so + ``handle_cohere_response`` raised a validation error whenever the model cited + tool output. +""" + +from litellm.llms.oci.chat.cohere import ( + adapt_messages_to_cohere_standard, + split_trailing_tool_results, +) +from litellm.llms.oci.chat.transformation import OCIChatConfig +from litellm.types.llms.oci import ( + CohereChatBotMessage, + CohereChatResult, + CohereCitation, + CohereMessage, + CohereToolMessage, +) + +TEST_COMPARTMENT_ID = "ocid1.compartment.oc1..xxxxxx" +BASE_OCI_PARAMS = { + "oci_region": "us-chicago-1", + "oci_user": "ocid1.user.oc1..xxxxxxEXAMPLExxxxxx", + "oci_fingerprint": "4f:29:77:cc:b1:3e:55:ab:61:2a:de:47:f1:38:4c:90", + "oci_tenancy": "ocid1.tenancy.oc1..xxxxxxEXAMPLExxxxxx", + "oci_compartment_id": TEST_COMPARTMENT_ID, + "oci_key_file": "/path/to/private_key.pem", +} + +TOOLS = [ + { + "type": "function", + "function": { + "name": "lookup_policy", + "description": "Look up a policy by topic.", + "parameters": { + "type": "object", + "properties": {"topic": {"type": "string"}}, + "required": ["topic"], + }, + }, + } +] + + +def _tool_result_turn_messages(): + """The exact agent-loop continuation from issue #46157.""" + return [ + {"role": "user", "content": "How is support ticket priority decided?"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": { + "name": "lookup_policy", + "arguments": '{"topic": "ticket priority"}', + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call_1", + "content": "P1 for an outage, P2 for degraded service, P3 for questions.", + }, + ] + + +def _transform(messages): + return OCIChatConfig().transform_request( + model="oci/cohere.command-a-03-2025", + messages=messages, # type: ignore[arg-type] + optional_params={**BASE_OCI_PARAMS, "tools": TOOLS, "max_tokens": 300}, + litellm_params={}, + headers={}, + ) + + +class TestTrailingToolResultHoisting: + """Request-side fix: trailing TOOL entries → top-level ``toolResults``.""" + + def test_tool_result_turn_hoists_to_tool_results_and_empties_message(self): + request = _transform(_tool_result_turn_messages()) + chat_request = request["chatRequest"] + + # The top-level message must be empty for a tool-result turn. + assert chat_request["message"] == "" + + # No TOOL entry may remain at the end of chatHistory. + history = chat_request["chatHistory"] + assert history + assert history[-1]["role"] != "TOOL" + # The assistant tool-call turn that produced the results stays in history. + assert any(entry["role"] == "CHATBOT" for entry in history) + + # The trailing result is hoisted into top-level toolResults with the + # originating call's name/parameters resolved from the assistant turn. + assert chat_request["toolResults"] == [ + { + "call": {"name": "lookup_policy", "parameters": {"topic": "ticket priority"}}, + "outputs": [{"output": "P1 for an outage, P2 for degraded service, P3 for questions."}], + } + ] + + def test_parallel_tool_results_hoisted_in_order(self): + messages = [ + {"role": "user", "content": "Check both policies."}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_a", + "type": "function", + "function": {"name": "lookup_policy", "arguments": '{"topic": "a"}'}, + }, + { + "id": "call_b", + "type": "function", + "function": {"name": "lookup_policy", "arguments": '{"topic": "b"}'}, + }, + ], + }, + {"role": "tool", "tool_call_id": "call_a", "content": "result A"}, + {"role": "tool", "tool_call_id": "call_b", "content": "result B"}, + ] + request = _transform(messages) + chat_request = request["chatRequest"] + + assert chat_request["message"] == "" + assert chat_request["toolResults"] == [ + { + "call": {"name": "lookup_policy", "parameters": {"topic": "a"}}, + "outputs": [{"output": "result A"}], + }, + { + "call": {"name": "lookup_policy", "parameters": {"topic": "b"}}, + "outputs": [{"output": "result B"}], + }, + ] + assert not any(entry["role"] == "TOOL" for entry in chat_request["chatHistory"]) + + def test_plain_turn_is_unchanged(self): + """A normal user→assistant turn keeps message non-empty and no toolResults.""" + messages = [ + {"role": "user", "content": "How is support ticket priority decided?"}, + {"role": "assistant", "content": "P1 for an outage."}, + ] + chat_request = _transform(messages)["chatRequest"] + + # The last user message becomes the top-level ``message``; the remaining + # history (the assistant reply here) stays in ``chatHistory``. + assert chat_request["message"] == "How is support ticket priority decided?" + assert "toolResults" not in chat_request + assert chat_request["chatHistory"] == [{"role": "CHATBOT", "message": "P1 for an outage."}] + + def test_tool_result_in_middle_is_not_hoisted_when_user_message_follows(self): + """A trailing user message ends the turn; the TOOL entry stays in history.""" + messages = [ + {"role": "user", "content": "How is support ticket priority decided?"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": {"name": "lookup_policy", "arguments": '{"topic": "ticket priority"}'}, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call_1", + "content": "P1 for an outage, P2 for degraded service, P3 for questions.", + }, + {"role": "user", "content": "Thanks, that helps."}, + ] + chat_request = _transform(messages)["chatRequest"] + + # The turn ends on a user message, so the request keeps the previous + # shape: the new question is sent as ``message`` and the tool result + # stays in ``chatHistory``. + assert chat_request["message"] == "Thanks, that helps." + assert "toolResults" not in chat_request + assert chat_request["chatHistory"][-1]["role"] == "TOOL" + + +class TestSplitTrailingToolResults: + """Unit coverage for the split helper itself (normal/None/boundary).""" + + def test_no_tool_message_returns_unchanged(self): + history = [CohereChatBotMessage(role="CHATBOT", message="hi")] + remaining, trailing = split_trailing_tool_results(history) + assert trailing == [] + assert remaining == history + + def test_empty_history_returns_empty(self): + remaining, trailing = split_trailing_tool_results([]) + assert remaining == [] + assert trailing == [] + + def test_all_tool_messages_are_hoisted(self): + history: list[CohereMessage] = [ + CohereChatBotMessage(role="CHATBOT", message=""), + CohereToolMessage( + role="TOOL", + toolResults=[ + { + "call": {"name": "lookup_policy", "parameters": {"topic": "a"}}, + "outputs": [{"output": "result A"}], + } + ], + ), + CohereToolMessage( + role="TOOL", + toolResults=[ + { + "call": {"name": "lookup_policy", "parameters": {"topic": "b"}}, + "outputs": [{"output": "result B"}], + } + ], + ), + ] + remaining, trailing = split_trailing_tool_results(history) + assert [m.role for m in remaining] == ["CHATBOT"] + assert [r.call.name for r in trailing] == ["lookup_policy", "lookup_policy"] + assert [r.call.parameters for r in trailing] == [ + {"topic": "a"}, + {"topic": "b"}, + ] + + def test_adapter_output_is_accepted_by_split(self): + """End-to-end: adapter output feeds straight into the split helper.""" + from litellm.types.llms.openai import AllMessageValues + + messages: list[AllMessageValues] = _tool_result_turn_messages() # type: ignore[assignment] + history = adapt_messages_to_cohere_standard(messages) + remaining, trailing = split_trailing_tool_results(history) + assert trailing + assert not any(isinstance(m, CohereToolMessage) for m in remaining) + + +class TestCohereCitationAlias: + """Response-side fix: accept OCI's camelCase ``documentIds``.""" + + def test_citation_accepts_document_ids(self): + citation = CohereCitation.model_validate( + { + "start": 0, + "end": 10, + "text": "cited", + "documentIds": ["lookup_policy:0:1:0"], + } + ) + assert citation.document_ids == ["lookup_policy:0:1:0"] + + def test_citation_still_accepts_snake_case(self): + citation = CohereCitation.model_validate( + { + "start": 0, + "end": 10, + "text": "cited", + "document_ids": ["doc-1"], + } + ) + assert citation.document_ids == ["doc-1"] + + def test_citation_serializes_back_to_camel_case(self): + citation = CohereCitation( + start=44, + end=70, + text="the cited snippet", + document_ids=["lookup_policy:0:1:0"], + ) + assert citation.model_dump(exclude_none=True, by_alias=True)["documentIds"] == ["lookup_policy:0:1:0"] + + def test_full_chat_result_with_oci_citations_validates(self): + """The exact failure from issue #46157: model cites tool output.""" + payload = { + "modelId": "cohere.command-a-03-2025", + "modelVersion": "1.0", + "chatResponse": { + "text": "According to the policy, outages map to P1.", + "apiFormat": "COHERE", + "finishReason": "COMPLETE", + "citations": [ + { + "start": 44, + "end": 70, + "text": "P1 for an outage, P2 for degraded service, P3 for questions.", + "documentIds": ["lookup_policy:0:1:0"], + } + ], + }, + } + result = CohereChatResult.model_validate(payload) + assert result.chatResponse.citations is not None + assert result.chatResponse.citations[0].document_ids == ["lookup_policy:0:1:0"] From 4446b053aca85aab77acaf5e3c61fe9bfa756ad9 Mon Sep 17 00:00:00 2001 From: dingdangmao <2756823972@qq.com> Date: Tue, 13 Oct 2026 14:36:49 +0800 Subject: [PATCH 2/6] fix(oci): annotate type-discipline suppressions for the Cohere tool-result turn The hoisted toolResults wire field, the split helper signature and the per-branch assignment are intentionally mutable / rebound; document that with reasoned suppressions so the type-discipline delta gate stays green. Signed-off-by: apex-mochen <2756823972@qq.com> --- litellm/llms/oci/chat/cohere.py | 6 +++--- litellm/llms/oci/chat/transformation.py | 2 +- litellm/types/llms/oci.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/litellm/llms/oci/chat/cohere.py b/litellm/llms/oci/chat/cohere.py index 9fb2d07eb33..bac81c16508 100644 --- a/litellm/llms/oci/chat/cohere.py +++ b/litellm/llms/oci/chat/cohere.py @@ -177,8 +177,8 @@ def _resolved_oci_parameter_schema(raw_parameters: dict[str, JsonValue]) -> Json def split_trailing_tool_results( - chat_history: list[CohereMessage], -) -> tuple[list[CohereMessage], list[CohereToolResult]]: + chat_history: list[CohereMessage], # mutable-ok: the caller's list is popped in place, so it must stay mutable +) -> tuple[list[CohereMessage], list[CohereToolResult]]: # mutable-ok: lists built by pop/prepend """Hoist trailing TOOL-role history entries into a top-level ``toolResults`` list. OCI's Cohere API rejects a request whose last ``chatHistory`` entry is a @@ -192,7 +192,7 @@ def split_trailing_tool_results( not a TOOL message, ``trailing_results`` is empty and the history is returned unchanged. """ - trailing: Final[list[CohereToolResult]] = [] + trailing: Final[list[CohereToolResult]] = [] # mutable-ok: prepend-reassigned in the pop loop below while chat_history and isinstance(chat_history[-1], CohereToolMessage): trailing = chat_history.pop().toolResults + trailing return chat_history, trailing diff --git a/litellm/llms/oci/chat/transformation.py b/litellm/llms/oci/chat/transformation.py index 738d7e4d0b1..54c48616096 100644 --- a/litellm/llms/oci/chat/transformation.py +++ b/litellm/llms/oci/chat/transformation.py @@ -586,7 +586,7 @@ class OCIChatConfig(BaseConfig): remaining_history, trailing_tool_results = split_trailing_tool_results(cohere_history) chat_message: Final = "" else: - remaining_history, trailing_tool_results = cohere_history, [] + remaining_history, trailing_tool_results = cohere_history, [] # rebind-ok: bound once per branch chat_message = extract_text_content(user_messages[-1]["content"]) chat_request: Final = CohereChatRequest( diff --git a/litellm/types/llms/oci.py b/litellm/types/llms/oci.py index aca02e6849c..f06b283141b 100644 --- a/litellm/types/llms/oci.py +++ b/litellm/types/llms/oci.py @@ -307,7 +307,7 @@ class CohereChatRequest(LiteLLMBaseModel): # Tool results for a tool-result turn. OCI's Cohere API refuses a non-empty # top-level ``message`` when the last ``chatHistory`` entry is a TOOL result, # and requires the trailing results to be hoisted here instead. - toolResults: list[CohereToolResult] | None = None + toolResults: list[CohereToolResult] | None = None # mutable-ok: mirror of OCI's toolResults list maxTokens: int | None = None temperature: float | None = None topP: float | None = None @@ -365,7 +365,7 @@ class CohereCitation(LiteLLMBaseModel): start: int end: int text: str - document_ids: list[str] = Field( + document_ids: list[str] = Field( # mutable-ok: wire mirror of OCI's documentIds list; owned by the citation model validation_alias=AliasChoices("documentIds", "document_ids"), serialization_alias="documentIds", ) From b76844fbe4c76e26a17d5bd03898d51184c78db5 Mon Sep 17 00:00:00 2001 From: dingdangmao <2756823972@qq.com> Date: Tue, 13 Oct 2026 17:23:04 +0800 Subject: [PATCH 3/6] fix(oci): narrow split_trailing_tool_results types for the basedpyright delta gate The previous hoist loop declared `trailing` Final but rebound it inside the while loop and called `.toolResults` on `chat_history.pop()` without narrowing, so basedpyright reported the return type as partially unknown (+7 on the delta gate). Keep the Final annotation (the type-discipline gate requires declared constants) and instead prepend each hoisted result in place via `trailing[:0] = last.toolResults`, after narrowing the last entry through a named local. No behaviour change: the slice-prepend order matches the previous `toolResults + trailing` accumulation. Signed-off-by: apex-mochen <2756823972@qq.com> --- litellm/llms/oci/chat/cohere.py | 86 ++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 22 deletions(-) diff --git a/litellm/llms/oci/chat/cohere.py b/litellm/llms/oci/chat/cohere.py index bac81c16508..e4f1e5bf1d3 100644 --- a/litellm/llms/oci/chat/cohere.py +++ b/litellm/llms/oci/chat/cohere.py @@ -130,11 +130,17 @@ def adapt_messages_to_cohere_standard( } last_user_index: Final = next( - (i for i in range(len(messages) - 1, -1, -1) if messages[i].get("role") == "user"), + ( + i + for i in range(len(messages) - 1, -1, -1) + if messages[i].get("role") == "user" + ), None, ) history_source: Final = ( - messages if last_user_index is None else [m for i, m in enumerate(messages) if i != last_user_index] + messages + if last_user_index is None + else [m for i, m in enumerate(messages) if i != last_user_index] ) chat_history: Final[list[CohereMessage]] = [] @@ -151,10 +157,14 @@ def adapt_messages_to_cohere_standard( if role == "user": chat_history.append(CohereMessage(role="USER", message=content)) elif role == "assistant": - chat_history.append(CohereMessage(role="CHATBOT", message=content, toolCalls=tool_calls)) + chat_history.append( + CohereMessage(role="CHATBOT", message=content, toolCalls=tool_calls) + ) elif role == "tool": tool_call_id = str(msg.get("tool_call_id", "") or "") - cohere_call = tool_call_lookup.get(tool_call_id, CohereToolCall(name="", parameters={})) + cohere_call = tool_call_lookup.get( + tool_call_id, CohereToolCall(name="", parameters={}) + ) tool_result = CohereToolResult( call=cohere_call, outputs=[{"output": content}], @@ -173,12 +183,14 @@ def adapt_messages_to_cohere_standard( def _resolved_oci_parameter_schema(raw_parameters: dict[str, JsonValue]) -> JsonValue: - return sanitize_oci_schema(resolve_oci_schema_anyof(resolve_oci_schema_refs(raw_parameters))) + return sanitize_oci_schema( + resolve_oci_schema_anyof(resolve_oci_schema_refs(raw_parameters)) + ) def split_trailing_tool_results( - chat_history: list[CohereMessage], # mutable-ok: the caller's list is popped in place, so it must stay mutable -) -> tuple[list[CohereMessage], list[CohereToolResult]]: # mutable-ok: lists built by pop/prepend + chat_history: list[CohereMessage], # mutable-ok: popped in place by caller +) -> tuple[list[CohereMessage], list[CohereToolResult]]: # mutable-ok: hoisted """Hoist trailing TOOL-role history entries into a top-level ``toolResults`` list. OCI's Cohere API rejects a request whose last ``chatHistory`` entry is a @@ -192,27 +204,41 @@ def split_trailing_tool_results( not a TOOL message, ``trailing_results`` is empty and the history is returned unchanged. """ - trailing: Final[list[CohereToolResult]] = [] # mutable-ok: prepend-reassigned in the pop loop below - while chat_history and isinstance(chat_history[-1], CohereToolMessage): - trailing = chat_history.pop().toolResults + trailing + trailing: Final[list[CohereToolResult]] = [] # mutable-ok: extended in place + while chat_history: + last = chat_history[-1] + if not isinstance(last, CohereToolMessage): + break + chat_history.pop() + trailing[:0] = last.toolResults return chat_history, trailing -def _cohere_parameter_definition(param_schema: dict[str, JsonValue], is_required: bool) -> CohereParameterDefinition: +def _cohere_parameter_definition( + param_schema: dict[str, JsonValue], is_required: bool +) -> CohereParameterDefinition: json_type: Final = _json_str(param_schema.get("type")) or "string" return CohereParameterDefinition( - description=enrich_cohere_param_description(_json_str(param_schema.get("description")), param_schema), + description=enrich_cohere_param_description( + _json_str(param_schema.get("description")), param_schema + ), type=OCI_JSON_TO_PYTHON_TYPES.get(json_type, json_type), isRequired=is_required, ) -def _cohere_parameter_definitions(resolved_schema: JsonValue) -> dict[str, CohereParameterDefinition]: +def _cohere_parameter_definitions( + resolved_schema: JsonValue, +) -> dict[str, CohereParameterDefinition]: schema_fields: Final = _json_dict(resolved_schema) required: Final = _json_list(schema_fields.get("required")) return { - param_name: _cohere_parameter_definition(_json_dict(param_schema), param_name in required) - for param_name, param_schema in _json_dict(schema_fields.get("properties")).items() + param_name: _cohere_parameter_definition( + _json_dict(param_schema), param_name in required + ) + for param_name, param_schema in _json_dict( + schema_fields.get("properties") + ).items() } @@ -259,13 +285,17 @@ def handle_cohere_response( model_response.created = int(datetime.datetime.now().timestamp()) response_text: Final = cohere_response.chatResponse.text - finish_reason: Final = normalize_oci_finish_reason(cohere_response.chatResponse.finishReason) + finish_reason: Final = normalize_oci_finish_reason( + cohere_response.chatResponse.finishReason + ) tool_calls: list[dict[str, object]] | None = None if cohere_response.chatResponse.toolCalls: tool_calls = [ { - "id": synthesize_oci_tool_call_id(i, tc.name, json.dumps(tc.parameters, sort_keys=True)), + "id": synthesize_oci_tool_call_id( + i, tc.name, json.dumps(tc.parameters, sort_keys=True) + ), "type": "function", "function": { "name": tc.name, @@ -302,7 +332,9 @@ def handle_cohere_response( total_tokens=usage_info.totalTokens, ) else: - model_response.usage = Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0) + model_response.usage = Usage( + prompt_tokens=0, completion_tokens=0, total_tokens=0 + ) return model_response @@ -334,10 +366,18 @@ def handle_cohere_stream_chunk( if typed_chunk.index is None: typed_chunk.index = 0 - restates_text: Final = typed_chunk.chatHistory is not None or typed_chunk.toolCalls is not None + restates_text: Final = ( + typed_chunk.chatHistory is not None or typed_chunk.toolCalls is not None + ) restates_tool_calls: Final = typed_chunk.chatHistory is not None - text: Final[str | None] = None if (restates_text and prior_text_emitted) else typed_chunk.text - cohere_tool_calls: Final = None if (restates_tool_calls and prior_tool_calls_emitted) else typed_chunk.toolCalls + text: Final[str | None] = ( + None if (restates_text and prior_text_emitted) else typed_chunk.text + ) + cohere_tool_calls: Final = ( + None + if (restates_tool_calls and prior_tool_calls_emitted) + else typed_chunk.toolCalls + ) tool_calls: list[dict[str, object]] | None = None if cohere_tool_calls: @@ -347,7 +387,9 @@ def handle_cohere_stream_chunk( # deterministically from the call's content/position. A random # uuid4 per chunk would cause downstream stream-mergers to # treat each chunk as a distinct tool call. - "id": synthesize_oci_tool_call_id(i, tc.name, json.dumps(tc.parameters, sort_keys=True)), + "id": synthesize_oci_tool_call_id( + i, tc.name, json.dumps(tc.parameters, sort_keys=True) + ), "type": "function", "function": { "name": tc.name, From 55c08166d5ff12f3e1fa7daa180feb22534b20fd Mon Sep 17 00:00:00 2001 From: dingdangmao <2756823972@qq.com> Date: Tue, 13 Oct 2026 17:49:09 +0800 Subject: [PATCH 4/6] fix(oci): keep an empty chatHistory field for Cohere chat requests `chatHistory=remaining_history or None` collapsed an empty history to None, so the serialized Cohere request dropped the `chatHistory` key entirely and two pre-existing unit tests that assert the field is always present failed in the llms-providers CI job. Restore upstream main's shape: pass the adapted history through unconditionally, exactly like `adapt_messages_to_cohere_standard` did before this change. The tool-result hoist still passes `[]` through the same field and only sets `toolResults` when trailing results exist. Signed-off-by: apex-mochen <2756823972@qq.com> --- litellm/llms/oci/chat/transformation.py | 89 ++++++++++++++++++------- 1 file changed, 65 insertions(+), 24 deletions(-) diff --git a/litellm/llms/oci/chat/transformation.py b/litellm/llms/oci/chat/transformation.py index 54c48616096..887519c7305 100644 --- a/litellm/llms/oci/chat/transformation.py +++ b/litellm/llms/oci/chat/transformation.py @@ -147,7 +147,9 @@ def _normalize_tool_choice(selected_params: dict) -> None: "required": {"type": "REQUIRED"}, "any": {"type": "REQUIRED"}, } - selected_params["toolChoice"] = tc_map.get(tc.lower(), {"type": "FUNCTION", "name": tc}) + selected_params["toolChoice"] = tc_map.get( + tc.lower(), {"type": "FUNCTION", "name": tc} + ) return if isinstance(tc, dict): raw_type: Final = tc.get("type") @@ -179,7 +181,9 @@ def _normalize_tool_choice(selected_params: dict) -> None: return raise OCIError( status_code=400, - message=(f"Invalid tool_choice for OCI: expected str or dict, got {type(tc).__name__}"), + message=( + f"Invalid tool_choice for OCI: expected str or dict, got {type(tc).__name__}" + ), ) @@ -213,7 +217,9 @@ def _normalize_response_format(selected_params: dict, vendor: OCIVendors) -> Non # OCI's ResponseJsonSchema accepts only name/description/schema/isStrict. # OpenAI sends `strict` instead of `isStrict`; forwarding it (or any # other extra key) makes OCI reject the whole request with HTTP 400. - oci_schema: Final[dict[str, object]] = {"name": json_schema.get("name") or "response"} + oci_schema: Final[dict[str, object]] = { + "name": json_schema.get("name") or "response" + } if json_schema.get("description") is not None: oci_schema["description"] = json_schema["description"] if json_schema.get("schema") is not None: @@ -227,7 +233,9 @@ def _normalize_response_format(selected_params: dict, vendor: OCIVendors) -> Non return fmt: Final = rf_type.upper() - selected_params["responseFormat"] = {"type": "JSON_OBJECT" if fmt == "JSON" else fmt} + selected_params["responseFormat"] = { + "type": "JSON_OBJECT" if fmt == "JSON" else fmt + } def get_vendor_from_model(model: str) -> OCIVendors: @@ -291,7 +299,8 @@ class OCIChatConfig(BaseConfig): # ``map_openai_params`` either drops them (under drop_params) or raises # a clear error, rather than silently passing them through. self.openai_to_oci_cohere_param_map = { - k: ("stopSequences" if k == "stop" else v) for k, v in self.openai_to_oci_generic_param_map.items() + k: ("stopSequences" if k == "stop" else v) + for k, v in self.openai_to_oci_generic_param_map.items() } self.openai_to_oci_cohere_param_map["tool_choice"] = False self.openai_to_oci_cohere_param_map["n"] = False @@ -335,7 +344,9 @@ class OCIChatConfig(BaseConfig): adapted_params: Final = {} vendor: Final = get_vendor_from_model(model) param_map: Final = ( - self.openai_to_oci_cohere_param_map if vendor == OCIVendors.COHERE else self.openai_to_oci_generic_param_map + self.openai_to_oci_cohere_param_map + if vendor == OCIVendors.COHERE + else self.openai_to_oci_generic_param_map ) for key, value in {**non_default_params, **optional_params}.items(): @@ -447,9 +458,13 @@ class OCIChatConfig(BaseConfig): base: Final = get_oci_base_url(optional_params, api_base or litellm.api_base) return f"{base}/{OCI_API_VERSION}/actions/chat" - def _get_optional_params(self, vendor: OCIVendors, optional_params: dict, model: str = "") -> dict: + def _get_optional_params( + self, vendor: OCIVendors, optional_params: dict, model: str = "" + ) -> dict: param_map: Final = ( - self.openai_to_oci_cohere_param_map if vendor == OCIVendors.COHERE else self.openai_to_oci_generic_param_map + self.openai_to_oci_cohere_param_map + if vendor == OCIVendors.COHERE + else self.openai_to_oci_generic_param_map ) selected_params: Final[dict] = {} @@ -459,7 +474,9 @@ class OCIChatConfig(BaseConfig): # endpoint uses "maxTokens" regardless, so the override is GENERIC-only. max_tokens_key: Final = ( "maxCompletionTokens" - if vendor != OCIVendors.COHERE and model and _model_uses_max_completion_tokens(model) + if vendor != OCIVendors.COHERE + and model + and _model_uses_max_completion_tokens(model) else "maxTokens" ) @@ -501,7 +518,9 @@ class OCIChatConfig(BaseConfig): if "tools" in selected_params: if vendor == OCIVendors.COHERE: - selected_params["tools"] = adapt_tool_definitions_to_cohere_standard(selected_params["tools"]) + selected_params["tools"] = adapt_tool_definitions_to_cohere_standard( + selected_params["tools"] + ) else: selected_params["tools"] = adapt_tool_definition_to_oci_standard( selected_params["tools"], @@ -564,7 +583,9 @@ class OCIChatConfig(BaseConfig): system_messages: Final = [m for m in messages if m.get("role") == "system"] preamble_override = None if system_messages: - preamble: Final = "\n".join(extract_text_content(m["content"]) for m in system_messages) + preamble: Final = "\n".join( + extract_text_content(m["content"]) for m in system_messages + ) if preamble: preamble_override = preamble @@ -581,18 +602,24 @@ class OCIChatConfig(BaseConfig): # When a user message follows the tool results, the turn is a normal # one and the request keeps the previous shape (tool results stay in # ``chatHistory``). - is_tool_result_turn: Final = bool(messages) and messages[-1].get("role") == "tool" - if is_tool_result_turn: - remaining_history, trailing_tool_results = split_trailing_tool_results(cohere_history) - chat_message: Final = "" - else: - remaining_history, trailing_tool_results = cohere_history, [] # rebind-ok: bound once per branch - chat_message = extract_text_content(user_messages[-1]["content"]) + is_tool_result_turn: Final = ( + bool(messages) and messages[-1].get("role") == "tool" + ) + remaining_history, trailing_tool_results = ( + split_trailing_tool_results(cohere_history) + if is_tool_result_turn + else (cohere_history, []) + ) + chat_message: Final = ( + "" + if is_tool_result_turn + else extract_text_content(user_messages[-1]["content"]) + ) chat_request: Final = CohereChatRequest( apiFormat="COHERE", message=chat_message, - chatHistory=remaining_history or None, + chatHistory=remaining_history, toolResults=trailing_tool_results or None, preambleOverride=preamble_override, **self._get_optional_params(OCIVendors.COHERE, optional_params, model), @@ -645,9 +672,13 @@ class OCIChatConfig(BaseConfig): vendor: Final = get_vendor_from_model(model) if vendor == OCIVendors.COHERE: - model_response = handle_cohere_response(response_json, model, model_response, raw_response) + model_response = handle_cohere_response( + response_json, model, model_response, raw_response + ) else: - model_response = handle_generic_response(response_json, model, model_response, raw_response) + model_response = handle_generic_response( + response_json, model, model_response, raw_response + ) model_response.hidden_params["additional_headers"] = raw_response.headers return model_response @@ -676,7 +707,11 @@ class OCIChatConfig(BaseConfig): response: Final = client.post( api_base, headers=headers, - data=(signed_json_body if signed_json_body is not None else json.dumps(data)), + data=( + signed_json_body + if signed_json_body is not None + else json.dumps(data) + ), stream=True, logging_obj=logging_obj, timeout=STREAMING_TIMEOUT, @@ -718,7 +753,11 @@ class OCIChatConfig(BaseConfig): response: Final = await client.post( api_base, headers=headers, - data=(signed_json_body if signed_json_body is not None else json.dumps(data)), + data=( + signed_json_body + if signed_json_body is not None + else json.dumps(data) + ), stream=True, logging_obj=logging_obj, timeout=STREAMING_TIMEOUT, @@ -736,7 +775,9 @@ class OCIChatConfig(BaseConfig): logging_obj=logging_obj, ) - def get_error_class(self, error_message: str, status_code: int, headers: dict | httpx.Headers) -> BaseLLMException: + def get_error_class( + self, error_message: str, status_code: int, headers: dict | httpx.Headers + ) -> BaseLLMException: return OCIError(status_code=status_code, message=error_message) From ba850cb6431e5cec65cee41a37fd4dbe04c6ec2f Mon Sep 17 00:00:00 2001 From: dingdangmao <2756823972@qq.com> Date: Tue, 13 Oct 2026 18:39:04 +0800 Subject: [PATCH 5/6] fix(oci): ruff-format cohere and transformation under repo config CI's Check ruff format step flagged both files: formatting them with the repository's ruff configuration (pyproject.toml) reformats a couple of hunks in each. No behavioral change; locally the 12 tool-result-turn tests and both pre-existing chatHistory tests still pass, and the type-discipline delta gate reports no LIT rule growth vs merge-base. Signed-off-by: apex-mochen <2756823972@qq.com> --- litellm/llms/oci/chat/cohere.py | 68 +++++---------------- litellm/llms/oci/chat/transformation.py | 81 ++++++------------------- 2 files changed, 35 insertions(+), 114 deletions(-) diff --git a/litellm/llms/oci/chat/cohere.py b/litellm/llms/oci/chat/cohere.py index e4f1e5bf1d3..80a6a3ed399 100644 --- a/litellm/llms/oci/chat/cohere.py +++ b/litellm/llms/oci/chat/cohere.py @@ -130,17 +130,11 @@ def adapt_messages_to_cohere_standard( } last_user_index: Final = next( - ( - i - for i in range(len(messages) - 1, -1, -1) - if messages[i].get("role") == "user" - ), + (i for i in range(len(messages) - 1, -1, -1) if messages[i].get("role") == "user"), None, ) history_source: Final = ( - messages - if last_user_index is None - else [m for i, m in enumerate(messages) if i != last_user_index] + messages if last_user_index is None else [m for i, m in enumerate(messages) if i != last_user_index] ) chat_history: Final[list[CohereMessage]] = [] @@ -157,14 +151,10 @@ def adapt_messages_to_cohere_standard( if role == "user": chat_history.append(CohereMessage(role="USER", message=content)) elif role == "assistant": - chat_history.append( - CohereMessage(role="CHATBOT", message=content, toolCalls=tool_calls) - ) + chat_history.append(CohereMessage(role="CHATBOT", message=content, toolCalls=tool_calls)) elif role == "tool": tool_call_id = str(msg.get("tool_call_id", "") or "") - cohere_call = tool_call_lookup.get( - tool_call_id, CohereToolCall(name="", parameters={}) - ) + cohere_call = tool_call_lookup.get(tool_call_id, CohereToolCall(name="", parameters={})) tool_result = CohereToolResult( call=cohere_call, outputs=[{"output": content}], @@ -183,9 +173,7 @@ def adapt_messages_to_cohere_standard( def _resolved_oci_parameter_schema(raw_parameters: dict[str, JsonValue]) -> JsonValue: - return sanitize_oci_schema( - resolve_oci_schema_anyof(resolve_oci_schema_refs(raw_parameters)) - ) + return sanitize_oci_schema(resolve_oci_schema_anyof(resolve_oci_schema_refs(raw_parameters))) def split_trailing_tool_results( @@ -214,14 +202,10 @@ def split_trailing_tool_results( return chat_history, trailing -def _cohere_parameter_definition( - param_schema: dict[str, JsonValue], is_required: bool -) -> CohereParameterDefinition: +def _cohere_parameter_definition(param_schema: dict[str, JsonValue], is_required: bool) -> CohereParameterDefinition: json_type: Final = _json_str(param_schema.get("type")) or "string" return CohereParameterDefinition( - description=enrich_cohere_param_description( - _json_str(param_schema.get("description")), param_schema - ), + description=enrich_cohere_param_description(_json_str(param_schema.get("description")), param_schema), type=OCI_JSON_TO_PYTHON_TYPES.get(json_type, json_type), isRequired=is_required, ) @@ -233,12 +217,8 @@ def _cohere_parameter_definitions( schema_fields: Final = _json_dict(resolved_schema) required: Final = _json_list(schema_fields.get("required")) return { - param_name: _cohere_parameter_definition( - _json_dict(param_schema), param_name in required - ) - for param_name, param_schema in _json_dict( - schema_fields.get("properties") - ).items() + param_name: _cohere_parameter_definition(_json_dict(param_schema), param_name in required) + for param_name, param_schema in _json_dict(schema_fields.get("properties")).items() } @@ -285,17 +265,13 @@ def handle_cohere_response( model_response.created = int(datetime.datetime.now().timestamp()) response_text: Final = cohere_response.chatResponse.text - finish_reason: Final = normalize_oci_finish_reason( - cohere_response.chatResponse.finishReason - ) + finish_reason: Final = normalize_oci_finish_reason(cohere_response.chatResponse.finishReason) tool_calls: list[dict[str, object]] | None = None if cohere_response.chatResponse.toolCalls: tool_calls = [ { - "id": synthesize_oci_tool_call_id( - i, tc.name, json.dumps(tc.parameters, sort_keys=True) - ), + "id": synthesize_oci_tool_call_id(i, tc.name, json.dumps(tc.parameters, sort_keys=True)), "type": "function", "function": { "name": tc.name, @@ -332,9 +308,7 @@ def handle_cohere_response( total_tokens=usage_info.totalTokens, ) else: - model_response.usage = Usage( - prompt_tokens=0, completion_tokens=0, total_tokens=0 - ) + model_response.usage = Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0) return model_response @@ -366,18 +340,10 @@ def handle_cohere_stream_chunk( if typed_chunk.index is None: typed_chunk.index = 0 - restates_text: Final = ( - typed_chunk.chatHistory is not None or typed_chunk.toolCalls is not None - ) + restates_text: Final = typed_chunk.chatHistory is not None or typed_chunk.toolCalls is not None restates_tool_calls: Final = typed_chunk.chatHistory is not None - text: Final[str | None] = ( - None if (restates_text and prior_text_emitted) else typed_chunk.text - ) - cohere_tool_calls: Final = ( - None - if (restates_tool_calls and prior_tool_calls_emitted) - else typed_chunk.toolCalls - ) + text: Final[str | None] = None if (restates_text and prior_text_emitted) else typed_chunk.text + cohere_tool_calls: Final = None if (restates_tool_calls and prior_tool_calls_emitted) else typed_chunk.toolCalls tool_calls: list[dict[str, object]] | None = None if cohere_tool_calls: @@ -387,9 +353,7 @@ def handle_cohere_stream_chunk( # deterministically from the call's content/position. A random # uuid4 per chunk would cause downstream stream-mergers to # treat each chunk as a distinct tool call. - "id": synthesize_oci_tool_call_id( - i, tc.name, json.dumps(tc.parameters, sort_keys=True) - ), + "id": synthesize_oci_tool_call_id(i, tc.name, json.dumps(tc.parameters, sort_keys=True)), "type": "function", "function": { "name": tc.name, diff --git a/litellm/llms/oci/chat/transformation.py b/litellm/llms/oci/chat/transformation.py index 887519c7305..4a2aa9887e0 100644 --- a/litellm/llms/oci/chat/transformation.py +++ b/litellm/llms/oci/chat/transformation.py @@ -147,9 +147,7 @@ def _normalize_tool_choice(selected_params: dict) -> None: "required": {"type": "REQUIRED"}, "any": {"type": "REQUIRED"}, } - selected_params["toolChoice"] = tc_map.get( - tc.lower(), {"type": "FUNCTION", "name": tc} - ) + selected_params["toolChoice"] = tc_map.get(tc.lower(), {"type": "FUNCTION", "name": tc}) return if isinstance(tc, dict): raw_type: Final = tc.get("type") @@ -181,9 +179,7 @@ def _normalize_tool_choice(selected_params: dict) -> None: return raise OCIError( status_code=400, - message=( - f"Invalid tool_choice for OCI: expected str or dict, got {type(tc).__name__}" - ), + message=(f"Invalid tool_choice for OCI: expected str or dict, got {type(tc).__name__}"), ) @@ -217,9 +213,7 @@ def _normalize_response_format(selected_params: dict, vendor: OCIVendors) -> Non # OCI's ResponseJsonSchema accepts only name/description/schema/isStrict. # OpenAI sends `strict` instead of `isStrict`; forwarding it (or any # other extra key) makes OCI reject the whole request with HTTP 400. - oci_schema: Final[dict[str, object]] = { - "name": json_schema.get("name") or "response" - } + oci_schema: Final[dict[str, object]] = {"name": json_schema.get("name") or "response"} if json_schema.get("description") is not None: oci_schema["description"] = json_schema["description"] if json_schema.get("schema") is not None: @@ -233,9 +227,7 @@ def _normalize_response_format(selected_params: dict, vendor: OCIVendors) -> Non return fmt: Final = rf_type.upper() - selected_params["responseFormat"] = { - "type": "JSON_OBJECT" if fmt == "JSON" else fmt - } + selected_params["responseFormat"] = {"type": "JSON_OBJECT" if fmt == "JSON" else fmt} def get_vendor_from_model(model: str) -> OCIVendors: @@ -299,8 +291,7 @@ class OCIChatConfig(BaseConfig): # ``map_openai_params`` either drops them (under drop_params) or raises # a clear error, rather than silently passing them through. self.openai_to_oci_cohere_param_map = { - k: ("stopSequences" if k == "stop" else v) - for k, v in self.openai_to_oci_generic_param_map.items() + k: ("stopSequences" if k == "stop" else v) for k, v in self.openai_to_oci_generic_param_map.items() } self.openai_to_oci_cohere_param_map["tool_choice"] = False self.openai_to_oci_cohere_param_map["n"] = False @@ -344,9 +335,7 @@ class OCIChatConfig(BaseConfig): adapted_params: Final = {} vendor: Final = get_vendor_from_model(model) param_map: Final = ( - self.openai_to_oci_cohere_param_map - if vendor == OCIVendors.COHERE - else self.openai_to_oci_generic_param_map + self.openai_to_oci_cohere_param_map if vendor == OCIVendors.COHERE else self.openai_to_oci_generic_param_map ) for key, value in {**non_default_params, **optional_params}.items(): @@ -458,13 +447,9 @@ class OCIChatConfig(BaseConfig): base: Final = get_oci_base_url(optional_params, api_base or litellm.api_base) return f"{base}/{OCI_API_VERSION}/actions/chat" - def _get_optional_params( - self, vendor: OCIVendors, optional_params: dict, model: str = "" - ) -> dict: + def _get_optional_params(self, vendor: OCIVendors, optional_params: dict, model: str = "") -> dict: param_map: Final = ( - self.openai_to_oci_cohere_param_map - if vendor == OCIVendors.COHERE - else self.openai_to_oci_generic_param_map + self.openai_to_oci_cohere_param_map if vendor == OCIVendors.COHERE else self.openai_to_oci_generic_param_map ) selected_params: Final[dict] = {} @@ -474,9 +459,7 @@ class OCIChatConfig(BaseConfig): # endpoint uses "maxTokens" regardless, so the override is GENERIC-only. max_tokens_key: Final = ( "maxCompletionTokens" - if vendor != OCIVendors.COHERE - and model - and _model_uses_max_completion_tokens(model) + if vendor != OCIVendors.COHERE and model and _model_uses_max_completion_tokens(model) else "maxTokens" ) @@ -518,9 +501,7 @@ class OCIChatConfig(BaseConfig): if "tools" in selected_params: if vendor == OCIVendors.COHERE: - selected_params["tools"] = adapt_tool_definitions_to_cohere_standard( - selected_params["tools"] - ) + selected_params["tools"] = adapt_tool_definitions_to_cohere_standard(selected_params["tools"]) else: selected_params["tools"] = adapt_tool_definition_to_oci_standard( selected_params["tools"], @@ -583,9 +564,7 @@ class OCIChatConfig(BaseConfig): system_messages: Final = [m for m in messages if m.get("role") == "system"] preamble_override = None if system_messages: - preamble: Final = "\n".join( - extract_text_content(m["content"]) for m in system_messages - ) + preamble: Final = "\n".join(extract_text_content(m["content"]) for m in system_messages) if preamble: preamble_override = preamble @@ -602,19 +581,11 @@ class OCIChatConfig(BaseConfig): # When a user message follows the tool results, the turn is a normal # one and the request keeps the previous shape (tool results stay in # ``chatHistory``). - is_tool_result_turn: Final = ( - bool(messages) and messages[-1].get("role") == "tool" - ) + is_tool_result_turn: Final = bool(messages) and messages[-1].get("role") == "tool" remaining_history, trailing_tool_results = ( - split_trailing_tool_results(cohere_history) - if is_tool_result_turn - else (cohere_history, []) - ) - chat_message: Final = ( - "" - if is_tool_result_turn - else extract_text_content(user_messages[-1]["content"]) + split_trailing_tool_results(cohere_history) if is_tool_result_turn else (cohere_history, []) ) + chat_message: Final = "" if is_tool_result_turn else extract_text_content(user_messages[-1]["content"]) chat_request: Final = CohereChatRequest( apiFormat="COHERE", @@ -672,13 +643,9 @@ class OCIChatConfig(BaseConfig): vendor: Final = get_vendor_from_model(model) if vendor == OCIVendors.COHERE: - model_response = handle_cohere_response( - response_json, model, model_response, raw_response - ) + model_response = handle_cohere_response(response_json, model, model_response, raw_response) else: - model_response = handle_generic_response( - response_json, model, model_response, raw_response - ) + model_response = handle_generic_response(response_json, model, model_response, raw_response) model_response.hidden_params["additional_headers"] = raw_response.headers return model_response @@ -707,11 +674,7 @@ class OCIChatConfig(BaseConfig): response: Final = client.post( api_base, headers=headers, - data=( - signed_json_body - if signed_json_body is not None - else json.dumps(data) - ), + data=(signed_json_body if signed_json_body is not None else json.dumps(data)), stream=True, logging_obj=logging_obj, timeout=STREAMING_TIMEOUT, @@ -753,11 +716,7 @@ class OCIChatConfig(BaseConfig): response: Final = await client.post( api_base, headers=headers, - data=( - signed_json_body - if signed_json_body is not None - else json.dumps(data) - ), + data=(signed_json_body if signed_json_body is not None else json.dumps(data)), stream=True, logging_obj=logging_obj, timeout=STREAMING_TIMEOUT, @@ -775,9 +734,7 @@ class OCIChatConfig(BaseConfig): logging_obj=logging_obj, ) - def get_error_class( - self, error_message: str, status_code: int, headers: dict | httpx.Headers - ) -> BaseLLMException: + def get_error_class(self, error_message: str, status_code: int, headers: dict | httpx.Headers) -> BaseLLMException: return OCIError(status_code=status_code, message=error_message) From 3e101a5d6f0824d221a176387be052cf33507720 Mon Sep 17 00:00:00 2001 From: dingdangmao <2756823972@qq.com> Date: Thu, 15 Oct 2026 22:40:59 +0800 Subject: [PATCH 6/6] fix(oci): use subscript access for tool-result-turn role check The basedpyright delta gate reported one new reportUnknownMemberType vs merge-base: `messages[-1].get("role")` in the tool-result-turn guard. Switch to subscript access, matching the surrounding code (`user_messages[-1]["content"]`), so the strict member-type rule does not flag it. No behavioral change; 12/12 tool-result-turn tests and both pre-existing chatHistory tests still pass. Signed-off-by: apex-mochen <2756823972@qq.com> --- litellm/llms/oci/chat/transformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/oci/chat/transformation.py b/litellm/llms/oci/chat/transformation.py index 4a2aa9887e0..f5bbf65d619 100644 --- a/litellm/llms/oci/chat/transformation.py +++ b/litellm/llms/oci/chat/transformation.py @@ -581,7 +581,7 @@ class OCIChatConfig(BaseConfig): # When a user message follows the tool results, the turn is a normal # one and the request keeps the previous shape (tool results stay in # ``chatHistory``). - is_tool_result_turn: Final = bool(messages) and messages[-1].get("role") == "tool" + is_tool_result_turn: Final = bool(messages) and messages[-1]["role"] == "tool" remaining_history, trailing_tool_results = ( split_trailing_tool_results(cohere_history) if is_tool_result_turn else (cohere_history, []) )