mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-26 01:12:21 +00:00
fix(oci): harden Cohere stream/finish-reason and dedupe maxTokens param mapping
Some checks are pending
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 / guardrails-hooks (push) Blocked by required conditions
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-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / schema-migration (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests: Security / security (push) Waiting to run
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Some checks are pending
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 / guardrails-hooks (push) Blocked by required conditions
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-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / schema-migration (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests: Security / security (push) Waiting to run
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
- Cohere stream: track per-stream tool-call emission and only suppress the terminal consolidation chunk's tool calls once they've been seen earlier. Prevents silent drop if tool calls are delivered exclusively on the terminal chunk. - Cohere stream: emit content=None (not "") on non-terminal text-free chunks (e.g. tool-call-only / keep-alive) so downstream consumers that distinguish missing vs explicitly-empty deltas behave correctly. - Generic handlers: accept singular TOOL_CALL finish reason in addition to TOOL_CALLS, matching the Cohere handlers. - _get_optional_params: when both max_tokens and max_completion_tokens are provided, explicitly prefer max_completion_tokens instead of relying on dict iteration order. Co-authored-by: Yassin Kortam <yassin@berri.ai>
This commit is contained in:
parent
48418f5b01
commit
61b99a0475
3 changed files with 59 additions and 14 deletions
|
|
@ -279,8 +279,18 @@ def handle_cohere_response(
|
|||
return model_response
|
||||
|
||||
|
||||
def handle_cohere_stream_chunk(dict_chunk: dict) -> ModelResponseStream:
|
||||
"""Parse a single Cohere SSE chunk into a LiteLLM ModelResponseStream."""
|
||||
def handle_cohere_stream_chunk(
|
||||
dict_chunk: dict, prior_tool_calls_emitted: bool = False
|
||||
) -> ModelResponseStream:
|
||||
"""Parse a single Cohere SSE chunk into a LiteLLM ModelResponseStream.
|
||||
|
||||
``prior_tool_calls_emitted`` lets the caller signal whether tool calls
|
||||
were already emitted in earlier chunks of the same stream. When set, the
|
||||
terminal consolidation chunk's tool calls are suppressed (they would
|
||||
duplicate prior deltas); otherwise they are passed through so a stream
|
||||
that delivers tool calls only on the terminal chunk doesn't silently
|
||||
drop them.
|
||||
"""
|
||||
try:
|
||||
typed_chunk = CohereStreamChunk(**dict_chunk)
|
||||
except (TypeError, ValidationError) as e:
|
||||
|
|
@ -301,17 +311,25 @@ def handle_cohere_stream_chunk(dict_chunk: dict) -> ModelResponseStream:
|
|||
is_terminal_consolidation = (
|
||||
typed_chunk.chatHistory is not None and typed_chunk.finishReason is not None
|
||||
)
|
||||
text: Optional[str] = (
|
||||
None if is_terminal_consolidation else (typed_chunk.text or "")
|
||||
)
|
||||
# On non-terminal text-free chunks (e.g. tool-call-only or keep-alive
|
||||
# chunks) emit ``content=None`` rather than ``content=""`` so downstream
|
||||
# stream-mergers that distinguish "no text in this delta" from "an
|
||||
# explicitly empty text delta" behave correctly.
|
||||
text: Optional[str] = None if is_terminal_consolidation else typed_chunk.text
|
||||
|
||||
# Tool calls on the terminal consolidation chunk (whether from
|
||||
# `typed_chunk.toolCalls` or from `chatHistory`) restate what was already
|
||||
# streamed in intermediate chunks. Re-emitting them here would mint fresh
|
||||
# `uuid4` IDs and cause downstream consumers to execute each tool call
|
||||
# twice. Suppress them on the terminal chunk for the same reason `text`
|
||||
# is suppressed above.
|
||||
cohere_tool_calls = None if is_terminal_consolidation else typed_chunk.toolCalls
|
||||
# `typed_chunk.toolCalls` or from `chatHistory`) typically restate what
|
||||
# was already streamed in intermediate chunks. Re-emitting them would
|
||||
# mint fresh `uuid4` IDs and cause downstream consumers to execute each
|
||||
# tool call twice. We only suppress when the caller has confirmed that
|
||||
# tool calls were already emitted earlier — otherwise (e.g. a short
|
||||
# response that delivers tool calls exclusively on the terminal chunk),
|
||||
# passing them through is the only chance to surface them.
|
||||
cohere_tool_calls = (
|
||||
None
|
||||
if (is_terminal_consolidation and prior_tool_calls_emitted)
|
||||
else typed_chunk.toolCalls
|
||||
)
|
||||
|
||||
tool_calls: Optional[List[Dict[str, Any]]] = None
|
||||
if cohere_tool_calls:
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@ def handle_generic_response(
|
|||
model_response.choices[0].finish_reason = "stop" # type: ignore[union-attr]
|
||||
elif oci_finish_reason == "MAX_TOKENS":
|
||||
model_response.choices[0].finish_reason = "length" # type: ignore[union-attr]
|
||||
elif oci_finish_reason == "TOOL_CALLS":
|
||||
elif oci_finish_reason in ("TOOL_CALL", "TOOL_CALLS"):
|
||||
model_response.choices[0].finish_reason = "tool_calls" # type: ignore[union-attr]
|
||||
elif oci_finish_reason is not None:
|
||||
# OCI GENERIC can emit non-OpenAI finish reasons (e.g. ``ERROR``,
|
||||
|
|
@ -418,7 +418,7 @@ def handle_generic_stream_chunk(dict_chunk: dict) -> ModelResponseStream:
|
|||
finish_reason: Optional[str] = "stop"
|
||||
elif oci_finish_reason == "MAX_TOKENS":
|
||||
finish_reason = "length"
|
||||
elif oci_finish_reason == "TOOL_CALLS":
|
||||
elif oci_finish_reason in ("TOOL_CALL", "TOOL_CALLS"):
|
||||
finish_reason = "tool_calls"
|
||||
elif oci_finish_reason is not None:
|
||||
# OCI can emit error/cancel finish reasons (e.g. ``ERROR``,
|
||||
|
|
|
|||
|
|
@ -355,8 +355,21 @@ class OCIChatConfig(BaseConfig):
|
|||
else "maxTokens"
|
||||
)
|
||||
|
||||
# Both ``max_tokens`` and ``max_completion_tokens`` map to OCI's
|
||||
# ``maxTokens`` (or ``maxCompletionTokens`` for reasoning models), so
|
||||
# if both are provided explicitly prefer ``max_completion_tokens``
|
||||
# rather than relying on dict iteration order to pick a winner.
|
||||
prefer_max_completion = (
|
||||
"max_tokens" in optional_params
|
||||
and "max_completion_tokens" in optional_params
|
||||
and param_map.get("max_tokens") == "maxTokens"
|
||||
and param_map.get("max_completion_tokens") == "maxTokens"
|
||||
)
|
||||
|
||||
for openai_key, oci_key in param_map.items():
|
||||
if oci_key and openai_key in optional_params:
|
||||
if prefer_max_completion and openai_key == "max_tokens":
|
||||
continue
|
||||
target = max_tokens_key if oci_key == "maxTokens" else oci_key
|
||||
selected_params[target] = optional_params[openai_key] # type: ignore[index]
|
||||
|
||||
|
|
@ -654,6 +667,11 @@ class OCIStreamWrapper(CustomStreamWrapper):
|
|||
|
||||
def __init__(self, **kwargs: Any):
|
||||
super().__init__(**kwargs)
|
||||
# Tracks whether any prior Cohere chunk in this stream has emitted
|
||||
# tool calls. The Cohere handler uses this to decide whether the
|
||||
# terminal consolidation chunk's tool calls are duplicates (suppress)
|
||||
# or the only copy of the tool calls (pass through).
|
||||
self._cohere_tool_calls_emitted = False
|
||||
|
||||
def chunk_creator(self, chunk: Any) -> ModelResponseStream:
|
||||
if not isinstance(chunk, str):
|
||||
|
|
@ -663,7 +681,16 @@ class OCIStreamWrapper(CustomStreamWrapper):
|
|||
dict_chunk = json.loads(chunk[5:])
|
||||
|
||||
if dict_chunk.get("apiFormat") == "COHERE":
|
||||
return handle_cohere_stream_chunk(dict_chunk)
|
||||
result = handle_cohere_stream_chunk(
|
||||
dict_chunk,
|
||||
prior_tool_calls_emitted=self._cohere_tool_calls_emitted,
|
||||
)
|
||||
if not self._cohere_tool_calls_emitted:
|
||||
for choice in result.choices:
|
||||
if getattr(choice.delta, "tool_calls", None):
|
||||
self._cohere_tool_calls_emitted = True
|
||||
break
|
||||
return result
|
||||
return handle_generic_stream_chunk(dict_chunk)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue