feat: add OCI Generative AI embeddings provider#4966
Conversation
3f53bcc to
d3e2666
Compare
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
|
OCI GenAI PR series (tracking issue: #4944)
Depends on #4959 (shared auth utilities). |
d3e2666 to
9527ffb
Compare
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
- 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
9527ffb to
f02efa1
Compare
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
|
@greysonlalonde @lorenzejay — bumping for review. Status (after rebase):
This PR adds: Adds 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
f02efa1 to
f5b9555
Compare
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
- 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
f5b9555 to
cfa8d8a
Compare
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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (22)
✅ Files skipped from review due to trivial changes (4)
🚧 Files skipped from review as they are similar to previous changes (14)
📝 WalkthroughWalkthroughChangesOCI support adds optional SDK installation, native LLM routing, chat completion, Cohere variants, multimodal requests, streaming, structured output, tool calling, and OCI text/image embeddings. Mocked and live tests cover the new LLM and embedding flows. OCI Provider
Sequence Diagram(s)sequenceDiagram
participant Client
participant LLM
participant OCICompletion
participant OCIInferenceClient
Client->>LLM: create model with oci prefix
LLM->>OCICompletion: route model to native provider
OCICompletion->>OCIInferenceClient: send chat request
OCIInferenceClient-->>OCICompletion: return response or SSE events
OCICompletion-->>Client: return text, structured output, or tool calls
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.
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
cfa8d8a to
499bcd0
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
499bcd0 to
8267577
Compare
|
Rebased onto current main and un-drafted — mergeable again. Embeddings verified live against OCI on-demand (us-chicago-1): |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (3)
lib/crewai/src/crewai/rag/embeddings/providers/oci/embedding_callable.py (1)
171-181: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider batching image embedding requests.
embed_image_batchsends one image per API call. If the OCIembed_textendpoint accepts multiple inputs withinput_type="IMAGE", batching images into a single request (respectingbatch_size) would significantly reduce latency for large image sets.🤖 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, Update embed_image_batch to group images according to the configured batch_size, convert each group to data URIs, and call _require_client().embed_text once per group with all data URIs and input_type="IMAGE"; accumulate each response’s embeddings in order and preserve the existing return behavior.lib/crewai/src/crewai/rag/embeddings/providers/oci/types.py (1)
11-11: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Annotated[str, "cohere.embed-english-v3.0"]metadata may be misleading.The second argument to
Annotatedis metadata, not a default value. Readers may mistake it for a default. Consider adding a comment or moving the default documentation to the docstring, since the actual default lives inOCIProviderandOCIEmbeddingFunction.📝 Suggested clarification
- model_name: Annotated[str, "cohere.embed-english-v3.0"] + model_name: str # default: "cohere.embed-english-v3.0" (see OCIProvider)🤖 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` at line 11, Clarify the `model_name` declaration in the OCI types definition so the `Annotated` metadata is not mistaken for a default value. Move the default-model documentation into the relevant docstring or add an explicit comment, and reference `OCIProvider` and `OCIEmbeddingFunction` as the locations where the actual default is defined.lib/crewai/tests/llms/oci/conftest.py (1)
182-187: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOptional: Avoid redundant
os.getenvcalls in_skip_unless_live_config.Each auth env var is fetched twice — once in the
ifguard and once in the assignment with an unreachable default. Extracting the value once is cleaner:♻️ Proposed refactor
- 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") - if os.getenv("OCI_AUTH_FILE_LOCATION"): - config["auth_file_location"] = os.getenv("OCI_AUTH_FILE_LOCATION", "~/.oci/config") + auth_type = os.getenv("OCI_AUTH_TYPE") + if auth_type: + config["auth_type"] = auth_type + auth_profile = os.getenv("OCI_AUTH_PROFILE") + if auth_profile: + config["auth_profile"] = auth_profile + auth_file_location = os.getenv("OCI_AUTH_FILE_LOCATION") + if auth_file_location: + config["auth_file_location"] = auth_file_location🤖 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/conftest.py` around lines 182 - 187, In _skip_unless_live_config, avoid fetching each OCI authentication environment variable twice. Store the result of each os.getenv call once, then use that value for the conditional check and corresponding config assignment, removing the unreachable fallback defaults.
🤖 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 1401-1433: Update the async stream facade containing `_producer`
to support cancellation-aware cleanup: use a bounded queue and have the producer
perform backpressured, cancellation-safe queue writes while checking a stop
signal. Wrap consumption in `try/finally` so early iterator closure or task
cancellation signals the producer to stop, unblocks any pending queue operation,
and joins the thread without leaking resources; preserve propagation of producer
exceptions after normal completion.
- Around line 1371-1375: Keep _client_lock held through the entire
response.data.events() consumption in the streaming path, rather than releasing
it immediately after _chat() returns. Refactor the relevant method and inherited
astream()/iter_stream() flow so event iteration occurs within the same lock
context, matching _stream_chat_events() and preventing concurrent use of the
non-reentrant client.
- Around line 1276-1300: The per-event enumerate index in the tool-call
aggregation loop can misassociate continuation chunks. Update
_extract_tool_calls_from_stream_event() to provide each call’s stable OCI call
ID or index, then use that key for tool_calls_by_index.setdefault and all
related state updates instead of the local enumerate index, while preserving
existing ID, function, argument accumulation, and event emission behavior.
In `@lib/crewai/src/crewai/utilities/oci.py`:
- Around line 46-47: Update the SECURITY_TOKEN configuration handling in the OCI
configuration function to validate that both “key_file” and
“security_token_file” are present before accessing them. Raise a descriptive
configuration error identifying the missing required key and explaining that
both entries are needed for security-token authentication, instead of allowing a
bare KeyError.
In `@lib/crewai/tests/llms/oci/test_oci_integration_tools.py`:
- Around line 87-100: Update test_oci_live_tool_call_with_execution to track
whether add_numbers is invoked, such as by recording calls or setting an
invocation flag inside the helper, and assert that the recorded invocation
occurred in addition to validating the result. Keep the existing
available_functions wiring and result assertions.
In `@lib/crewai/tests/rag/embeddings/test_factory_oci.py`:
- Around line 111-115: Remove the `.tolist()` conversion from the result
processing in the test, since the mocked embeddings are already plain Python
lists. Compare `result` directly with `expected_rows` using the existing length
and `pytest.approx` assertions, preserving the behavioral verification without
assuming a NumPy type.
---
Nitpick comments:
In `@lib/crewai/src/crewai/rag/embeddings/providers/oci/embedding_callable.py`:
- Around line 171-181: Update embed_image_batch to group images according to the
configured batch_size, convert each group to data URIs, and call
_require_client().embed_text once per group with all data URIs and
input_type="IMAGE"; accumulate each response’s embeddings in order and preserve
the existing return behavior.
In `@lib/crewai/src/crewai/rag/embeddings/providers/oci/types.py`:
- Line 11: Clarify the `model_name` declaration in the OCI types definition so
the `Annotated` metadata is not mistaken for a default value. Move the
default-model documentation into the relevant docstring or add an explicit
comment, and reference `OCIProvider` and `OCIEmbeddingFunction` as the locations
where the actual default is defined.
In `@lib/crewai/tests/llms/oci/conftest.py`:
- Around line 182-187: In _skip_unless_live_config, avoid fetching each OCI
authentication environment variable twice. Store the result of each os.getenv
call once, then use that value for the conditional check and corresponding
config assignment, removing the unreachable fallback defaults.
🪄 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: b682955a-f6a2-4424-867b-6a2d80d77a6c
📒 Files selected for processing (27)
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/tests/llms/oci/__init__.pylib/crewai/tests/llms/oci/conftest.pylib/crewai/tests/llms/oci/test_oci.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
| stream_tool_calls = self._extract_tool_calls_from_stream_event(event_data) | ||
| for index, tc in enumerate(stream_tool_calls): | ||
| state = tool_calls_by_index.setdefault( | ||
| index, | ||
| {"id": None, "type": "function", "function": {"name": None, "arguments": ""}}, | ||
| ) | ||
| if tc.get("id"): | ||
| state["id"] = tc["id"] | ||
| fn = tc.get("function", {}) | ||
| if fn.get("name"): | ||
| state["function"]["name"] = fn["name"] | ||
| chunk_args = fn.get("arguments") | ||
| if chunk_args: | ||
| state["function"]["arguments"] += str(chunk_args) | ||
| self._emit_stream_chunk_event( | ||
| chunk=str(chunk_args or ""), | ||
| tool_call={"id": state["id"], "type": "function", "function": { | ||
| "name": state["function"]["name"], | ||
| "arguments": str(chunk_args or ""), | ||
| }}, | ||
| from_task=from_task, | ||
| from_agent=from_agent, | ||
| call_type=LLMCallType.TOOL_CALL, | ||
| response_id=response_id, | ||
| ) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
sed -n '1230,1335p' lib/crewai/src/crewai/llms/providers/oci/completion.pyRepository: crewAIInc/crewAI
Length of output: 1955
🏁 Script executed:
sed -n '1180,1365p' lib/crewai/src/crewai/llms/providers/oci/completion.py && printf '\n---\n' && rg -n "_extract_tool_calls_from_stream_event|tool_calls_by_index|_emit_stream_chunk_event" lib/crewai/src/crewai/llms/providers/oci/completion.pyRepository: crewAIInc/crewAI
Length of output: 1955
🌐 Web query:
crewAIInc/crewAI lib/crewai/src/crewai/llms/providers/oci/completion.py _extract_tool_calls_from_stream_event tool_calls_by_index
💡 Result:
The method _extract_tool_calls_from_stream_event in the OCI LLM provider for CrewAI (located at lib/crewai/src/crewai/llms/providers/oci/completion.py) is part of the implementation for handling tool calling in streaming responses [1][2]. As part of the OCI Generative AI integration, the provider needs to aggregate fragmented tool call data received across multiple streaming chunks [2]. The variable tool_calls_by_index is used within this process to group and accumulate these partial tool call fragments (such as IDs, function names, and arguments) by their index [3][4]. This allows the provider to reconstruct complete tool calls from the stream before they are passed to the agent's tool-execution loop [3][2]. This functionality was introduced as part of a series of pull requests adding native Oracle Cloud Infrastructure support to CrewAI, specifically focusing on native function calling support for various model families [2][5]. The implementation follows standard patterns for streaming tool call reconstruction found in agentic frameworks, ensuring that incremental deltas are correctly merged before downstream processing [3][6][4].
Citations:
- 1: feat: add OCI Generative AI provider — basic text completion #4959
- 2: feat: add tool calling support to OCI Generative AI provider #4962
- 3: https://www.stepcodex.com/en/issue/feature-native-oracle-cloud-oci-generative
- 4: [Bug]: MoA _collect_stream drops tool_calls in quiet mode — aggregator tool-call responses cause empty_response_exhausted crash NousResearch/hermes-agent#58437
- 5: [FEATURE] Native Oracle Cloud (OCI) Generative AI Integration #4944
- 6: https://github.com/omnigent-ai/omnigent/blob/main/omnigent/llms/_responses_to_chat.py
🌐 Web query:
OCI Generative AI streaming tool call delta index repeated per event function calling schema
💡 Result:
In Oracle Cloud Infrastructure (OCI) Generative AI streaming, tool call deltas are provided as progressive event fragments [1]. Unlike a static schema, the streaming output structure for function calling involves the model emitting initial chunks containing identification metadata (such as the id and name of the tool) followed by subsequent chunks containing only argument fragments [1]. Key aspects of handling this streaming index include: 1. Tracking Tool Call Consistency: Because OCI may not send the tool call id in every continuation chunk, clients must track the stable tool call index internally [1]. The index is typically anchored by the id when present. If a chunk arrives without an id, the system inherits the current index to ensure argument fragments are appended to the correct tool call object rather than incorrectly incrementing the index [1]. 2. Handling Delta Fragments: To correctly reconstruct the tool call, you should accumulate fragments [1][2]. A common implementation pattern involves using a dictionary or map that links the tool call index or ID to its partially constructed data [3]. 3. Streaming Schema Behavior: - Initial Chunk: Contains the tool call id, function name, and potentially the start of the arguments string. - Continuation Chunks: Contain only the incremental arguments delta [1]. - Finalization: Once the model completes, it emits a finish_reason (e.g., "tool_calls") [1]. If your implementation is seeing fragmented tool call indices (e.g., [1] instead of), it is likely due to the logic failing to recognize that a chunk without an ID belongs to the same tool call as the preceding chunk [1]. You must verify that your streaming processor preserves the index association when the unique ID is absent [1].
Citations:
- 1: fix(oracle): emit tool_calls in streaming responses Portkey-AI/gateway#1537
- 2: https://developers.llamaindex.ai/python/framework-api-reference/llms/oci_genai/
- 3: https://github.com/oracle/langchain-oracle/blob/main/libs/oci/langchain_oci/chat_models/providers/generic.py
Preserve a stable tool-call key here. enumerate() resets for each stream event, so continuation chunks without an id can be attached to the wrong accumulator when OCI emits multiple tool calls. Use the OCI call ID/index from _extract_tool_calls_from_stream_event() instead of the per-event loop index.
🤖 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 1276 -
1300, The per-event enumerate index in the tool-call aggregation loop can
misassociate continuation chunks. Update _extract_tool_calls_from_stream_event()
to provide each call’s stable OCI call ID or index, then use that key for
tool_calls_by_index.setdefault and all related state updates instead of the
local enumerate index, while preserving existing ID, function, argument
accumulation, and event emission behavior.
| response = self._chat(chat_details) | ||
| usage_data: dict[str, int] = {} | ||
| response_metadata: dict[str, Any] = {} | ||
|
|
||
| for event in response.data.events(): |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Keep the client lock while consuming iter_stream events.
Line 1371 releases _client_lock when _chat() returns, then Line 1375 iterates the stream unlocked. This bypasses _stream_chat_events() and races concurrent requests despite the client being documented as non-reentrant. astream() inherits the same issue through iter_stream().
Proposed fix
- response = self._chat(chat_details)
usage_data: dict[str, int] = {}
response_metadata: dict[str, Any] = {}
- for event in response.data.events():
+ for event in self._stream_chat_events(chat_details):📝 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.
| response = self._chat(chat_details) | |
| usage_data: dict[str, int] = {} | |
| response_metadata: dict[str, Any] = {} | |
| for event in response.data.events(): | |
| usage_data: dict[str, int] = {} | |
| response_metadata: dict[str, Any] = {} | |
| for event in self._stream_chat_events(chat_details): |
🤖 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 1371 -
1375, Keep _client_lock held through the entire response.data.events()
consumption in the streaming path, rather than releasing it immediately after
_chat() returns. Refactor the relevant method and inherited
astream()/iter_stream() flow so event iteration occurs within the same lock
context, matching _stream_chat_events() and preventing concurrent use of the
non-reentrant client.
| """Expose the sync OCI SSE stream through an async generator facade.""" | ||
| loop = asyncio.get_running_loop() | ||
| queue: asyncio.Queue[str | None] = asyncio.Queue() | ||
| error_holder: list[BaseException] = [] | ||
|
|
||
| def _producer() -> None: | ||
| try: | ||
| for chunk in self.iter_stream( | ||
| messages=messages, | ||
| tools=tools, | ||
| callbacks=callbacks, | ||
| available_functions=available_functions, | ||
| from_task=from_task, | ||
| from_agent=from_agent, | ||
| ): | ||
| loop.call_soon_threadsafe(queue.put_nowait, chunk) | ||
| except BaseException as error: | ||
| error_holder.append(error) | ||
| finally: | ||
| loop.call_soon_threadsafe(queue.put_nowait, None) | ||
|
|
||
| thread = threading.Thread(target=_producer, daemon=True) | ||
| thread.start() | ||
|
|
||
| while True: | ||
| chunk = await queue.get() | ||
| if chunk is None: | ||
| break | ||
| yield chunk | ||
|
|
||
| thread.join() | ||
| if error_holder: | ||
| raise error_holder[0] |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Cancel the producer when the async iterator is closed.
If an async for consumer exits early or is cancelled, the daemon thread continues reading OCI events and appending to the unbounded queue. Long-lived streams can leak a thread and unbounded memory. Add cancellation-aware cleanup plus bounded, backpressured queue writes.
🤖 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 1401 -
1433, Update the async stream facade containing `_producer` to support
cancellation-aware cleanup: use a bounded queue and have the producer perform
backpressured, cancellation-safe queue writes while checking a stop signal. Wrap
consumption in `try/finally` so early iterator closure or task cancellation
signals the producer to stop, unblocks any pending queue operation, and joins
the thread without leaking resources; preserve propagation of producer
exceptions after normal completion.
| key_file = config["key_file"] | ||
| security_token_file = config["security_token_file"] |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Add validation for required SECURITY_TOKEN config keys.
Direct config["key_file"] / config["security_token_file"] access raises a bare KeyError if the OCI profile is missing these entries, giving the user no actionable guidance. A descriptive error would significantly improve the debugging experience for misconfigured security-token auth.
🛡️ Proposed fix
config = oci.config.from_file(
file_location=auth_file_location,
profile_name=auth_profile,
)
- key_file = config["key_file"]
- security_token_file = config["security_token_file"]
+ key_file = config.get("key_file")
+ security_token_file = config.get("security_token_file")
+ if not key_file or not security_token_file:
+ raise ValueError(
+ "SECURITY_TOKEN auth requires 'key_file' and 'security_token_file' "
+ f"in OCI config profile '{auth_profile}'"
+ )
private_key = oci.signer.load_private_key_from_file(key_file, None)📝 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.
| key_file = config["key_file"] | |
| security_token_file = config["security_token_file"] | |
| key_file = config.get("key_file") | |
| security_token_file = config.get("security_token_file") | |
| if not key_file or not security_token_file: | |
| raise ValueError( | |
| "SECURITY_TOKEN auth requires 'key_file' and 'security_token_file' " | |
| f"in OCI config profile '{auth_profile}'" | |
| ) |
🤖 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.py` around lines 46 - 47, Update the
SECURITY_TOKEN configuration handling in the OCI configuration function to
validate that both “key_file” and “security_token_file” are present before
accessing them. Raise a descriptive configuration error identifying the missing
required key and explaining that both entries are needed for security-token
authentication, instead of allowing a bare KeyError.
| def test_oci_live_tool_call_with_execution(oci_tool_model: str, oci_tool_config: dict): | ||
| """With available_functions, tools should execute and model should respond.""" | ||
| def add_numbers(a: float, b: float) -> str: | ||
| return str(float(a) + float(b)) | ||
|
|
||
| llm = OCICompletion(model=oci_tool_model, **oci_tool_config) | ||
| result = llm.call( | ||
| messages=[{"role": "user", "content": "What is 3 + 7? Use the add_numbers tool."}], | ||
| tools=TOOL_SPEC, | ||
| available_functions={"add_numbers": add_numbers}, | ||
| ) | ||
|
|
||
| assert isinstance(result, str) | ||
| assert "10" in result |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert that the supplied tool actually ran.
The model can return "10" without calling add_numbers, so this test can pass if tool execution regresses. Record the invocation and assert it occurred.
Proposed fix
def test_oci_live_tool_call_with_execution(oci_tool_model: str, oci_tool_config: dict):
"""With available_functions, tools should execute and model should respond."""
+ calls = []
+
def add_numbers(a: float, b: float) -> str:
+ calls.append((a, b))
return str(float(a) + float(b))
...
assert isinstance(result, str)
+ assert calls == [(3, 7)]
assert "10" in resultAs per coding guidelines, “Write unit tests for new functionality that focus on behavior rather than implementation details.”
📝 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 test_oci_live_tool_call_with_execution(oci_tool_model: str, oci_tool_config: dict): | |
| """With available_functions, tools should execute and model should respond.""" | |
| def add_numbers(a: float, b: float) -> str: | |
| return str(float(a) + float(b)) | |
| llm = OCICompletion(model=oci_tool_model, **oci_tool_config) | |
| result = llm.call( | |
| messages=[{"role": "user", "content": "What is 3 + 7? Use the add_numbers tool."}], | |
| tools=TOOL_SPEC, | |
| available_functions={"add_numbers": add_numbers}, | |
| ) | |
| assert isinstance(result, str) | |
| assert "10" in result | |
| def test_oci_live_tool_call_with_execution(oci_tool_model: str, oci_tool_config: dict): | |
| """With available_functions, tools should execute and model should respond.""" | |
| calls = [] | |
| def add_numbers(a: float, b: float) -> str: | |
| calls.append((a, b)) | |
| return str(float(a) + float(b)) | |
| llm = OCICompletion(model=oci_tool_model, **oci_tool_config) | |
| result = llm.call( | |
| messages=[{"role": "user", "content": "What is 3 + 7? Use the add_numbers tool."}], | |
| tools=TOOL_SPEC, | |
| available_functions={"add_numbers": add_numbers}, | |
| ) | |
| assert isinstance(result, str) | |
| assert calls == [(3, 7)] | |
| assert "10" in result |
🤖 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 87 -
100, Update test_oci_live_tool_call_with_execution to track whether add_numbers
is invoked, such as by recording calls or setting an invocation flag inside the
helper, and assert that the recorded invocation occurred in addition to
validating the result. Keep the existing available_functions wiring and result
assertions.
Source: Coding guidelines
| 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 raise AttributeError on plain Python lists.
The mock returns embeddings as plain lists ([[0.1, 0.2], [0.3, 0.4], ...]), not numpy arrays. After __call__ extends the results, result is a list of lists. Calling .tolist() on a plain list raises AttributeError: 'list' object has no attribute 'tolist'.
As per coding guidelines, tests should focus on behavior rather than implementation details — the .tolist() call assumes a numpy type that isn't present.
🐛 Proposed fix
- result_rows = [embedding.tolist() for embedding in result]
+ result_rows = [list(embedding) for embedding in result]📝 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_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_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 111 - 115,
Remove the `.tolist()` conversion from the result processing in the test, since
the mocked embeddings are already plain Python lists. Compare `result` directly
with `expected_rows` using the existing length and `pytest.approx` assertions,
preserving the behavioral verification without assuming a NumPy type.
Source: Coding guidelines
Summary
OCIEmbeddingFunction: ChromaDB-compatible embedding callable with batching, config serialization, and image embedding supportOCIProvider: Pydantic-based provider withAliasChoicesfor env var and config key validationembeddings/factory.py+types.py(provider name:"oci")utilities/oci.py(from PR 1)Depends on #4964, #4963, #4962, #4961, #4959. Draft until all merge.
Tracking issue: #4944
Diff breakdown (vs multimodal PR)
embedding_callable.py(new)oci_provider.py(new)types.py(new)__init__.py(new)factory.py(registration + overload)types.py(ProviderSpec + Literal)test_factory_oci.py(5 unit tests)test_oci_embedding_integration.py(2 live tests)Test plan
cohere.embed-english-v3.0(API_KEY_AUTH): single text + batch