Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ea49167
feat(oci): add embeddings, fix streaming/reasoning, expand model catalog
fede-kamel Apr 5, 2026
85c1785
fix(oci): remove dead OCI elif branch in utils.py, align async split_…
fede-kamel Apr 5, 2026
723ff11
test(oci): add unit tests for split_chunks fix and no-duplicate-OCI-b…
fede-kamel Apr 5, 2026
9e16dd8
fix(oci): address remaining bugs from issue #25082 β€” streaming signed…
fede-kamel Apr 5, 2026
507a41d
fix(oci): comprehensive code quality pass β€” bugs, tests, schema accuracy
fede-kamel Apr 5, 2026
ac3f2ce
test(oci): move integration tests to tests/llm_translation/
fede-kamel Apr 5, 2026
b4fbfdb
fix(oci): align types and transformation with official OCI SDK
fede-kamel Apr 5, 2026
594b2c7
fix(oci): use 'output' key in Cohere tool result outputs (matches ref…
fede-kamel Apr 5, 2026
f2c7bce
fix(oci): port schema/type utilities from langchain-oracle reference …
fede-kamel Apr 5, 2026
58b9ef0
refactor(oci): split transformation.py into cohere.py and generic.py
fede-kamel Apr 5, 2026
feaae97
refactor(oci): principal-level code quality pass
fede-kamel Apr 5, 2026
85499db
fix(oci): address PR review findings
fede-kamel Apr 7, 2026
40e0e27
test(oci): add unit tests to improve patch coverage
fede-kamel Apr 7, 2026
d4c0f59
fix(oci): rename supports_streaming to supports_native_streaming in m…
fede-kamel Apr 7, 2026
3d665bb
test(oci): add 67 tests targeting uncovered happy paths for coverage
fede-kamel Apr 7, 2026
fd04797
fix(oci): suppress CodeQL false positive on sha256_base64 (OCI HTTP s…
fede-kamel Apr 7, 2026
1db9984
fix(oci): remove 6 duplicate model price entries and reconcile confli…
fede-kamel Apr 7, 2026
52029df
fix(oci): route GPT-5 family to maxCompletionTokens
fede-kamel May 6, 2026
b98b091
ci(oci): fix CI failures β€” black formatting + recursive_detector ignore
fede-kamel May 6, 2026
1858468
fix(oci): silence MyPy errors in cohere.py β€” typed-dict access
fede-kamel May 6, 2026
ae795ed
fix(oci): silence CodeQL py/weak-sensitive-data-hashing on sha256_base64
fede-kamel May 6, 2026
22c0904
feat(oci): add reasoning_effort passthrough β€” only true missing primi…
fede-kamel May 6, 2026
d124c25
feat(oci): reasoning_effort + reasoning_tokens for OCI GenAI
fede-kamel May 14, 2026
1632973
fix(codeql): re-scope py/weak-sensitive-data-hashing exclusion to OCI…
fede-kamel May 14, 2026
f8bfb6d
fix(oci): drop duplicate text on Cohere streaming terminal chunk
fede-kamel May 17, 2026
2f10425
fix(oci): buffer SSE stream across HTTP read boundaries
fede-kamel May 18, 2026
8a9ba25
test(oci): repoint TestOCIKeyNormalization to sign_with_manual_creden…
fede-kamel May 18, 2026
60082de
test(oci): end-to-end proxy integration test against real OCI GenAI
fede-kamel May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/codeql/codeql-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,17 @@ paths-ignore:
- tests
- docs
- "**/*.md"
# py/weak-sensitive-data-hashing (CWE-328): the OCI signing call at
# litellm/llms/oci/common_utils.py hashes the HTTP request body to produce
# the x-content-sha256 header required by the OCI HTTP signing spec β€” a
# content-integrity hash, not a password or secret hash. SHA-256 is mandated
# by Oracle for this header; see
# https://docs.oracle.com/en-us/iaas/Content/API/Concepts/signingrequests.htm
#
# CodeQL has no native per-query path-scope filter and GitHub Code Scanning
# ignores `# lgtm[...]` / `# codeql[...]` inline comments, so path-ignoring
# this single file is the narrowest available suppression. The `usedforsecurity=False`
# flag on the hashlib.sha256 call already declares non-security intent but
# CodeQL's taint flow still re-fires when callers further up the stack are
# modified.
- litellm/llms/oci/common_utils.py
36 changes: 29 additions & 7 deletions litellm/llms/custom_httpx/llm_http_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,18 @@ def embedding(
headers=headers,
)

# Some providers (e.g. OCI) require request signing after the body is built.
# The default BaseConfig.sign_request returns (headers, None) β€” a no-op for
# providers that don't need signing.
headers, signed_body = provider_config.sign_request(
headers=headers,
optional_params=optional_params,
request_data=data,
api_base=api_base,
api_key=api_key,
model=model,
)

## LOGGING
logging_obj.pre_call(
input=input,
Expand All @@ -916,6 +928,7 @@ def embedding(
client=client,
optional_params=optional_params,
litellm_params=litellm_params,
signed_body=signed_body,
)

if client is None or not isinstance(client, HTTPHandler):
Expand All @@ -929,7 +942,7 @@ def embedding(
response = sync_httpx_client.post(
url=api_base,
headers=headers,
data=json.dumps(data),
data=signed_body if signed_body is not None else json.dumps(data),
timeout=timeout,
)
except Exception as e:
Expand Down Expand Up @@ -964,6 +977,7 @@ async def aembedding(
api_key: Optional[str] = None,
timeout: Optional[Union[float, httpx.Timeout]] = None,
client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None,
signed_body: Optional[bytes] = None,
) -> EmbeddingResponse:
if client is None or not isinstance(client, AsyncHTTPHandler):
async_httpx_client = get_async_httpx_client(
Expand All @@ -974,12 +988,20 @@ async def aembedding(
async_httpx_client = client

try:
response = await async_httpx_client.post(
url=api_base,
headers=headers,
json=request_data,
timeout=timeout,
)
if signed_body is not None:
response = await async_httpx_client.post(
url=api_base,
headers=headers,
data=signed_body,
timeout=timeout,
)
else:
response = await async_httpx_client.post(
url=api_base,
headers=headers,
json=request_data,
timeout=timeout,
)
except Exception as e:
raise self._handle_error(e=e, provider_config=provider_config)

Expand Down
283 changes: 283 additions & 0 deletions litellm/llms/oci/chat/cohere.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
"""
OCI Generative AI β€” Cohere-specific chat transformation helpers.

Handles message history building, tool definition adaptation, non-streaming
response parsing, and streaming chunk parsing for models served with
``apiFormat="COHERE"`` (e.g. ``cohere.command-*``).
"""

import datetime
import json
import uuid
from typing import Any, Dict, List, Optional

from litellm.llms.oci.common_utils import (
OCI_JSON_TO_PYTHON_TYPES,
OCIError,
enrich_cohere_param_description,
resolve_oci_schema_anyof,
resolve_oci_schema_refs,
sanitize_oci_schema,
)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
from litellm.types.llms.oci import (
CohereChatResult,
CohereMessage,
CohereParameterDefinition,
CohereStreamChunk,
CohereTool,
CohereToolCall,
CohereToolMessage,
CohereToolResult,
)
from litellm.types.llms.openai import AllMessageValues

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing architecture-level cycle shared by all ~180 provider modules in litellm/llms/. All provider transformation files do import litellm at module level to access runtime flags like litellm.api_base β€” the cycle flows through litellm.__init__ regardless of our code. The direct litellm.utils imports have been replaced with litellm.types.utils where possible (commit 14ed750), but the root cycle via import litellm cannot be eliminated without a codebase-wide refactor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing architecture-level cycle shared by all ~180 provider modules in litellm/llms/. Every provider does import litellm at module level to access runtime flags like litellm.api_base; that single edge creates the fundamental cycle through litellm.__init__. The direct litellm.utils imports have been replaced with litellm.types.utils / litellm_core_utils.streaming_handler where possible (commit 14ed750), but the root cycle via import litellm cannot be eliminated without a codebase-wide refactor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing architecture-level cycle shared by all ~180 provider modules in litellm/llms/. Every provider does import litellm at module level to access runtime flags like litellm.api_base; that single edge creates the fundamental cycle through litellm.__init__. The direct litellm.utils imports have been replaced with litellm.types.utils / litellm_core_utils.streaming_handler where possible (commit 14ed750), but the root cycle via import litellm cannot be eliminated without a codebase-wide refactor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing architecture pattern shared by all ~180 LiteLLM providers: each provider does import litellm at module level which routes back through litellm.__init__ β†’ litellm.utils. This cycle exists in the base codebase and cannot be fixed within scope of this PR without a repo-wide refactor. The direct litellm.utils import edges that were in our new files have been replaced with litellm.types.utils / litellm.litellm_core_utils.* to reduce cycle depth (commit 14ed750).

from litellm.types.utils import (
Choices,
Delta,
ModelResponse,
ModelResponseStream,
StreamingChoices,
)
Comment on lines +33 to +39

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing architecture-level cycle shared by all ~180 provider modules in litellm/llms/. All provider transformation files do import litellm at module level to access runtime flags like litellm.api_base β€” the cycle flows through litellm.__init__ regardless of our code. The direct litellm.utils imports have been replaced with litellm.types.utils where possible (commit 14ed750), but the root cycle via import litellm cannot be eliminated without a codebase-wide refactor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing architecture-level cycle shared by all ~180 provider modules in litellm/llms/. Every provider does import litellm at module level to access runtime flags like litellm.api_base; that single edge creates the fundamental cycle through litellm.__init__. The direct litellm.utils imports have been replaced with litellm.types.utils / litellm_core_utils.streaming_handler where possible (commit 14ed750), but the root cycle via import litellm cannot be eliminated without a codebase-wide refactor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing architecture-level cycle shared by all ~180 provider modules in litellm/llms/. Every provider does import litellm at module level to access runtime flags like litellm.api_base; that single edge creates the fundamental cycle through litellm.__init__. The direct litellm.utils imports have been replaced with litellm.types.utils / litellm_core_utils.streaming_handler where possible (commit 14ed750), but the root cycle via import litellm cannot be eliminated without a codebase-wide refactor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing architecture pattern shared by all ~180 LiteLLM providers: each provider does import litellm at module level which routes back through litellm.__init__ β†’ litellm.utils. This cycle exists in the base codebase and cannot be fixed within scope of this PR without a repo-wide refactor. The direct litellm.utils import edges that were in our new files have been replaced with litellm.types.utils / litellm.litellm_core_utils.* to reduce cycle depth (commit 14ed750).

from litellm.types.utils import Usage

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing architecture-level cycle shared by all ~180 provider modules in litellm/llms/. Every provider does import litellm at module level to access runtime flags like litellm.api_base; that single edge creates the fundamental cycle through litellm.__init__. Direct litellm.utils imports have already been replaced with litellm.types.utils / litellm.litellm_core_utils.* in commit 14ed750 to reduce the cycle depth, but the root cycle via import litellm cannot be eliminated without a codebase-wide refactor β€” CodeQL flags every provider file equally. No scope for this PR to fix.



def _extract_text_content(content: Any) -> str:
"""Return the plain-text representation of a message content value."""
if content is None:
return ""
if isinstance(content, str):
return content
if isinstance(content, list):
return "".join(
item.get("text", "")
for item in content
if isinstance(item, dict) and item.get("type") == "text"
)
return str(content)


def adapt_messages_to_cohere_standard(
messages: List[AllMessageValues],
) -> List[CohereMessage]:
"""Build a Cohere ``chatHistory`` list from an OpenAI-format message array.

- All messages except the last are included (the last becomes ``message``).
- Tool results are expressed as OCI ``CohereToolMessage.toolResults`` entries,
with the originating call's name and parameters resolved from the preceding
assistant message via a ``tool_call_id`` lookup.
"""
# First pass: build tool_call_id β†’ CohereToolCall so tool-result messages can
# reference the originating call by name and parameters.
tool_call_lookup: Dict[str, CohereToolCall] = {}
for msg in messages:
if msg.get("role") == "assistant":
tool_calls_raw: Any = msg.get("tool_calls") or []
for tc in tool_calls_raw:
tc_id = tc.get("id", "")
raw_args: Any = tc.get("function", {}).get("arguments", "{}")
try:
params: Dict[str, Any] = (
json.loads(raw_args) if isinstance(raw_args, str) else raw_args
)
except json.JSONDecodeError:
params = {}
tool_call_lookup[tc_id] = CohereToolCall(
name=str(tc.get("function", {}).get("name", "")),
parameters=params,
)

chat_history: List[CohereMessage] = []
for msg in messages[:-1]:
role = msg.get("role")
content = _extract_text_content(msg.get("content"))

tool_calls: Optional[List[CohereToolCall]] = None
if role == "assistant" and msg.get("tool_calls"): # type: ignore[union-attr,typeddict-item]
tool_calls = []
for tc in msg["tool_calls"]: # type: ignore[union-attr,typeddict-item]
raw_arguments: Any = tc.get("function", {}).get("arguments", {})
if isinstance(raw_arguments, str):
try:
arguments: Dict[str, Any] = json.loads(raw_arguments)
except json.JSONDecodeError:
arguments = {}
else:
arguments = raw_arguments
tool_calls.append(
CohereToolCall(
name=str(tc.get("function", {}).get("name", "")),
parameters=arguments,
)
)

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)
)
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={})
)
chat_history.append(
CohereToolMessage(
toolResults=[
CohereToolResult(
call=cohere_call,
outputs=[{"output": content}],
)
]
)
)

return chat_history


def adapt_tool_definitions_to_cohere_standard(
tools: List[Dict[str, Any]],
) -> List[CohereTool]:
"""Adapt OpenAI-format tool definitions to the OCI Cohere format.

- Resolves ``$ref``/``$defs`` and ``anyOf`` patterns that OCI rejects.
- Maps JSON Schema type names to Python type names (``"string"`` β†’ ``"str"``).
- Embeds unsupported constraints (enum, format, range, pattern) into the
parameter description so the model can still see them.
"""
cohere_tools = []
for tool in tools:
function_def = tool.get("function", {})
raw_params = function_def.get("parameters", {})

resolved = sanitize_oci_schema(
resolve_oci_schema_anyof(resolve_oci_schema_refs(raw_params))
)
properties = resolved.get("properties", {})
required = resolved.get("required", [])

parameter_definitions = {}
for param_name, param_schema in properties.items():
json_type = param_schema.get("type", "string")
python_type = OCI_JSON_TO_PYTHON_TYPES.get(json_type, json_type)
parameter_definitions[param_name] = CohereParameterDefinition(
description=enrich_cohere_param_description(
param_schema.get("description", ""), param_schema
),
type=python_type,
isRequired=param_name in required,
)

cohere_tools.append(
CohereTool(
name=function_def.get("name", ""),
description=function_def.get("description", ""),
parameterDefinitions=parameter_definitions,
)
)

return cohere_tools


def handle_cohere_response(
json_response: dict,
model: str,
model_response: ModelResponse,
) -> ModelResponse:
"""Parse a non-streaming Cohere OCI response into a LiteLLM ModelResponse."""
cohere_response = CohereChatResult(**json_response)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 CohereChatResult instantiation has no error guard

CohereChatResult(**json_response) raises pydantic.ValidationError when the payload doesn't match the schema (e.g. a missing required field or an unexpected type). Unlike the streaming path β€” which at least has an except TypeError guard β€” the non-streaming Cohere response path has no exception handler at all. Any structural mismatch from an OCI endpoint version change or a partial error response propagates as an unhandled exception rather than a structured OCIError.

Suggested change
cohere_response = CohereChatResult(**json_response)
try:
cohere_response = CohereChatResult(**json_response)
except Exception as e:
raise OCIError(
status_code=500,
message=f"Response cannot be casted to CohereChatResult: {str(e)}",
)


model_response.model = model
model_response.created = int(datetime.datetime.now().timestamp())

response_text = cohere_response.chatResponse.text
oci_finish_reason = cohere_response.chatResponse.finishReason

if oci_finish_reason == "COMPLETE":
finish_reason = "stop"
elif oci_finish_reason == "MAX_TOKENS":
finish_reason = "length"
elif oci_finish_reason == "TOOL_CALL":
finish_reason = "tool_calls"
else:
finish_reason = oci_finish_reason

tool_calls: Optional[List[Dict[str, Any]]] = None
if cohere_response.chatResponse.toolCalls:
tool_calls = [
{
"id": f"call_{uuid.uuid4().hex[:24]}",
"type": "function",
"function": {
"name": tc.name,
"arguments": json.dumps(tc.parameters),
},
}
for tc in cohere_response.chatResponse.toolCalls
]

model_response.choices = [
Choices(
index=0,
message={
"role": "assistant",
"content": response_text,
"tool_calls": tool_calls,
},
finish_reason=finish_reason,
)
]

usage_info = cohere_response.chatResponse.usage
model_response.usage = Usage( # type: ignore[attr-defined]
prompt_tokens=usage_info.promptTokens, # type: ignore[union-attr]
completion_tokens=usage_info.completionTokens, # type: ignore[union-attr]
total_tokens=usage_info.totalTokens, # type: ignore[union-attr]
)

return model_response


def handle_cohere_stream_chunk(dict_chunk: dict) -> ModelResponseStream:
"""Parse a single Cohere SSE chunk into a LiteLLM ModelResponseStream."""
try:
typed_chunk = CohereStreamChunk(**dict_chunk)
except TypeError as e:
raise OCIError(
status_code=500,
message=f"Chunk cannot be parsed as CohereStreamChunk: {str(e)}",
)

if typed_chunk.index is None:
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 = "" if is_terminal_consolidation else (typed_chunk.text or "")

finish_reason = typed_chunk.finishReason
if finish_reason == "COMPLETE":
finish_reason = "stop"
elif finish_reason == "MAX_TOKENS":
finish_reason = "length"
elif finish_reason == "TOOL_CALL":
finish_reason = "tool_calls"

return ModelResponseStream(
choices=[
StreamingChoices(
index=typed_chunk.index if typed_chunk.index else 0,
delta=Delta(
content=text,
tool_calls=None,
provider_specific_fields=None,
thinking_blocks=None,
reasoning_content=None,
),
finish_reason=finish_reason,
)
]
)
Loading
Loading