mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-15 09:12:18 +00:00
fix(oci): keep an aborted tool turn on stop instead of tool_calls
The finish-reason upgrade fired on every reason that normalized to stop, so a Cohere turn OCI ended with ERROR, ERROR_TOXIC, ERROR_LIMIT, USER_CANCEL or CANCELLED after streaming a tool delta came back as tool_calls and invited the client to run a call the provider had just blocked. Gate the upgrade on the reasons that mean the turn ran to completion, which is the only case it was ever needed for: GENERIC already sends tool_calls itself.
This commit is contained in:
parent
6c7cc62c52
commit
07de96ca98
3 changed files with 30 additions and 17 deletions
|
|
@ -252,22 +252,18 @@ _OCI_FINISH_REASONS: Final = MappingProxyType(
|
|||
|
||||
_OPENAI_FINISH_REASONS: Final = frozenset({"stop", "length", "tool_calls", "content_filter", "function_call"})
|
||||
|
||||
_OCI_RAN_TO_COMPLETION_REASONS: Final = frozenset({"COMPLETE", "stop"})
|
||||
|
||||
|
||||
def _normalize_oci_finish_reason(raw: str | None, *, has_tool_calls: bool = False) -> str | None:
|
||||
"""Map an OCI finish reason to its OpenAI-standard equivalent.
|
||||
|
||||
OCI uses two vocabularies. Cohere sends ``COMPLETE`` / ``MAX_TOKENS`` /
|
||||
``TOOL_CALL(S)`` plus a long tail of error and cancel reasons, while GENERIC
|
||||
already sends the OpenAI names and has to be passed through rather than
|
||||
collapsed. Anything outside both sets becomes ``"stop"``, ``None`` stays
|
||||
``None`` so intermediate stream chunks stay open, and ``has_tool_calls``
|
||||
upgrades ``stop`` to ``tool_calls`` for a Cohere tool turn, which OCI still
|
||||
labels ``COMPLETE``.
|
||||
"""Cohere speaks ``COMPLETE`` / ``MAX_TOKENS`` / ``TOOL_CALL(S)`` plus a tail of error and
|
||||
cancel reasons while GENERIC already speaks OpenAI, and only a turn that ran to completion
|
||||
may be upgraded to ``tool_calls``, so an aborted one never invites a client to run the call.
|
||||
"""
|
||||
if raw is None:
|
||||
return None
|
||||
normalized: Final = _OCI_FINISH_REASONS.get(raw, raw if raw in _OPENAI_FINISH_REASONS else "stop")
|
||||
return "tool_calls" if has_tool_calls and normalized == "stop" else normalized
|
||||
return "tool_calls" if has_tool_calls and raw in _OCI_RAN_TO_COMPLETION_REASONS else normalized
|
||||
|
||||
|
||||
def _synthesize_oci_tool_call_id(position: int, name: str, arguments: str) -> str:
|
||||
|
|
|
|||
|
|
@ -502,6 +502,27 @@ class TestOCICohereToolCalls:
|
|||
|
||||
assert chunks[-1].choices[0].finish_reason == "tool_calls"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"aborted_reason", ["ERROR", "ERROR_TOXIC", "ERROR_LIMIT", "USER_CANCEL", "CANCELLED"]
|
||||
)
|
||||
def test_cohere_aborted_tool_turn_still_reports_stop(self, aborted_reason: str):
|
||||
"""A turn OCI cut short is not a tool turn, however many tool deltas already went out."""
|
||||
wrapper = OCIStreamWrapper(
|
||||
completion_stream=MagicMock(), model="cohere.command-a-03-2025", logging_obj=MagicMock()
|
||||
)
|
||||
tool_calls_event = {
|
||||
"apiFormat": "COHERE",
|
||||
"text": "I will use the get_weather tool.",
|
||||
"toolCalls": [{"name": "get_weather", "parameters": {"city": "Paris"}}],
|
||||
}
|
||||
|
||||
chunks = [
|
||||
wrapper.chunk_creator(f"data: {json.dumps(event)}")
|
||||
for event in (tool_calls_event, {"apiFormat": "COHERE", "finishReason": aborted_reason})
|
||||
]
|
||||
|
||||
assert chunks[-1].choices[0].finish_reason == "stop"
|
||||
|
||||
def test_cohere_non_streaming_tool_call_reports_tool_calls(self):
|
||||
"""Same COMPLETE-on-a-tool-turn story on the non-streaming Cohere response."""
|
||||
body = {
|
||||
|
|
|
|||
|
|
@ -472,13 +472,9 @@ class TestGpt5MaxCompletionTokens:
|
|||
|
||||
|
||||
class TestGenericFinishReasonFidelity:
|
||||
"""OCI GENERIC sends the OpenAI finish-reason names verbatim.
|
||||
|
||||
Captured against ``oci/meta.llama-3.3-70b-instruct`` in ``us-chicago-1``: a
|
||||
tool-calling turn ends with ``"finishReason": "tool_calls"`` and a
|
||||
max-token cut-off with ``"finishReason": "length"``, both lowercase.
|
||||
Collapsing either to ``stop`` leaves an agent loop with no signal to run
|
||||
the tool and hides truncation from the caller.
|
||||
"""``_TOOL_CALL_WIRE`` was captured against ``oci/meta.llama-3.3-70b-instruct`` in
|
||||
``us-chicago-1``, where a GENERIC tool-calling turn ends with ``"finishReason": "tool_calls"``
|
||||
and a max-token cut-off with ``"finishReason": "length"``, both already lowercase.
|
||||
"""
|
||||
|
||||
_TOOL_CALL_WIRE = [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue