feat: add true async support to OCI provider via aiohttp#4982
feat: add true async support to OCI provider via aiohttp#4982fede-kamel wants to merge 14 commits into
Conversation
4fdf952 to
6d3a27b
Compare
|
Note: we are currently integrating with CrewAI. For now, our official integration is with LangChain: https://github.com/NVIDIA/NeMo-Agent-Toolkit |
|
OCI GenAI PR series (tracking issue: #4944)
|
6d3a27b to
fd8c270
Compare
- OCICompletion(BaseLLM): sync call() and async acall() for generic (Meta, Google, OpenAI, xAI) and Cohere model families - Shared OCI auth utilities (utilities/oci.py): API key, security token, instance principal, and resource principal auth - Provider routing in llm.py: oci/ prefix and OCI model-id patterns - oci registered as optional dependency (crewai[oci]) - Configurable timeout via DEFAULT_OCI_TIMEOUT constant - Cohere and generic request/response paths fully separated Tracking issue: crewAIInc#4944 Part 1 of series: crewAIInc#4959 → crewAIInc#4961 → crewAIInc#4962 → crewAIInc#4963 → crewAIInc#4964 → crewAIInc#4966 → crewAIInc#4982
fd8c270 to
e19bde3
Compare
|
@greysonlalonde @lorenzejay — bumping for review. Status (after rebase):
This PR adds: OCI's official Python SDK is sync-only. This PR wires an v1 spec compliance: like #4959, this targets OCI's This is part of the 7-PR series (#4959, #4961, #4962, #4963, #4964, #4966, #4982). Happy to fold any subset into a single PR if that helps review velocity. |
- OCICompletion(BaseLLM): sync call() and async acall() for generic (Meta, Google, OpenAI, xAI) and Cohere model families - Shared OCI auth utilities (utilities/oci.py): API key, security token, instance principal, and resource principal auth - Provider routing in llm.py: oci/ prefix and OCI model-id patterns - oci registered as optional dependency (crewai[oci]) - Configurable timeout via DEFAULT_OCI_TIMEOUT constant - Cohere and generic request/response paths fully separated Tracking issue: crewAIInc#4944 Part 1 of series: crewAIInc#4959 → crewAIInc#4961 → crewAIInc#4962 → crewAIInc#4963 → crewAIInc#4964 → crewAIInc#4966 → crewAIInc#4982
e19bde3 to
7c93976
Compare
- OCICompletion(BaseLLM): sync call() and async acall() for generic (Meta, Google, OpenAI, xAI) and Cohere model families - Shared OCI auth utilities (utilities/oci.py): API key, security token, instance principal, and resource principal auth - Provider routing in llm.py: oci/ prefix and OCI model-id patterns - oci registered as optional dependency (crewai[oci]) - Configurable timeout via DEFAULT_OCI_TIMEOUT constant - Cohere and generic request/response paths fully separated Tracking issue: crewAIInc#4944 Part 1 of series: crewAIInc#4959 → crewAIInc#4961 → crewAIInc#4962 → crewAIInc#4963 → crewAIInc#4964 → crewAIInc#4966 → crewAIInc#4982
7c93976 to
b8982a9
Compare
📝 WalkthroughWalkthroughChangesOCI support is added for native LLM calls and embeddings, including provider routing, optional dependencies, authentication, synchronous and asynchronous requests, streaming, tools, structured outputs, multimodal inputs, image embeddings, and unit/live integration tests. OCI provider support
Sequence Diagram(s)sequenceDiagram
participant CrewAI
participant OCICompletion
participant OCIAsyncClient
participant OCI_GenAI
CrewAI->>OCICompletion: call or acall messages
OCICompletion->>OCICompletion: build provider-specific request
OCICompletion->>OCIAsyncClient: send async request
OCIAsyncClient->>OCI_GenAI: signed chat request
OCI_GenAI-->>OCIAsyncClient: response or stream events
OCIAsyncClient-->>OCICompletion: decoded content and metadata
OCICompletion-->>CrewAI: text, tool calls, or structured model
sequenceDiagram
participant EmbeddingsFactory
participant OCIProvider
participant OCIEmbeddingFunction
participant OCI_GenAI
EmbeddingsFactory->>OCIProvider: build OCI configuration
OCIProvider->>OCIEmbeddingFunction: initialize callable
OCIEmbeddingFunction->>OCI_GenAI: submit batched embedding request
OCI_GenAI-->>OCIEmbeddingFunction: embedding vectors
OCIEmbeddingFunction-->>EmbeddingsFactory: return aggregated vectors
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
This PR is stale because it has been open for 45 days with no activity. |
- OCICompletion(BaseLLM): sync call() and async acall() for generic (Meta, Google, OpenAI, xAI) and Cohere model families - Shared OCI auth utilities (utilities/oci.py): API key, security token, instance principal, and resource principal auth - Provider routing in llm.py: oci/ prefix and OCI model-id patterns - oci registered as optional dependency (crewai[oci]) - Configurable timeout via DEFAULT_OCI_TIMEOUT constant - Cohere and generic request/response paths fully separated Tracking issue: crewAIInc#4944 Part 1 of series: crewAIInc#4959 → crewAIInc#4961 → crewAIInc#4962 → crewAIInc#4963 → crewAIInc#4964 → crewAIInc#4966 → crewAIInc#4982
- 15 unit tests (mocked OCI SDK, no credentials needed) - 2 parametrized integration tests across 4 model families: meta.llama-3.3-70b-instruct, cohere.command-r-plus-08-2024, google.gemini-2.5-flash, openai.gpt-5.2-chat-latest - conftest.py with shared fixtures for both test suites
Previously: any `openai.*` model on the GENERIC api_format route was sent `max_completion_tokens` instead of `max_tokens`. That works for the GPT-5 family (which OCI requires it on with HTTP 400 if you send `max_tokens`), but is inconsistent with `_is_openai_gpt5_family()` already used elsewhere in the same method for temperature and stop. Switch the param-name routing to the same predicate so GPT-4o / GPT-4.1 keep the standard `max_tokens` field, while only `openai.gpt-5*` gets `max_completion_tokens`. Also reword the test pair to reflect the two distinct behaviours. Verified live against OCI us-chicago-1 — all 7 model families pass: openai.gpt-5.5 ok (max_completion_tokens) openai.gpt-5 ok (max_completion_tokens) openai.gpt-4o Ok. (max_tokens) openai.gpt-4.1 ok (max_tokens) meta.llama-3.3-70b-instruct ok meta.llama-4-scout-17b-16e-instruct ok cohere.command-latest ok (CohereChatRequest)
Add reasoning_effort: str | None = None as a constructor kwarg. When set, it's passed as `reasoning_effort` on the OCI GenericChatRequest — honoured by GPT-5 family, Gemini 2.5, Grok reasoning variants, and Cohere Command-A-Reasoning; ignored by non-reasoning models. Single biggest cost knob for reasoning-capable models on OCI: setting "LOW" typically cuts reasoning-token spend 5-10×. Other GenericChatRequest fields (verbosity, parallel_tool_calls, logit_bias, n, metadata, etc.) aren't exposed — they either duplicate existing controls or aren't missing primitives.
Coderabbit's pre-merge check flagged 45.10% docstring coverage on this PR; required threshold is 80%. Add concise docstrings for the remaining 14 methods on OCICompletion (init, provider helpers, message helpers, response extractors, call/acall, _chat). Coverage now 100% per ast walk; no behavioural change.
b8982a9 to
b55be34
Compare
Drop unreachable os.getenv defaults in the live-config fixture and fix the pytest path in the run instructions to include the lib/crewai/ prefix.
Add streaming text completion via OCI SSE events: - stream=True in call() routes to _stream_call_impl with chunk events - iter_stream() yields raw text chunks (sync generator) - astream() wraps iter_stream via thread+queue for async callers - _stream_chat_events holds client lock for full stream duration - SSE event parsing handles both string and mapping payloads Tested live against meta.llama-3.3-70b-instruct, cohere.command-r-plus-08-2024, google.gemini-2.5-flash, and openai.gpt-5.2-chat-latest. Depends on: crewAIInc#4959 Tracking issue: crewAIInc#4944
Add native function calling for generic and Cohere model families: - _format_tools converts CrewAI tool specs to OCI SDK format - _extract_tool_calls normalizes responses back to CrewAI shape - _handle_tool_calls executes tools and recurses until model finishes - Cohere tool message handling with trailing tool results - Tool choice control (auto/none/required/function) - Passthrough parameter filtering via SDK introspection - Streaming tool call accumulation from SSE fragments - supports_function_calling() returns True Tested live against meta.llama-3.3-70b-instruct with raw tool call return and recursive tool execution. Depends on: crewAIInc#4961 (streaming), crewAIInc#4959 (basic text) Tracking issue: crewAIInc#4944
Add response_model (Pydantic) support for structured output: - _build_response_format converts Pydantic schema to OCI JsonSchemaResponseFormat (generic) or CohereResponseJsonFormat - _parse_structured_response validates and returns typed models - response_model threaded through call, _call_impl, _stream_call_impl, and _handle_tool_calls for full coverage - Handles JSON in markdown fences via base class _validate_structured_output Tested live against meta.llama-3.3-70b-instruct and google.gemini-2.5-flash. Depends on: crewAIInc#4962 (tool calling), crewAIInc#4961 (streaming), crewAIInc#4959 (basic text) Tracking issue: crewAIInc#4944
Add multimodal content handling for generic model families: - vision.py: model lists, data URI helpers, image encoding utilities - _build_generic_content handles image_url, document_url, video_url, audio_url content types mapped to OCI SDK content objects - _message_has_multimodal_content detects non-text payloads - Cohere models reject multimodal with clear error message - supports_multimodal() returns True Depends on: crewAIInc#4963, crewAIInc#4962, crewAIInc#4961, crewAIInc#4959 Tracking issue: crewAIInc#4944
Send a 2x2 red PNG to google.gemini-2.5-flash via data URI and verify it identifies the color. Tests the full image_url content pipeline end-to-end against a live OCI vision model.
cohere.command-a-vision rejects the v1 COHERE api format, so route command-a-vision / command-a-reasoning model ids to a new cohere_v2 request family built on CohereChatRequestV2: message-based history, text + image_url content, CohereToolV2 function tools, JSON response format, and SSE streaming (v2 events carry message.content[].text). Verified live against OCI on-demand in us-chicago-1: plain chat, streaming, and vision all pass; tool use is wired but the service currently rejects tools for command-a-vision.
Add OCI embedding support integrated with CrewAI's RAG pipeline: - OCIEmbeddingFunction: ChromaDB-compatible embedding callable with batching, config serialization, image embedding support - OCIProvider: Pydantic-based provider with alias validation for env vars and config keys - Factory registration in embeddings/factory.py + types.py - Supports text and image embeddings, output dimensions, custom endpoints, all 4 OCI auth modes Tested live against cohere.embed-english-v3.0 with API_KEY auth. Depends on: crewAIInc#4964, crewAIInc#4963, crewAIInc#4962, crewAIInc#4961, crewAIInc#4959 Tracking issue: crewAIInc#4944
Replace asyncio.to_thread wrappers with true async I/O using aiohttp for acall() and astream(). The OCI SDK is sync-only, so we bypass it for HTTP and use its signer for request authentication directly. - oci_async.py: OCIAsyncClient with aiohttp, OCI request signing, native SSE parsing, connection pooling - acall(): true async chat completion (no thread pool) - astream(): true async SSE streaming (no thread+queue bridge) - Graceful fallback to asyncio.to_thread when aiohttp unavailable or client is mocked (unit tests) - aiohttp + certifi added to crewai[oci] optional deps Temporary measure until OCI SDK ships native async support. Tested live: acall, astream, and concurrent acall against meta.llama-3.3-70b-instruct with API_KEY auth. Depends on: crewAIInc#4966, crewAIInc#4964, crewAIInc#4963, crewAIInc#4962, crewAIInc#4961, crewAIInc#4959 Tracking issue: crewAIInc#4944
b55be34 to
50a083a
Compare
|
Rebased onto current main and un-drafted — mergeable again. True-async path verified live against OCI on-demand (us-chicago-1): |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (7)
lib/crewai/src/crewai/rag/embeddings/providers/oci/types.py (1)
8-24: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Annotatedmetadata onmodel_namemay be misleading.Line 11 uses
Annotated[str, "cohere.embed-english-v3.0"]which appears to suggest a default value, butAnnotatedmetadata in aTypedDictis purely informational — it does not set a default. Since the actual default lives inOCIProvider(the Pydantic model), this annotation could confuse consumers into thinking the TypedDict itself provides a default. Consider using a docstring or comment instead to document the recommended model name.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai/src/crewai/rag/embeddings/providers/oci/types.py` around lines 8 - 24, Replace the Annotated metadata on OCIProviderConfig.model_name with a plain str annotation, and document the recommended “cohere.embed-english-v3.0” model using a comment or docstring; keep the actual default defined in OCIProvider.lib/crewai/src/crewai/rag/embeddings/providers/oci/embedding_callable.py (2)
171-181: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value
embed_image_batchsends one API request per image.Each image in the batch results in a separate
embed_textcall (line 177). For large image batches this is inefficient — it could be worth batching multiple data-URI inputs into a singleembed_textrequest if the OCI API supports multiple inputs withinput_type="IMAGE".If the OCI API only accepts one image per request, a comment documenting this constraint would help future maintainers understand the design choice.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai/src/crewai/rag/embeddings/providers/oci/embedding_callable.py` around lines 171 - 181, Review embed_image_batch and the OCI embed_text API to determine whether multiple IMAGE data URIs can be submitted in one request. If supported, convert all images first and issue a single request with input_type="IMAGE"; otherwise retain the per-image calls and add a concise comment documenting the API’s single-image constraint.
154-161: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueNo error handling around
embed_textAPI calls.If an OCI API call fails mid-batch (e.g., rate limit, network error), partial embeddings are silently lost and the exception propagates with no context about which batch failed. Consider wrapping the
embed_textcall with error context or letting callers know this is expected (delegating retry to the caller).This is a common pattern in embedding functions, so if retry/error handling is intentionally left to the caller, a brief docstring note would clarify the contract.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai/src/crewai/rag/embeddings/providers/oci/embedding_callable.py` around lines 154 - 161, Add explicit error context around each OCI embed_text call in __call__, identifying the failed batch and preserving the original exception (or document that retry handling is intentionally delegated to callers). Ensure failures do not appear as unexplained mid-batch errors and clarify the behavior in the callable’s docstring if no internal retry is added.lib/crewai/tests/llms/oci/test_oci_integration_multimodal.py (1)
22-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider extracting
_skip_unless_liveinto a shared conftest.py fixture.This function is duplicated verbatim in
test_oci_integration_tools.pyand partially intest_oci_async.py. The streaming and structured integration tests already useoci_live_configfrom conftest.py for the same purpose. Consolidating would ensure consistent skip logic and reduce maintenance burden when env-var requirements change.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai/tests/llms/oci/test_oci_integration_multimodal.py` around lines 22 - 42, Extract the shared live OCI configuration and skip logic from `_skip_unless_live` into a `conftest.py` fixture, preferably reusing or extending `oci_live_config`. Update `test_oci_integration_multimodal.py`, `test_oci_integration_tools.py`, and `test_oci_async.py` to consume the fixture and remove their local helper implementations, preserving the existing environment validation and returned configuration.lib/crewai/tests/llms/oci/test_oci_integration_tools.py (1)
19-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
_skip_unless_liveand_env_modelsare duplicated fromtest_oci_integration_multimodal.py.These helpers are identical to the ones in
test_oci_integration_multimodal.py. Consider extracting_skip_unless_liveto conftest.py (whereoci_live_configalready exists) and parameterizing_env_modelswith the env var names and defaults.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai/tests/llms/oci/test_oci_integration_tools.py` around lines 19 - 39, Remove the duplicated _skip_unless_live and _env_models helpers from this test module. Reuse the shared OCI live configuration fixture/helper from conftest.py (including oci_live_config), and replace _env_models with a parameterized shared utility or direct environment handling that accepts the relevant variable names and fallback/default values; update all call sites in the integration tests accordingly.lib/crewai/src/crewai/llms/providers/oci/completion.py (1)
1689-1690: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocument the public capability methods.
Add concise docstrings describing the capability assumptions and context-window fallback.
As per coding guidelines, “Document public APIs and complex logic in Python code.”
Also applies to: 1695-1705
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai/src/crewai/llms/providers/oci/completion.py` around lines 1689 - 1690, Document the public capability methods, including supports_function_calling and the related methods around it, with concise docstrings explaining their capability assumptions and context-window fallback behavior. Use the existing method names to locate each public API and ensure every method has documentation consistent with the Python coding guidelines.Source: Coding guidelines
lib/crewai/src/crewai/llms/providers/oci/vision.py (1)
46-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocument the exported vision helpers.
Add concise argument, return-value, and error behavior docstrings for these public functions.
As per coding guidelines, “Document public APIs and complex logic in Python code.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai/src/crewai/llms/providers/oci/vision.py` around lines 46 - 57, Add concise docstrings to the public functions load_image, encode_image, and is_vision_model in vision.py, documenting their arguments, return values, and any relevant errors or error behavior. Keep the documentation consistent with the module’s existing style and accurately describe each function’s behavior.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/crewai/src/crewai/llms/providers/oci/completion.py`:
- Around line 1692-1693: The supports_multimodal method currently reports
support for every OCI model; change it to return is_vision_model(self.model),
using the existing VISION_MODELS detection in vision.py. Add an explicit
override for dedicated OCI endpoints whose multimodal capability cannot be
inferred from the model name.
- Around line 1544-1584: The native async branch must preserve the full
semantics of call(). Update the async implementation around
_prepare_async_request and _async_client.chat_async to run call context,
lifecycle events, hooks, callbacks, tool-call execution, structured response
validation, and response metadata handling consistently with the thread
fallback, including honoring response_model and hook failures; reuse shared
call() helpers where possible rather than returning raw content directly.
- Line 85: The configured timeout is not propagated through the async OCI
request path. Update the OCI provider’s async client setup and `chat_async()`
invocation in `acall()` to pass the `timeout` value, matching the synchronous
path and preserving the caller’s connect/read limits.
In `@lib/crewai/src/crewai/utilities/oci_async.py`:
- Around line 35-45: The recursive _convert_keys_to_camel function incorrectly
transforms user-defined schema keys in async OCI payloads. Restrict key
conversion to known OCI model-attribute mappings, preserving arbitrary nested
JSON schema keys under tool properties and structured-output schemas; update the
call site around the async request construction accordingly.
- Around line 79-89: Update OCIAsyncClient session lifecycle management in
_get_session and close, and ensure OCICompletion tears down or recreates the
cached client/session per event loop. Track the owning event loop for the cached
aiohttp.ClientSession, close and clear it when the loop changes or the client is
no longer needed, and avoid reusing loop-bound sessions across calls.
In `@lib/crewai/tests/llms/oci/test_oci_async.py`:
- Around line 12-24: Update _skip_unless_live to accept either OCI_REGION or
OCI_SERVICE_ENDPOINT, skipping only when both are unset. Add the configured
OCI_SERVICE_ENDPOINT to the returned config as service_endpoint so OCICompletion
uses it, while preserving the existing compartment, authentication type, and
profile handling.
In `@lib/crewai/tests/rag/embeddings/test_factory_oci.py`:
- Around line 109-115: Remove the `.tolist()` conversion in the result
normalization for `OCIEmbeddingFunction.__call__` tests, since the mocked
embeddings are plain lists; compare `result` directly against the expected
nested lists while retaining the length and strict zip assertions.
---
Nitpick comments:
In `@lib/crewai/src/crewai/llms/providers/oci/completion.py`:
- Around line 1689-1690: Document the public capability methods, including
supports_function_calling and the related methods around it, with concise
docstrings explaining their capability assumptions and context-window fallback
behavior. Use the existing method names to locate each public API and ensure
every method has documentation consistent with the Python coding guidelines.
In `@lib/crewai/src/crewai/llms/providers/oci/vision.py`:
- Around line 46-57: Add concise docstrings to the public functions load_image,
encode_image, and is_vision_model in vision.py, documenting their arguments,
return values, and any relevant errors or error behavior. Keep the documentation
consistent with the module’s existing style and accurately describe each
function’s behavior.
In `@lib/crewai/src/crewai/rag/embeddings/providers/oci/embedding_callable.py`:
- Around line 171-181: Review embed_image_batch and the OCI embed_text API to
determine whether multiple IMAGE data URIs can be submitted in one request. If
supported, convert all images first and issue a single request with
input_type="IMAGE"; otherwise retain the per-image calls and add a concise
comment documenting the API’s single-image constraint.
- Around line 154-161: Add explicit error context around each OCI embed_text
call in __call__, identifying the failed batch and preserving the original
exception (or document that retry handling is intentionally delegated to
callers). Ensure failures do not appear as unexplained mid-batch errors and
clarify the behavior in the callable’s docstring if no internal retry is added.
In `@lib/crewai/src/crewai/rag/embeddings/providers/oci/types.py`:
- Around line 8-24: Replace the Annotated metadata on
OCIProviderConfig.model_name with a plain str annotation, and document the
recommended “cohere.embed-english-v3.0” model using a comment or docstring; keep
the actual default defined in OCIProvider.
In `@lib/crewai/tests/llms/oci/test_oci_integration_multimodal.py`:
- Around line 22-42: Extract the shared live OCI configuration and skip logic
from `_skip_unless_live` into a `conftest.py` fixture, preferably reusing or
extending `oci_live_config`. Update `test_oci_integration_multimodal.py`,
`test_oci_integration_tools.py`, and `test_oci_async.py` to consume the fixture
and remove their local helper implementations, preserving the existing
environment validation and returned configuration.
In `@lib/crewai/tests/llms/oci/test_oci_integration_tools.py`:
- Around line 19-39: Remove the duplicated _skip_unless_live and _env_models
helpers from this test module. Reuse the shared OCI live configuration
fixture/helper from conftest.py (including oci_live_config), and replace
_env_models with a parameterized shared utility or direct environment handling
that accepts the relevant variable names and fallback/default values; update all
call sites in the integration tests accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c9176c25-f841-4dfc-a3a3-6089bba2d71d
📒 Files selected for processing (29)
lib/crewai/pyproject.tomllib/crewai/src/crewai/llm.pylib/crewai/src/crewai/llms/providers/oci/__init__.pylib/crewai/src/crewai/llms/providers/oci/completion.pylib/crewai/src/crewai/llms/providers/oci/vision.pylib/crewai/src/crewai/rag/embeddings/factory.pylib/crewai/src/crewai/rag/embeddings/providers/oci/__init__.pylib/crewai/src/crewai/rag/embeddings/providers/oci/embedding_callable.pylib/crewai/src/crewai/rag/embeddings/providers/oci/oci_provider.pylib/crewai/src/crewai/rag/embeddings/providers/oci/types.pylib/crewai/src/crewai/rag/embeddings/types.pylib/crewai/src/crewai/utilities/oci.pylib/crewai/src/crewai/utilities/oci_async.pylib/crewai/tests/llms/oci/__init__.pylib/crewai/tests/llms/oci/conftest.pylib/crewai/tests/llms/oci/test_oci.pylib/crewai/tests/llms/oci/test_oci_async.pylib/crewai/tests/llms/oci/test_oci_cohere_v2.pylib/crewai/tests/llms/oci/test_oci_integration_basic.pylib/crewai/tests/llms/oci/test_oci_integration_multimodal.pylib/crewai/tests/llms/oci/test_oci_integration_streaming.pylib/crewai/tests/llms/oci/test_oci_integration_structured.pylib/crewai/tests/llms/oci/test_oci_integration_tools.pylib/crewai/tests/llms/oci/test_oci_multimodal.pylib/crewai/tests/llms/oci/test_oci_streaming.pylib/crewai/tests/llms/oci/test_oci_structured.pylib/crewai/tests/llms/oci/test_oci_tools.pylib/crewai/tests/rag/embeddings/test_factory_oci.pylib/crewai/tests/rag/embeddings/test_oci_embedding_integration.py
| reasoning_effort: str | None = None, | ||
| stream: bool = False, | ||
| oci_provider: str | None = None, | ||
| timeout: tuple[int, int] = DEFAULT_OCI_TIMEOUT, |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n -C4 'DEFAULT_OCI_TIMEOUT|ClientTimeout|timeout[=:]' \
lib/crewai/src/crewai/llms/providers/oci/completion.py \
lib/crewai/src/crewai/utilities/oci_async.pyRepository: crewAIInc/crewAI
Length of output: 4840
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the relevant class/method structure first.
ast-grep outline lib/crewai/src/crewai/llms/providers/oci/completion.py --view expanded
printf '\n---\n'
ast-grep outline lib/crewai/src/crewai/utilities/oci_async.py --view expanded
printf '\n---\n'
# Read the relevant slices with line numbers.
sed -n '1,240p' lib/crewai/src/crewai/llms/providers/oci/completion.py | cat -n
printf '\n---\n'
sed -n '1,260p' lib/crewai/src/crewai/utilities/oci_async.py | cat -nRepository: crewAIInc/crewAI
Length of output: 22065
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1520,1705p' lib/crewai/src/crewai/llms/providers/oci/completion.py | cat -nRepository: crewAIInc/crewAI
Length of output: 8812
Propagate the configured timeout into the async OCI path. timeout is only forwarded to the sync SDK client; acall() creates OCIAsyncClient and calls chat_async() without it, so native async requests still use the hardcoded 300s default instead of the caller’s connect/read limits.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/crewai/src/crewai/llms/providers/oci/completion.py` at line 85, The
configured timeout is not propagated through the async OCI request path. Update
the OCI provider’s async client setup and `chat_async()` invocation in `acall()`
to pass the `timeout` value, matching the synchronous path and preserving the
caller’s connect/read limits.
| def supports_multimodal(self) -> bool: | ||
| return True |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Report multimodal support per model.
Returning True for every OCI model advertises image support for text-only models, even though vision.py provides VISION_MODELS and is_vision_model. Use model-specific detection, with an explicit override for dedicated endpoints whose capability cannot be inferred.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/crewai/src/crewai/llms/providers/oci/completion.py` around lines 1692 -
1693, The supports_multimodal method currently reports support for every OCI
model; change it to return is_vision_model(self.model), using the existing
VISION_MODELS detection in vision.py. Add an explicit override for dedicated OCI
endpoints whose multimodal capability cannot be inferred from the model name.
| async def _get_session(self) -> aiohttp.ClientSession: | ||
| if self._session is None or self._session.closed: | ||
| ssl_context = ssl.create_default_context(cafile=certifi.where()) | ||
| connector = aiohttp.TCPConnector(ssl=ssl_context) | ||
| self._session = aiohttp.ClientSession(connector=connector) | ||
| return self._session | ||
|
|
||
| async def close(self) -> None: | ||
| if self._session is not None and not self._session.closed: | ||
| await self._session.close() | ||
| self._session = None |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n -C4 '_async_client_instance|_session|aclose\(|\.close\(' \
lib/crewai/src/crewaiRepository: crewAIInc/crewAI
Length of output: 50372
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- lib/crewai/src/crewai/utilities/oci_async.py ---'
sed -n '1,220p' lib/crewai/src/crewai/utilities/oci_async.py
echo
echo '--- lib/crewai/src/crewai/llms/providers/oci/completion.py (async client lifecycle sections) ---'
sed -n '1600,1705p' lib/crewai/src/crewai/llms/providers/oci/completion.py
echo
echo '--- search for OCIAsyncClient.close/aclose/__aexit__/cleanup references ---'
rg -n -C3 'OCIAsyncClient|_async_client_instance|aclose\(|\.close\(\)|__aexit__' lib/crewai/src/crewai/llms/providers/oci lib/crewai/src/crewai/utilities/oci_async.pyRepository: crewAIInc/crewAI
Length of output: 14371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- all _async_client usages in OCI completion ---'
rg -n -C3 '_async_client\b|_async_client_instance\b|OCIAsyncClient\b|async with .*_async_client|await .*_async_client' lib/crewai/src/crewai/llms/providers/oci/completion.py
echo
echo '--- provider cleanup/close methods in OCI completion ---'
rg -n -C4 'def (close|aclose|__aexit__|__del__|cleanup)|async def (close|aclose)|await .*close\(' lib/crewai/src/crewai/llms/providers/oci/completion.py
echo
echo '--- inspect async call paths using the async client ---'
sed -n '1700,1905p' lib/crewai/src/crewai/llms/providers/oci/completion.pyRepository: crewAIInc/crewAI
Length of output: 3154
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- provider/base cleanup hooks ---'
rg -n -C3 'async def aclose|def aclose|async def close|def close|__aexit__|__enter__|__del__' \
lib/crewai/src/crewai/llms lib/crewai/src/crewai/utilities | sed -n '1,260p'
echo
echo '--- OCI completion class header and inheritance ---'
sed -n '1,220p' lib/crewai/src/crewai/llms/providers/oci/completion.py
echo
echo '--- references to provider cleanup in callers ---'
rg -n -C2 'aclose\(\)|close\(\)|async with .*OCI|async with .*completion|context manager' \
lib/crewai/src/crewai | sed -n '1,260p'Repository: crewAIInc/crewAI
Length of output: 31768
Manage the cached aiohttp session per event loop. OCIAsyncClient can close its session, but OCICompletion never tears it down and reuses one cached instance across calls. If that instance crosses event loops, the existing ClientSession stays loop-bound and can fail or leak connectors.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/crewai/src/crewai/utilities/oci_async.py` around lines 79 - 89, Update
OCIAsyncClient session lifecycle management in _get_session and close, and
ensure OCICompletion tears down or recreates the cached client/session per event
loop. Track the owning event loop for the cached aiohttp.ClientSession, close
and clear it when the loop changes or the client is no longer needed, and avoid
reusing loop-bound sessions across calls.
| def _skip_unless_live() -> dict[str, str]: | ||
| compartment = os.getenv("OCI_COMPARTMENT_ID") | ||
| if not compartment: | ||
| pytest.skip("OCI_COMPARTMENT_ID not set") | ||
| region = os.getenv("OCI_REGION") | ||
| if not region: | ||
| pytest.skip("OCI_REGION not set") | ||
| config: dict[str, str] = {"compartment_id": compartment} | ||
| if os.getenv("OCI_AUTH_TYPE"): | ||
| config["auth_type"] = os.getenv("OCI_AUTH_TYPE", "API_KEY") | ||
| if os.getenv("OCI_AUTH_PROFILE"): | ||
| config["auth_profile"] = os.getenv("OCI_AUTH_PROFILE", "DEFAULT") | ||
| return config |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
_skip_unless_live doesn't handle OCI_SERVICE_ENDPOINT, inconsistent with other integration test files.
This function requires OCI_REGION and skips if unset, but test_oci_integration_multimodal.py and test_oci_integration_tools.py accept either OCI_REGION or OCI_SERVICE_ENDPOINT. When only OCI_SERVICE_ENDPOINT is configured, these async tests will be incorrectly skipped. Additionally, service_endpoint is never passed into the config, so even if the env var is set, the OCICompletion instance won't use it.
🔧 Proposed fix to align with other test files
def _skip_unless_live() -> dict[str, str]:
compartment = os.getenv("OCI_COMPARTMENT_ID")
if not compartment:
pytest.skip("OCI_COMPARTMENT_ID not set")
region = os.getenv("OCI_REGION")
- if not region:
- pytest.skip("OCI_REGION not set")
+ endpoint = os.getenv("OCI_SERVICE_ENDPOINT")
+ if not region and not endpoint:
+ pytest.skip("Set OCI_REGION or OCI_SERVICE_ENDPOINT")
config: dict[str, str] = {"compartment_id": compartment}
+ if endpoint:
+ config["service_endpoint"] = endpoint
if os.getenv("OCI_AUTH_TYPE"):
config["auth_type"] = os.getenv("OCI_AUTH_TYPE", "API_KEY")
if os.getenv("OCI_AUTH_PROFILE"):
config["auth_profile"] = os.getenv("OCI_AUTH_PROFILE", "DEFAULT")
return config📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def _skip_unless_live() -> dict[str, str]: | |
| compartment = os.getenv("OCI_COMPARTMENT_ID") | |
| if not compartment: | |
| pytest.skip("OCI_COMPARTMENT_ID not set") | |
| region = os.getenv("OCI_REGION") | |
| if not region: | |
| pytest.skip("OCI_REGION not set") | |
| config: dict[str, str] = {"compartment_id": compartment} | |
| if os.getenv("OCI_AUTH_TYPE"): | |
| config["auth_type"] = os.getenv("OCI_AUTH_TYPE", "API_KEY") | |
| if os.getenv("OCI_AUTH_PROFILE"): | |
| config["auth_profile"] = os.getenv("OCI_AUTH_PROFILE", "DEFAULT") | |
| return config | |
| def _skip_unless_live() -> dict[str, str]: | |
| compartment = os.getenv("OCI_COMPARTMENT_ID") | |
| if not compartment: | |
| pytest.skip("OCI_COMPARTMENT_ID not set") | |
| region = os.getenv("OCI_REGION") | |
| endpoint = os.getenv("OCI_SERVICE_ENDPOINT") | |
| if not region and not endpoint: | |
| pytest.skip("Set OCI_REGION or OCI_SERVICE_ENDPOINT") | |
| config: dict[str, str] = {"compartment_id": compartment} | |
| if endpoint: | |
| config["service_endpoint"] = endpoint | |
| if os.getenv("OCI_AUTH_TYPE"): | |
| config["auth_type"] = os.getenv("OCI_AUTH_TYPE", "API_KEY") | |
| if os.getenv("OCI_AUTH_PROFILE"): | |
| config["auth_profile"] = os.getenv("OCI_AUTH_PROFILE", "DEFAULT") | |
| return config |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/crewai/tests/llms/oci/test_oci_async.py` around lines 12 - 24, Update
_skip_unless_live to accept either OCI_REGION or OCI_SERVICE_ENDPOINT, skipping
only when both are unset. Add the configured OCI_SERVICE_ENDPOINT to the
returned config as service_endpoint so OCICompletion uses it, while preserving
the existing compartment, authentication type, and profile handling.
| result = embedder(["a", "b", "c"]) | ||
|
|
||
| result_rows = [embedding.tolist() for embedding in result] | ||
| expected_rows = [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]] | ||
| assert len(result_rows) == len(expected_rows) | ||
| for actual, expected in zip(result_rows, expected_rows, strict=True): | ||
| assert actual == pytest.approx(expected) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
.tolist() will fail on plain list embeddings.
The mock at line 90 returns embeddings as plain Python lists ([[0.1, 0.2], ...]), not numpy arrays. OCIEmbeddingFunction.__call__ extends the result directly with response.data.embeddings, so result is a list of lists. Calling .tolist() on a plain list raises AttributeError.
🐛 Proposed fix
result = embedder(["a", "b", "c"])
- result_rows = [embedding.tolist() for embedding in result]
+ result_rows = [list(embedding) for embedding in result]
expected_rows = [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| result = embedder(["a", "b", "c"]) | |
| result_rows = [embedding.tolist() for embedding in result] | |
| expected_rows = [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]] | |
| assert len(result_rows) == len(expected_rows) | |
| for actual, expected in zip(result_rows, expected_rows, strict=True): | |
| assert actual == pytest.approx(expected) | |
| result = embedder(["a", "b", "c"]) | |
| result_rows = [list(embedding) for embedding in result] | |
| expected_rows = [[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]] | |
| assert len(result_rows) == len(expected_rows) | |
| for actual, expected in zip(result_rows, expected_rows, strict=True): | |
| assert actual == pytest.approx(expected) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/crewai/tests/rag/embeddings/test_factory_oci.py` around lines 109 - 115,
Remove the `.tolist()` conversion in the result normalization for
`OCIEmbeddingFunction.__call__` tests, since the mocked embeddings are plain
lists; compare `result` directly against the expected nested lists while
retaining the length and strict zip assertions.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/crewai/src/crewai/llms/providers/oci/completion.py (1)
1526-1584: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftCritical gaps remain in
acallnative async path despite "Addressed" marker.The past review (lines 1544-1584) flagged that the native async branch skips
call()semantics and was marked "Addressed." The current code added stop-word trimming, usage tracking, andcall_completedevents, but critical functional gaps remain:
response_modelnot parsed —response_modelis forwarded to_prepare_async_request(line 1546) to include the JSON schema in the request, but the response is returned as a raw string (line 1584). The sync path parses it via_parse_structured_response(lines 1213-1220). Callers ofacall(response_model=SomeModel)will receive astrinstead of aBaseModel.Tool calls not handled — If the model returns tool calls, they are silently ignored. The sync path extracts and recurses via
_handle_tool_calls(lines 1199-1211). The async path has no equivalent, soacallwith tools returns an empty string when the model requests a tool.
last_response_metadatanot set — The sync path setsself.last_response_metadataat line 1193. The async path tracks usage (lines 1567-1575) but never assignsself.last_response_metadata, leaving it stale orNone.Missing lifecycle events and hooks — No
llm_call_context(),_emit_call_started_event,_emit_call_failed_event,_invoke_before_llm_call_hooks,_invoke_after_llm_call_hooks, or try/except error handling. The synccall()has all of these (lines 1478-1524).Usage values not cast to
int— Lines 1571-1573 useusage.get("promptTokens", 0)withoutint(), unlike the streaming path (lines 1000-1002) which casts explicitly. If the API returns string values,_track_token_usage_internalmay fail.Consider refactoring
acallto delegate tocall()viaasyncio.to_threadwhenresponse_modelortoolsare provided (preserving full semantics), and only use the native async path for simple text completions where the gaps are acceptable.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai/src/crewai/llms/providers/oci/completion.py` around lines 1526 - 1584, The native async path in acall does not preserve call() semantics for structured responses, tool calls, metadata, lifecycle hooks, failures, and usage typing. Refactor acall to delegate through asyncio.to_thread(self.call, ...) whenever response_model or tools are provided; otherwise, complete the native path by matching call()’s lifecycle/error handling, setting last_response_metadata, casting usage values to int, and preserving the corresponding events and hooks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@lib/crewai/src/crewai/llms/providers/oci/completion.py`:
- Around line 1526-1584: The native async path in acall does not preserve call()
semantics for structured responses, tool calls, metadata, lifecycle hooks,
failures, and usage typing. Refactor acall to delegate through
asyncio.to_thread(self.call, ...) whenever response_model or tools are provided;
otherwise, complete the native path by matching call()’s lifecycle/error
handling, setting last_response_metadata, casting usage values to int, and
preserving the corresponding events and hooks.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 98947822-856f-4d57-8573-d86178f4f4c0
📒 Files selected for processing (25)
lib/crewai/pyproject.tomllib/crewai/src/crewai/llms/providers/oci/__init__.pylib/crewai/src/crewai/llms/providers/oci/completion.pylib/crewai/src/crewai/llms/providers/oci/vision.pylib/crewai/src/crewai/rag/embeddings/factory.pylib/crewai/src/crewai/rag/embeddings/providers/oci/__init__.pylib/crewai/src/crewai/rag/embeddings/providers/oci/embedding_callable.pylib/crewai/src/crewai/rag/embeddings/providers/oci/oci_provider.pylib/crewai/src/crewai/rag/embeddings/providers/oci/types.pylib/crewai/src/crewai/rag/embeddings/types.pylib/crewai/src/crewai/utilities/oci_async.pylib/crewai/tests/llms/oci/conftest.pylib/crewai/tests/llms/oci/test_oci_async.pylib/crewai/tests/llms/oci/test_oci_cohere_v2.pylib/crewai/tests/llms/oci/test_oci_integration_basic.pylib/crewai/tests/llms/oci/test_oci_integration_multimodal.pylib/crewai/tests/llms/oci/test_oci_integration_streaming.pylib/crewai/tests/llms/oci/test_oci_integration_structured.pylib/crewai/tests/llms/oci/test_oci_integration_tools.pylib/crewai/tests/llms/oci/test_oci_multimodal.pylib/crewai/tests/llms/oci/test_oci_streaming.pylib/crewai/tests/llms/oci/test_oci_structured.pylib/crewai/tests/llms/oci/test_oci_tools.pylib/crewai/tests/rag/embeddings/test_factory_oci.pylib/crewai/tests/rag/embeddings/test_oci_embedding_integration.py
🚧 Files skipped from review as they are similar to previous changes (21)
- lib/crewai/tests/llms/oci/test_oci_integration_basic.py
- lib/crewai/src/crewai/rag/embeddings/providers/oci/init.py
- lib/crewai/tests/llms/oci/test_oci_integration_streaming.py
- lib/crewai/tests/llms/oci/test_oci_cohere_v2.py
- lib/crewai/src/crewai/rag/embeddings/providers/oci/types.py
- lib/crewai/tests/rag/embeddings/test_oci_embedding_integration.py
- lib/crewai/src/crewai/llms/providers/oci/init.py
- lib/crewai/tests/llms/oci/test_oci_integration_structured.py
- lib/crewai/src/crewai/rag/embeddings/providers/oci/oci_provider.py
- lib/crewai/src/crewai/llms/providers/oci/vision.py
- lib/crewai/src/crewai/rag/embeddings/factory.py
- lib/crewai/tests/llms/oci/test_oci_integration_tools.py
- lib/crewai/tests/llms/oci/conftest.py
- lib/crewai/tests/llms/oci/test_oci_integration_multimodal.py
- lib/crewai/src/crewai/rag/embeddings/types.py
- lib/crewai/tests/llms/oci/test_oci_async.py
- lib/crewai/pyproject.toml
- lib/crewai/tests/llms/oci/test_oci_multimodal.py
- lib/crewai/tests/rag/embeddings/test_factory_oci.py
- lib/crewai/src/crewai/rag/embeddings/providers/oci/embedding_callable.py
- lib/crewai/src/crewai/utilities/oci_async.py
Summary
Replace
asyncio.to_threadwrappers with true async I/O foracall()andastream(). The OCI SDK is synchronous, so we bypass it for HTTP and use its signer for request authentication directly viaaiohttp.oci_async.py:OCIAsyncClient— aiohttp-based HTTP client with OCI request signing, native SSE parsing, connection poolingacall(): true async chat completion, no thread poolastream(): true async SSE streaming, no thread+queue bridgeasyncio.to_threadwhen aiohttp is unavailable or the client is mockedaiohttp+certifiadded tocrewai[oci]optional depsTemporary measure until the OCI SDK ships native async. Based on the same pattern used in langchain-oracle.
Depends on #4966, #4964, #4963, #4962, #4961, #4959. Draft until all merge.
Tracking issue: #4944
Diff breakdown (vs embeddings PR)
oci_async.py(new — async client)completion.py(async paths + fallback)pyproject.toml(aiohttp dep)test_oci_async.py(4 live integration tests)Test plan
meta.llama-3.3-70b-instruct:acallreturns text responseastreamyields chunksasyncio.gatherwith 2 simultaneous calls