feat: add structured output support to OCI Generative AI provider#4963
feat: add structured output support to OCI Generative AI provider#4963fede-kamel wants to merge 9 commits into
Conversation
8858bef to
a9797b3
Compare
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
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
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
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
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
|
OCI GenAI PR series (tracking issue: #4944)
Depends on #4962. |
a9797b3 to
f3beadc
Compare
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
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
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
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
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
- 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
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
f3beadc to
1ce007a
Compare
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
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
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
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
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
- 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
2001c30 to
ac8a1f7
Compare
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
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
|
Warning Review limit reached
Next review available in: 48 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (9)
📝 WalkthroughWalkthroughAdds OCI as a native CrewAI LLM provider, including optional dependency registration, model routing, authentication, generic and Cohere request handling, streaming, structured outputs, tool execution, and unit/live integration coverage. ChangesOCI provider integration
Sequence Diagram(s)sequenceDiagram
participant Caller
participant LLM
participant OCICompletion
participant OCI
participant Tool
Caller->>LLM: provide OCI model
LLM->>OCICompletion: route request
OCICompletion->>OCI: send chat request
OCI-->>OCICompletion: response or stream events
OCICompletion->>Tool: execute tool call
Tool-->>OCICompletion: tool result
OCICompletion-->>Caller: final text, chunks, structured model, or tool calls
🚥 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 |
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
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
|
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
ac8a1f7 to
dc1d0bb
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
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
dc1d0bb to
3fc19ce
Compare
|
Rebased onto current main and un-drafted — mergeable again. Structured output verified live against OCI on-demand (us-chicago-1) with a Pydantic response model on |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
lib/crewai/tests/llms/oci/test_oci_streaming.py (1)
10-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMove
import jsonto module level.
import jsoninside_make_fake_stream_eventshould be at the top of the file per PEP 8 conventions.♻️ Proposed refactor
"""Unit tests for OCI provider streaming (mocked SDK).""" from __future__ import annotations +import json from unittest.mock import MagicMock import pytest def _make_fake_stream_event(text: str = "", finish_reason: str | None = None, usage: dict | None = None) -> MagicMock: """Build a single SSE event with optional text, finish, and usage.""" payload: dict = {} if text: payload["message"] = {"content": [{"text": text}]} if finish_reason: payload["finishReason"] = finish_reason if usage: payload["usage"] = usage - import json event = MagicMock() event.data = json.dumps(payload) return event🤖 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_streaming.py` around lines 10 - 23, Move the local json import out of _make_fake_stream_event and add it with the module-level imports at the top of the test file, leaving the helper to use the module-level json reference.lib/crewai/tests/llms/oci/test_oci_integration_tools.py (1)
24-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReuse conftest's
oci_live_configfixture instead of duplicating_skip_unless_live().The
_skip_unless_live()function duplicates the conftest's_skip_unless_live_config()logic (env var checks, config dict construction). Since the conftest already provides theoci_live_configfixture, the tools test can reuse it directly and only define its ownoci_tool_modelfixture forOCI_TEST_TOOL_MODELS. This eliminates duplication and would also fix the missingregionissue if the conftest includes it.♻️ Proposed refactor: reuse conftest fixtures
-def _skip_unless_live(): - 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 - - TOOL_SPEC = [-@pytest.fixture() -def oci_tool_config(): - return _skip_unless_live() - - def test_oci_live_tool_call_returns_raw(oci_tool_model: str, oci_tool_config: dict): - llm = OCICompletion(model=oci_tool_model, **oci_tool_config) + llm = OCICompletion(model=oci_tool_model, **oci_live_config)def test_oci_live_tool_call_with_execution(oci_tool_model: str, oci_tool_config: dict): - llm = OCICompletion(model=oci_tool_model, **oci_tool_config) + llm = OCICompletion(model=oci_tool_model, **oci_live_config)Then update both test signatures to accept
oci_live_configinstead ofoci_tool_config, and remove theosimport if no longer needed.Also applies to: 69-72
🤖 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 24 - 39, Remove the duplicated _skip_unless_live() helper and reuse the conftest-provided oci_live_config fixture, retaining only the oci_tool_model fixture for OCI_TEST_TOOL_MODELS. Update both affected tests to accept oci_live_config instead of oci_tool_config, pass that configuration to the tool setup, and remove the unused os import.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 1134-1214: Update iter_stream and astream to preserve the behavior
of _stream_call_impl: either implement handling for tools, callbacks,
available_functions, from_task, and from_agent, including relevant events and
tool recursion, or explicitly reject unsupported arguments instead of silently
discarding them. Ensure the OCI response event iteration in iter_stream executes
under the same _client_lock used by other OCI client calls, including safe lock
handling across the synchronous producer thread.
- Around line 1056-1081: Preserve each streamed tool call’s payload index
instead of using the per-event position from enumerate(stream_tool_calls).
Update _extract_tool_calls_from_stream_event to return the OCI/OpenAI-compatible
index, then use that index as the key for tool_calls_by_index and retain it when
emitting _emit_stream_chunk_event data.
---
Nitpick comments:
In `@lib/crewai/tests/llms/oci/test_oci_integration_tools.py`:
- Around line 24-39: Remove the duplicated _skip_unless_live() helper and reuse
the conftest-provided oci_live_config fixture, retaining only the oci_tool_model
fixture for OCI_TEST_TOOL_MODELS. Update both affected tests to accept
oci_live_config instead of oci_tool_config, pass that configuration to the tool
setup, and remove the unused os import.
In `@lib/crewai/tests/llms/oci/test_oci_streaming.py`:
- Around line 10-23: Move the local json import out of _make_fake_stream_event
and add it with the module-level imports at the top of the test file, leaving
the helper to use the module-level json reference.
🪄 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: ae3d4c73-2257-48f4-a26e-fb245f99ebf9
📒 Files selected for processing (15)
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/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_integration_basic.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_streaming.pylib/crewai/tests/llms/oci/test_oci_structured.pylib/crewai/tests/llms/oci/test_oci_tools.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.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Does the OCI Generative AI streaming chat response include an explicit index field for toolCalls deltas, or only positional array entries?
💡 Result:
The OCI Generative AI streaming chat response structure—particularly for OpenAI-compatible endpoints—does not natively include an explicit, persistent index field within the toolCalls delta object itself in the way some other providers might structured it [1]. Instead, OCI streaming chunks often bundle toolCalls data [1]. When integrating with streaming tools on OCI, developers frequently need to implement a client-side layer to manage indices [1]. For instance, integration layers (like those used in AI gateways) must often track and "anchor" the tool_calls[].index value across consecutive streaming chunks to ensure consistency with the OpenAI wire format [1]. Without this client-side transformation, raw OCI streaming responses may lack the explicit index required to correctly map and assemble multi-part tool call deltas [1]. While OCI Generative AI's static (non-streaming) model objects and Python SDK models define ToolCall and FunctionCall structures with explicit id and type fields [2][3], these definitions apply to the final message content, not the ephemeral streaming deltas [1][2]. Therefore, when handling streaming, the responsibility for generating or maintaining the positional index for toolCalls deltas generally falls to the client implementation to match the expected API specifications [1].
Citations:
- 1: fix(oracle): emit tool_calls in streaming responses Portkey-AI/gateway#1537
- 2: https://docs.oracle.com/en-us/iaas/tools/python/latest/api/generative_ai_inference/models/oci.generative_ai_inference.models.FunctionCall.html
- 3: https://docs.oracle.com/en-us/iaas/tools/python/latest/api/generative_ai_inference/models/oci.generative_ai_inference.models.ToolCall.html
🏁 Script executed:
python3 - <<'PY'
from pathlib import Path
p = Path('lib/crewai/src/crewai/llms/providers/oci/completion.py')
text = p.read_text()
for needle in ['def _extract_tool_calls_from_stream_event', 'toolCalls', 'tool_calls_by_index', 'index']:
print(f'\n=== {needle} ===')
for i, line in enumerate(text.splitlines(), 1):
if needle in line:
start = max(1, i-20)
end = min(len(text.splitlines()), i+40)
for j in range(start, end+1):
print(f"{j}: {text.splitlines()[j-1]}")
break
PYRepository: crewAIInc/crewAI
Length of output: 11889
🏁 Script executed:
python3 - <<'PY'
from pathlib import Path
p = Path('lib/crewai/src/crewai/llms/providers/oci/completion.py')
lines = p.read_text().splitlines()
needles = [
'def _extract_tool_calls_from_stream_event',
'toolCalls',
'tool_calls_by_index',
'enumerate(stream_tool_calls)',
]
for needle in needles:
print(f'\n=== {needle} ===')
found = False
for i, line in enumerate(lines, 1):
if needle in line:
found = True
start = max(1, i - 20)
end = min(len(lines), i + 40)
for j in range(start, end + 1):
print(f"{j}: {lines[j-1]}")
break
if not found:
print('not found')
PYRepository: crewAIInc/crewAI
Length of output: 11770
🌐 Web query:
OCI Generative AI streaming toolCalls payload index field delta array stable slot OpenAI compatible
💡 Result:
The Oracle Cloud Infrastructure (OCI) Generative AI service provides an OpenAI-compatible API for streaming responses [1][2][3]. When performing streaming tool calls, the API follows the standard OpenAI response format where tool calls are emitted as a sequence of deltas [4][5]. In the streaming response structure, the tool_calls field within the delta object utilizes an index field to identify which specific tool call is being updated [4][5]. This index is essential for handling multiple parallel tool calls, allowing clients to correctly assemble the complete function name and arguments by matching incoming chunks to the same tool call instance [4]. Key technical aspects include: 1. Payload Structure: During streaming, the choices[].delta field contains a tool_calls array [4]. Each element in this array includes an index (an integer) and a function object containing the name and arguments (partial) [4][5]. 2. Stability and Mapping: To ensure compatibility with standard OpenAI client libraries and frameworks (like LangChain or OpenAI SDK), middleware or client-side logic must track these indices to maintain stability across streamed chunks [4]. If a provider omits the id or if multiple calls are initiated, the index serves as the primary mechanism for associating subsequent streaming deltas with the correct, ongoing tool call [6][4]. 3. OpenAI Compatibility: While OCI Generative AI offers OpenAI-compatible endpoints, the underlying model responses may vary (e.g., some models might omit the id field) [7][4]. Consequently, integration layers (like those found in LangChain or gateway services) often implement logic to normalize these payloads, ensuring that tool_calls are emitted in a consistent format that includes the required index for proper assembly [6][4]. In summary, the index field within the tool_calls delta array is the standard, stable slot used to correlate streaming chunks with their respective tool call operations in the OCI Generative AI OpenAI-compatible interface [4][5].
Citations:
- 1: https://docs.oracle.com/en-us/iaas/Content/generative-ai/openai-compatible-api.htm
- 2: https://docs.oracle.com/en-us/iaas/Content/generative-ai/responses-api.htm
- 3: https://docs.oracle.com/en-us/iaas/Content/generative-ai/oci-openai.htm
- 4: fix(oracle): emit tool_calls in streaming responses Portkey-AI/gateway#1537
- 5: https://developers.openai.com/api/docs/guides/function-calling
- 6: https://github.com/oracle/langchain-oracle/blob/main/libs/oci/langchain_oci/chat_models/providers/generic.py
- 7: [Bug] Missing 'id' in toolCalls from Google Gemini models, causing errors with multiple tool calls oracle/oci-typescript-sdk#465
Preserve the streamed tool-call index here. enumerate(stream_tool_calls) keys state by per-event list position, but OCI/OpenAI-compatible tool-call deltas are indexed in the payload. If a stream emits only the updated delta or the array order shifts, arguments can be merged into the wrong tool_calls_by_index entry. Thread the payload index through _extract_tool_calls_from_stream_event and key on that instead.
🤖 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 1056 -
1081, Preserve each streamed tool call’s payload index instead of using the
per-event position from enumerate(stream_tool_calls). Update
_extract_tool_calls_from_stream_event to return the OCI/OpenAI-compatible index,
then use that index as the key for tool_calls_by_index and retain it when
emitting _emit_stream_chunk_event data.
| def iter_stream( | ||
| self, | ||
| messages: str | list[LLMMessage], | ||
| tools: list[dict[str, BaseTool]] | None = None, | ||
| callbacks: list[Any] | None = None, | ||
| available_functions: dict[str, Any] | None = None, | ||
| from_task: Task | None = None, | ||
| from_agent: Agent | None = None, | ||
| ) -> Any: | ||
| """Yield raw text chunks from OCI without triggering tool recursion.""" | ||
| normalized_messages = self._normalize_messages(messages) | ||
| chat_request = self._build_chat_request(normalized_messages, is_stream=True) | ||
| chat_details = self._oci.generative_ai_inference.models.ChatDetails( | ||
| compartment_id=self.compartment_id, | ||
| serving_mode=self._build_serving_mode(), | ||
| chat_request=chat_request, | ||
| ) | ||
| response = self._chat(chat_details) | ||
| usage_data: dict[str, int] = {} | ||
| response_metadata: dict[str, Any] = {} | ||
|
|
||
| for event in response.data.events(): | ||
| event_data = self._parse_stream_event(event) | ||
| if not event_data: | ||
| continue | ||
| text_chunk = self._extract_text_from_stream_event(event_data) | ||
| if text_chunk: | ||
| yield text_chunk | ||
| usage_chunk = self._extract_usage_from_stream_event(event_data) | ||
| if usage_chunk: | ||
| usage_data = usage_chunk | ||
| response_metadata.update(self._extract_metadata_from_stream_event(event_data)) | ||
|
|
||
| if usage_data: | ||
| self._track_token_usage_internal(usage_data) | ||
| response_metadata["usage"] = usage_data | ||
| self.last_response_metadata = response_metadata or None | ||
|
|
||
| async def astream( | ||
| self, | ||
| messages: str | list[LLMMessage], | ||
| tools: list[dict[str, BaseTool]] | None = None, | ||
| callbacks: list[Any] | None = None, | ||
| available_functions: dict[str, Any] | None = None, | ||
| from_task: Task | None = None, | ||
| from_agent: Agent | None = None, | ||
| ) -> Any: | ||
| """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.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
ast-grep outline lib/crewai/src/crewai/llms/providers/oci/completion.py --view expandedRepository: crewAIInc/crewAI
Length of output: 2637
🏁 Script executed:
sed -n '515,610p;948,1133p;1134,1325p' lib/crewai/src/crewai/llms/providers/oci/completion.pyRepository: crewAIInc/crewAI
Length of output: 19552
🏁 Script executed:
rg -n "iter_stream\(" lib/crewai/src testsRepository: crewAIInc/crewAI
Length of output: 388
iter_stream should match _stream_call_impl or explicitly reject unsupported args.
tools,callbacks,available_functions,from_task, andfrom_agentare dropped here, soastream()bypasses tool handling and call/stream events without warning.response.data.events()also runs outside_client_lock; the OCI client is treated as non-reentrant elsewhere, so this path reopens the concurrency hazard.
🤖 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 1134 -
1214, Update iter_stream and astream to preserve the behavior of
_stream_call_impl: either implement handling for tools, callbacks,
available_functions, from_task, and from_agent, including relevant events and
tool recursion, or explicitly reject unsupported arguments instead of silently
discarding them. Ensure the OCI response event iteration in iter_stream executes
under the same _client_lock used by other OCI client calls, including safe lock
handling across the synchronous producer thread.
Summary
response_model(Pydantic BaseModel) support for structured JSON output_build_response_format: converts Pydantic schema → OCIJsonSchemaResponseFormat(generic) orCohereResponseJsonFormat(Cohere)_parse_structured_response: validates model output and returns typed Pydantic instanceresponse_modelthreaded throughcall()→_call_impl/_stream_call_impl→_handle_tool_callsfor full coverage_validate_structured_outputDepends on #4962, #4961, #4959. Draft until all merge.
Tracking issue: #4944
Diff breakdown (vs tool calling PR)
completion.py(structured output methods + threading)test_oci_structured.py(5 unit tests)test_oci_integration_structured.py(1 parametrized test)Test plan
meta.llama-3.3-70b-instructandgoogle.gemini-2.5-flash