fix(oci): address bug review findings in chat transformation

- Cohere param map: keep tool_choice/n as False (not omitted) so unsupported
  params are dropped or rejected rather than silently passed through.
- get_complete_url: when an explicit api_base/litellm.api_base is provided,
  use it as-is instead of unconditionally appending /20231130/actions/chat
  (mirrors the embed config behavior).
- Cohere stream: require both chatHistory and finishReason to be present to
  identify a terminal consolidation chunk, avoiding silent text suppression
  if chatHistory ever appears on a non-terminal chunk.
- Generic usage: use 'is not None' for reasoningTokens so a legitimate value
  of 0 is preserved instead of being treated as absent.

Co-authored-by: Yassin Kortam <yassin@berri.ai>
This commit is contained in:
Cursor Agent 2026-05-19 07:19:17 +00:00
parent b59dc01159
commit 14be80d39f
No known key found for this signature in database
3 changed files with 20 additions and 8 deletions

View file

@ -254,11 +254,14 @@ def handle_cohere_stream_chunk(dict_chunk: dict) -> ModelResponseStream:
typed_chunk.index = 0
# OCI Cohere's terminal SSE event re-sends the full assembled response in
# `text` alongside a populated `chatHistory`. Emitting that text would
# concatenate the whole response onto the already-streamed deltas.
# `chatHistory` is the correct discriminator: `finishReason` is a weaker
# signal that could in principle appear on a non-consolidated chunk.
is_terminal_consolidation = typed_chunk.chatHistory is not None
# `text` alongside a populated `chatHistory` and a non-null `finishReason`.
# Emitting that text would concatenate the whole response onto the
# already-streamed deltas. We require both signals to be present so that a
# future API change which adds `chatHistory` to intermediate chunks (or a
# rare early-populated case) doesn't silently drop legitimate token deltas.
is_terminal_consolidation = (
typed_chunk.chatHistory is not None and typed_chunk.finishReason is not None
)
text = "" if is_terminal_consolidation else (typed_chunk.text or "")
finish_reason = typed_chunk.finishReason

View file

@ -327,7 +327,7 @@ def handle_generic_response(
reasoning_tokens: Optional[int] = None
if (
oci_usage.completionTokensDetails
and oci_usage.completionTokensDetails.reasoningTokens
and oci_usage.completionTokensDetails.reasoningTokens is not None
):
reasoning_tokens = oci_usage.completionTokensDetails.reasoningTokens
model_response.usage = Usage( # type: ignore[attr-defined]

View file

@ -195,11 +195,15 @@ class OCIChatConfig(BaseConfig):
# - tool_choice is unsupported
# - stop sequences key is "stopSequences" not "stop"
# - n (numGenerations) is GENERIC-only
# The unsupported keys are kept in the map with value ``False`` so
# ``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()
if k not in ("tool_choice", "max_retries", "n")
}
self.openai_to_oci_cohere_param_map["tool_choice"] = False
self.openai_to_oci_cohere_param_map["n"] = False
# OCI Cohere models are not reasoning models; mark reasoning_effort
# explicitly unsupported so callers either get a clear error or have
# the param dropped under drop_params, rather than silently passing
@ -316,7 +320,12 @@ class OCIChatConfig(BaseConfig):
litellm_params: dict,
stream: Optional[bool] = None,
) -> str:
base = get_oci_base_url(optional_params, api_base or litellm.api_base)
# If the caller provides a full endpoint URL, use it as-is.
# Otherwise construct the standard OCI GenAI chat endpoint from the region.
resolved_base = api_base or litellm.api_base
if resolved_base:
return resolved_base.rstrip("/")
base = get_oci_base_url(optional_params, None)
return f"{base}/{OCI_API_VERSION}/actions/chat"
def _get_optional_params(