Merge branch 'main' into gl/fix/output-parser-exception-retry-logic

# Conflicts:
#	lib/crewai/src/crewai/llms/providers/anthropic/completion.py
This commit is contained in:
Greyson Lalonde
2025-11-05 11:58:49 -05:00
29 changed files with 3156 additions and 1362 deletions

View File

@@ -1200,6 +1200,52 @@ Learn how to get the most out of your LLM configuration:
)
```
</Accordion>
<Accordion title="Transport Interceptors">
CrewAI provides message interceptors for several providers, allowing you to hook into request/response cycles at the transport layer.
**Supported Providers:**
- ✅ OpenAI
- ✅ Anthropic
**Basic Usage:**
```python
import httpx
from crewai import LLM
from crewai.llms.hooks import BaseInterceptor
class CustomInterceptor(BaseInterceptor[httpx.Request, httpx.Response]):
"""Custom interceptor to modify requests and responses."""
def on_outbound(self, request: httpx.Request) -> httpx.Request:
"""Print request before sending to the LLM provider."""
print(request)
return request
def on_inbound(self, response: httpx.Response) -> httpx.Response:
"""Process response after receiving from the LLM provider."""
print(f"Status: {response.status_code}")
print(f"Response time: {response.elapsed}")
return response
# Use the interceptor with an LLM
llm = LLM(
model="openai/gpt-4o",
interceptor=CustomInterceptor()
)
```
**Important Notes:**
- Both methods must return the received object or type of object.
- Modifying received objects may result in unexpected behavior or application crashes.
- Not all providers support interceptors - check the supported providers list above
<Info>
Interceptors operate at the transport layer. This is particularly useful for:
- Message transformation and filtering
- Debugging API interactions
</Info>
</Accordion>
</AccordionGroup>
## Common Issues and Solutions

View File

@@ -20,6 +20,7 @@ from typing import (
)
from dotenv import load_dotenv
import httpx
from pydantic import BaseModel, Field
from typing_extensions import Self
@@ -53,6 +54,7 @@ if TYPE_CHECKING:
from litellm.utils import supports_response_schema
from crewai.agent.core import Agent
from crewai.llms.hooks.base import BaseInterceptor
from crewai.task import Task
from crewai.tools.base_tool import BaseTool
from crewai.utilities.types import LLMMessage
@@ -334,6 +336,8 @@ class LLM(BaseLLM):
return cast(
Self, native_class(model=model_string, provider=provider, **kwargs)
)
except NotImplementedError:
raise
except Exception as e:
raise ImportError(f"Error importing native provider: {e}") from e
@@ -403,6 +407,7 @@ class LLM(BaseLLM):
callbacks: list[Any] | None = None,
reasoning_effort: Literal["none", "low", "medium", "high"] | None = None,
stream: bool = False,
interceptor: BaseInterceptor[httpx.Request, httpx.Response] | None = None,
**kwargs: Any,
) -> None:
"""Initialize LLM instance.
@@ -442,6 +447,7 @@ class LLM(BaseLLM):
self.additional_params = kwargs
self.is_anthropic = self._is_anthropic_model(model)
self.stream = stream
self.interceptor = interceptor
litellm.drop_params = True

View File

@@ -0,0 +1,6 @@
"""Interceptor contracts for crewai"""
from crewai.llms.hooks.base import BaseInterceptor
__all__ = ["BaseInterceptor"]

View File

@@ -0,0 +1,82 @@
"""Base classes for LLM transport interceptors.
This module provides abstract base classes for intercepting and modifying
outbound and inbound messages at the transport level.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U")
class BaseInterceptor(ABC, Generic[T, U]):
"""Abstract base class for intercepting transport-level messages.
Provides hooks to intercept and modify outbound and inbound messages
at the transport layer.
Type parameters:
T: Outbound message type (e.g., httpx.Request)
U: Inbound message type (e.g., httpx.Response)
Example:
>>> class CustomInterceptor(BaseInterceptor[httpx.Request, httpx.Response]):
... def on_outbound(self, message: httpx.Request) -> httpx.Request:
... message.headers["X-Custom-Header"] = "value"
... return message
...
... def on_inbound(self, message: httpx.Response) -> httpx.Response:
... print(f"Status: {message.status_code}")
... return message
"""
@abstractmethod
def on_outbound(self, message: T) -> T:
"""Intercept outbound message before sending.
Args:
message: Outbound message object.
Returns:
Modified message object.
"""
...
@abstractmethod
def on_inbound(self, message: U) -> U:
"""Intercept inbound message after receiving.
Args:
message: Inbound message object.
Returns:
Modified message object.
"""
...
async def aon_outbound(self, message: T) -> T:
"""Async version of on_outbound.
Args:
message: Outbound message object.
Returns:
Modified message object.
"""
raise NotImplementedError
async def aon_inbound(self, message: U) -> U:
"""Async version of on_inbound.
Args:
message: Inbound message object.
Returns:
Modified message object.
"""
raise NotImplementedError

View File

@@ -0,0 +1,87 @@
"""HTTP transport implementations for LLM request/response interception.
This module provides internal transport classes that integrate with BaseInterceptor
to enable request/response modification at the transport level.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import httpx
if TYPE_CHECKING:
from crewai.llms.hooks.base import BaseInterceptor
class HTTPTransport(httpx.HTTPTransport):
"""HTTP transport that uses an interceptor for request/response modification.
This transport is used internally when a user provides a BaseInterceptor.
Users should not instantiate this class directly - instead, pass an interceptor
to the LLM client and this transport will be created automatically.
"""
def __init__(
self,
interceptor: BaseInterceptor[httpx.Request, httpx.Response],
**kwargs: Any,
) -> None:
"""Initialize transport with interceptor.
Args:
interceptor: HTTP interceptor for modifying raw request/response objects.
**kwargs: Additional arguments passed to httpx.HTTPTransport.
"""
super().__init__(**kwargs)
self.interceptor = interceptor
def handle_request(self, request: httpx.Request) -> httpx.Response:
"""Handle request with interception.
Args:
request: The HTTP request to handle.
Returns:
The HTTP response.
"""
request = self.interceptor.on_outbound(request)
response = super().handle_request(request)
return self.interceptor.on_inbound(response)
class AsyncHTTPransport(httpx.AsyncHTTPTransport):
"""Async HTTP transport that uses an interceptor for request/response modification.
This transport is used internally when a user provides a BaseInterceptor.
Users should not instantiate this class directly - instead, pass an interceptor
to the LLM client and this transport will be created automatically.
"""
def __init__(
self,
interceptor: BaseInterceptor[httpx.Request, httpx.Response],
**kwargs: Any,
) -> None:
"""Initialize async transport with interceptor.
Args:
interceptor: HTTP interceptor for modifying raw request/response objects.
**kwargs: Additional arguments passed to httpx.AsyncHTTPTransport.
"""
super().__init__(**kwargs)
self.interceptor = interceptor
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
"""Handle async request with interception.
Args:
request: The HTTP request to handle.
Returns:
The HTTP response.
"""
request = await self.interceptor.aon_outbound(request)
response = await super().handle_async_request(request)
return await self.interceptor.aon_inbound(response)

View File

@@ -2,12 +2,13 @@ from __future__ import annotations
import logging
import os
from typing import Any, cast
from typing import TYPE_CHECKING, Any, cast
from pydantic import BaseModel
from crewai.events.types.llm_events import LLMCallType
from crewai.llms.base_llm import BaseLLM
from crewai.llms.hooks.transport import HTTPTransport
from crewai.utilities.agent_utils import is_context_length_exceeded
from crewai.utilities.exceptions.context_window_exceeding_exception import (
LLMContextLengthExceededError,
@@ -15,10 +16,14 @@ from crewai.utilities.exceptions.context_window_exceeding_exception import (
from crewai.utilities.types import LLMMessage
if TYPE_CHECKING:
from crewai.llms.hooks.base import BaseInterceptor
try:
from anthropic import Anthropic
from anthropic.types import Message
from anthropic.types.tool_use_block import ToolUseBlock
import httpx
except ImportError:
raise ImportError(
'Anthropic native provider not available, to install: uv add "crewai[anthropic]"'
@@ -45,6 +50,7 @@ class AnthropicCompletion(BaseLLM):
stop_sequences: list[str] | None = None,
stream: bool = False,
client_params: dict[str, Any] | None = None,
interceptor: BaseInterceptor[httpx.Request, httpx.Response] | None = None,
**kwargs: Any,
):
"""Initialize Anthropic chat completion client.
@@ -61,6 +67,7 @@ class AnthropicCompletion(BaseLLM):
stop_sequences: Stop sequences (Anthropic uses stop_sequences, not stop)
stream: Enable streaming responses
client_params: Additional parameters for the Anthropic client
interceptor: HTTP interceptor for modifying requests/responses at transport level.
**kwargs: Additional parameters
"""
super().__init__(
@@ -68,6 +75,7 @@ class AnthropicCompletion(BaseLLM):
)
# Client params
self.interceptor = interceptor
self.client_params = client_params
self.base_url = base_url
self.timeout = timeout
@@ -100,6 +108,11 @@ class AnthropicCompletion(BaseLLM):
"max_retries": self.max_retries,
}
if self.interceptor:
transport = HTTPTransport(interceptor=self.interceptor)
http_client = httpx.Client(transport=transport)
client_params["http_client"] = http_client # type: ignore[assignment]
if self.client_params:
client_params.update(self.client_params)

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import json
import logging
import os
from typing import Any, TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from pydantic import BaseModel
@@ -13,23 +13,25 @@ from crewai.utilities.exceptions.context_window_exceeding_exception import (
)
from crewai.utilities.types import LLMMessage
if TYPE_CHECKING:
from crewai.llms.hooks.base import BaseInterceptor
from crewai.tools.base_tool import BaseTool
try:
from azure.ai.inference import ( # type: ignore[import-not-found]
from azure.ai.inference import (
ChatCompletionsClient,
)
from azure.ai.inference.models import ( # type: ignore[import-not-found]
from azure.ai.inference.models import (
ChatCompletions,
ChatCompletionsToolCall,
StreamingChatCompletionsUpdate,
)
from azure.core.credentials import ( # type: ignore[import-not-found]
from azure.core.credentials import (
AzureKeyCredential,
)
from azure.core.exceptions import ( # type: ignore[import-not-found]
from azure.core.exceptions import (
HttpResponseError,
)
@@ -64,7 +66,8 @@ class AzureCompletion(BaseLLM):
max_tokens: int | None = None,
stop: list[str] | None = None,
stream: bool = False,
**kwargs,
interceptor: BaseInterceptor[Any, Any] | None = None,
**kwargs: Any,
):
"""Initialize Azure AI Inference chat completion client.
@@ -82,8 +85,15 @@ class AzureCompletion(BaseLLM):
max_tokens: Maximum tokens in response
stop: Stop sequences
stream: Enable streaming responses
interceptor: HTTP interceptor (not yet supported for Azure).
**kwargs: Additional parameters
"""
if interceptor is not None:
raise NotImplementedError(
"HTTP interceptors are not yet supported for Azure AI Inference provider. "
"Interceptors are currently supported for OpenAI and Anthropic providers only."
)
super().__init__(
model=model, temperature=temperature, stop=stop or [], **kwargs
)
@@ -121,7 +131,7 @@ class AzureCompletion(BaseLLM):
if self.api_version:
client_kwargs["api_version"] = self.api_version
self.client = ChatCompletionsClient(**client_kwargs)
self.client = ChatCompletionsClient(**client_kwargs) # type: ignore[arg-type]
self.top_p = top_p
self.frequency_penalty = frequency_penalty
@@ -249,7 +259,7 @@ class AzureCompletion(BaseLLM):
def _prepare_completion_params(
self,
messages: list[LLMMessage],
tools: list[dict] | None = None,
tools: list[dict[str, Any]] | None = None,
response_model: type[BaseModel] | None = None,
) -> dict[str, Any]:
"""Prepare parameters for Azure AI Inference chat completion.
@@ -302,7 +312,9 @@ class AzureCompletion(BaseLLM):
return params
def _convert_tools_for_interference(self, tools: list[dict]) -> list[dict]:
def _convert_tools_for_interference(
self, tools: list[dict[str, Any]]
) -> list[dict[str, Any]]:
"""Convert CrewAI tool format to Azure OpenAI function calling format."""
from crewai.llms.providers.utils.common import safe_tool_conversion

View File

@@ -30,6 +30,8 @@ if TYPE_CHECKING:
ToolTypeDef,
)
from crewai.llms.hooks.base import BaseInterceptor
try:
from boto3.session import Session
@@ -157,8 +159,9 @@ class BedrockCompletion(BaseLLM):
guardrail_config: dict[str, Any] | None = None,
additional_model_request_fields: dict[str, Any] | None = None,
additional_model_response_field_paths: list[str] | None = None,
**kwargs,
):
interceptor: BaseInterceptor[Any, Any] | None = None,
**kwargs: Any,
) -> None:
"""Initialize AWS Bedrock completion client.
Args:
@@ -176,8 +179,15 @@ class BedrockCompletion(BaseLLM):
guardrail_config: Guardrail configuration for content filtering
additional_model_request_fields: Model-specific request parameters
additional_model_response_field_paths: Custom response field paths
interceptor: HTTP interceptor (not yet supported for Bedrock).
**kwargs: Additional parameters
"""
if interceptor is not None:
raise NotImplementedError(
"HTTP interceptors are not yet supported for AWS Bedrock provider. "
"Interceptors are currently supported for OpenAI and Anthropic providers only."
)
# Extract provider from kwargs to avoid duplicate argument
kwargs.pop("provider", None)
@@ -247,7 +257,7 @@ class BedrockCompletion(BaseLLM):
try:
# Emit call started event
self._emit_call_started_event(
messages=messages, # type: ignore[arg-type]
messages=messages,
tools=tools,
callbacks=callbacks,
available_functions=available_functions,
@@ -740,7 +750,9 @@ class BedrockCompletion(BaseLLM):
return converse_messages, system_message
@staticmethod
def _format_tools_for_converse(tools: list[dict]) -> list[ConverseToolTypeDef]:
def _format_tools_for_converse(
tools: list[dict[str, Any]],
) -> list[ConverseToolTypeDef]:
"""Convert CrewAI tools to Converse API format following AWS specification."""
from crewai.llms.providers.utils.common import safe_tool_conversion

View File

@@ -1,4 +1,3 @@
import json
import logging
import os
from typing import Any, cast
@@ -7,6 +6,7 @@ from pydantic import BaseModel
from crewai.events.types.llm_events import LLMCallType
from crewai.llms.base_llm import BaseLLM
from crewai.llms.hooks.base import BaseInterceptor
from crewai.utilities.agent_utils import is_context_length_exceeded
from crewai.utilities.exceptions.context_window_exceeding_exception import (
LLMContextLengthExceededError,
@@ -45,7 +45,8 @@ class GeminiCompletion(BaseLLM):
stream: bool = False,
safety_settings: dict[str, Any] | None = None,
client_params: dict[str, Any] | None = None,
**kwargs,
interceptor: BaseInterceptor[Any, Any] | None = None,
**kwargs: Any,
):
"""Initialize Google Gemini chat completion client.
@@ -63,8 +64,15 @@ class GeminiCompletion(BaseLLM):
safety_settings: Safety filter settings
client_params: Additional parameters to pass to the Google Gen AI Client constructor.
Supports parameters like http_options, credentials, debug_config, etc.
interceptor: HTTP interceptor (not yet supported for Gemini).
**kwargs: Additional parameters
"""
if interceptor is not None:
raise NotImplementedError(
"HTTP interceptors are not yet supported for Google Gemini provider. "
"Interceptors are currently supported for OpenAI and Anthropic providers only."
)
super().__init__(
model=model, temperature=temperature, stop=stop_sequences or [], **kwargs
)
@@ -96,7 +104,7 @@ class GeminiCompletion(BaseLLM):
self.is_gemini_1_5 = "gemini-1.5" in model.lower()
self.supports_tools = self.is_gemini_1_5 or self.is_gemini_2
def _initialize_client(self, use_vertexai: bool = False) -> genai.Client:
def _initialize_client(self, use_vertexai: bool = False) -> genai.Client: # type: ignore[no-any-unimported]
"""Initialize the Google Gen AI client with proper parameter handling.
Args:
@@ -171,7 +179,7 @@ class GeminiCompletion(BaseLLM):
def call(
self,
messages: str | list[LLMMessage],
tools: list[dict] | None = None,
tools: list[dict[str, Any]] | None = None,
callbacks: list[Any] | None = None,
available_functions: dict[str, Any] | None = None,
from_task: Any | None = None,
@@ -193,7 +201,7 @@ class GeminiCompletion(BaseLLM):
"""
try:
self._emit_call_started_event(
messages=messages, # type: ignore[arg-type]
messages=messages,
tools=tools,
callbacks=callbacks,
available_functions=available_functions,
@@ -203,7 +211,7 @@ class GeminiCompletion(BaseLLM):
self.tools = tools
formatted_content, system_instruction = self._format_messages_for_gemini(
messages # type: ignore[arg-type]
messages
)
config = self._prepare_generation_config(
@@ -245,10 +253,10 @@ class GeminiCompletion(BaseLLM):
)
raise
def _prepare_generation_config(
def _prepare_generation_config( # type: ignore[no-any-unimported]
self,
system_instruction: str | None = None,
tools: list[dict] | None = None,
tools: list[dict[str, Any]] | None = None,
response_model: type[BaseModel] | None = None,
) -> types.GenerateContentConfig:
"""Prepare generation config for Google Gemini API.
@@ -297,7 +305,9 @@ class GeminiCompletion(BaseLLM):
return types.GenerateContentConfig(**config_params)
def _convert_tools_for_interference(self, tools: list[dict]) -> list[types.Tool]:
def _convert_tools_for_interference( # type: ignore[no-any-unimported]
self, tools: list[dict[str, Any]]
) -> list[types.Tool]:
"""Convert CrewAI tool format to Gemini function declaration format."""
gemini_tools = []
@@ -320,7 +330,7 @@ class GeminiCompletion(BaseLLM):
return gemini_tools
def _format_messages_for_gemini(
def _format_messages_for_gemini( # type: ignore[no-any-unimported]
self, messages: str | list[LLMMessage]
) -> tuple[list[types.Content], str | None]:
"""Format messages for Gemini API.
@@ -364,7 +374,7 @@ class GeminiCompletion(BaseLLM):
return contents, system_instruction
def _handle_completion(
def _handle_completion( # type: ignore[no-any-unimported]
self,
contents: list[types.Content],
system_instruction: str | None,
@@ -431,7 +441,7 @@ class GeminiCompletion(BaseLLM):
return content
def _handle_streaming_completion(
def _handle_streaming_completion( # type: ignore[no-any-unimported]
self,
contents: list[types.Content],
config: types.GenerateContentConfig,
@@ -560,8 +570,9 @@ class GeminiCompletion(BaseLLM):
}
return {"total_tokens": 0}
def _convert_contents_to_dict(
self, contents: list[types.Content]
def _convert_contents_to_dict( # type: ignore[no-any-unimported]
self,
contents: list[types.Content],
) -> list[dict[str, str]]:
"""Convert contents to dict format."""
return [

View File

@@ -6,6 +6,7 @@ import logging
import os
from typing import TYPE_CHECKING, Any
import httpx
from openai import APIConnectionError, NotFoundError, OpenAI
from openai.types.chat import ChatCompletion, ChatCompletionChunk
from openai.types.chat.chat_completion import Choice
@@ -14,6 +15,7 @@ from pydantic import BaseModel
from crewai.events.types.llm_events import LLMCallType
from crewai.llms.base_llm import BaseLLM
from crewai.llms.hooks.transport import HTTPTransport
from crewai.utilities.agent_utils import is_context_length_exceeded
from crewai.utilities.exceptions.context_window_exceeding_exception import (
LLMContextLengthExceededError,
@@ -23,6 +25,7 @@ from crewai.utilities.types import LLMMessage
if TYPE_CHECKING:
from crewai.agent.core import Agent
from crewai.llms.hooks.base import BaseInterceptor
from crewai.task import Task
from crewai.tools.base_tool import BaseTool
@@ -59,6 +62,7 @@ class OpenAICompletion(BaseLLM):
top_logprobs: int | None = None,
reasoning_effort: str | None = None,
provider: str | None = None,
interceptor: BaseInterceptor[httpx.Request, httpx.Response] | None = None,
**kwargs: Any,
) -> None:
"""Initialize OpenAI chat completion client."""
@@ -66,6 +70,7 @@ class OpenAICompletion(BaseLLM):
if provider is None:
provider = kwargs.pop("provider", "openai")
self.interceptor = interceptor
# Client configuration attributes
self.organization = organization
self.project = project
@@ -88,6 +93,11 @@ class OpenAICompletion(BaseLLM):
)
client_config = self._get_client_params()
if self.interceptor:
transport = HTTPTransport(interceptor=self.interceptor)
http_client = httpx.Client(transport=transport)
client_config["http_client"] = http_client
self.client = OpenAI(**client_config)
# Completion parameters

View File

@@ -1,21 +1,75 @@
"""Utility functions for the crewai project module."""
from collections.abc import Callable
from functools import lru_cache
from typing import ParamSpec, TypeVar, cast
from functools import wraps
from typing import Any, ParamSpec, TypeVar, cast
from pydantic import BaseModel
from crewai.agents.cache.cache_handler import CacheHandler
P = ParamSpec("P")
R = TypeVar("R")
cache = CacheHandler()
def _make_hashable(arg: Any) -> Any:
"""Convert argument to hashable form for caching.
Args:
arg: The argument to convert.
Returns:
Hashable representation of the argument.
"""
if isinstance(arg, BaseModel):
return arg.model_dump_json()
if isinstance(arg, dict):
return tuple(sorted((k, _make_hashable(v)) for k, v in arg.items()))
if isinstance(arg, list):
return tuple(_make_hashable(item) for item in arg)
if hasattr(arg, "__dict__"):
return ("__instance__", id(arg))
return arg
def memoize(meth: Callable[P, R]) -> Callable[P, R]:
"""Memoize a method by caching its results based on arguments.
Handles Pydantic BaseModel instances by converting them to JSON strings
before hashing for cache lookup.
Args:
meth: The method to memoize.
Returns:
A memoized version of the method that caches results.
"""
return cast(Callable[P, R], lru_cache(typed=True)(meth))
@wraps(meth)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
"""Wrapper that converts arguments to hashable form before caching.
Args:
*args: Positional arguments to the memoized method.
**kwargs: Keyword arguments to the memoized method.
Returns:
The result of the memoized method call.
"""
hashable_args = tuple(_make_hashable(arg) for arg in args)
hashable_kwargs = tuple(
sorted((k, _make_hashable(v)) for k, v in kwargs.items())
)
cache_key = str((hashable_args, hashable_kwargs))
cached_result: R | None = cache.read(tool=meth.__name__, input=cache_key)
if cached_result is not None:
return cached_result
result = meth(*args, **kwargs)
cache.add(tool=meth.__name__, input=cache_key, output=result)
return result
return cast(Callable[P, R], wrapper)

View File

@@ -0,0 +1,106 @@
interactions:
- request:
body: '{"max_tokens":4096,"messages":[{"role":"user","content":"Reply with just
the word: SUCCESS"}],"model":"claude-3-5-haiku-20241022","stream":false}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate, zstd
anthropic-version:
- '2023-06-01'
connection:
- keep-alive
content-length:
- '145'
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- Anthropic/Python 0.71.0
x-stainless-arch:
- arm64
x-stainless-async:
- 'false'
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.71.0
x-stainless-retry-count:
- '0'
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.12.10
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: !!binary |
H4sIAAAAAAAAAwAAAP//dJDLasMwEEX/5a5lsF1nUe1KCARCu6gpFEoRgzTEamzJ1qNNCf734tDQ
F10N3HNmBu4JgzfcQ0L3lA0XV8Wq6MgeclGXdVOVdQ0BayAxxL0qq9rcXm/vb2g37ehtc+xeHu+m
4xYC6X3kxeIYac8QCL5fAorRxkQuQUB7l9glyKfTxU98XMh5SLQP6/WmbTE/C8TkRxWYoneQYGdU
ysHhE0SeMjvNkC73vUA+f5UnWDfmpJI/sIuQVSOgSXesdGBK1jv1UygvPDCZ/9hld7nPY8cDB+rV
avjrf9Gq+01nAZ/T96gRiBxerWaVLAdILE0ZCgbz/AEAAP//AwA4VVIcmwEAAA==
headers:
CF-RAY:
- 9997ac4cbfb443fa-EWR
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Tue, 04 Nov 2025 22:50:55 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 87faf353-e074-4658-b885-bfac7aa5a7b5
anthropic-ratelimit-input-tokens-limit:
- '400000'
anthropic-ratelimit-input-tokens-remaining:
- '400000'
anthropic-ratelimit-input-tokens-reset:
- '2025-11-04T22:50:55Z'
anthropic-ratelimit-output-tokens-limit:
- '80000'
anthropic-ratelimit-output-tokens-remaining:
- '80000'
anthropic-ratelimit-output-tokens-reset:
- '2025-11-04T22:50:55Z'
anthropic-ratelimit-requests-limit:
- '4000'
anthropic-ratelimit-requests-remaining:
- '3999'
anthropic-ratelimit-requests-reset:
- '2025-11-04T22:50:54Z'
anthropic-ratelimit-tokens-limit:
- '480000'
anthropic-ratelimit-tokens-remaining:
- '480000'
anthropic-ratelimit-tokens-reset:
- '2025-11-04T22:50:55Z'
cf-cache-status:
- DYNAMIC
request-id:
- req_011CUofgQ1jTrjQ2sveXU1cC
retry-after:
- '5'
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
x-envoy-upstream-service-time:
- '461'
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,108 @@
interactions:
- request:
body: '{"max_tokens":4096,"messages":[{"role":"user","content":"Say ''Hello World''
and nothing else"}],"model":"claude-3-5-haiku-20241022","stream":false}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate, zstd
anthropic-version:
- '2023-06-01'
connection:
- keep-alive
content-length:
- '146'
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- Anthropic/Python 0.71.0
x-stainless-arch:
- arm64
x-stainless-async:
- 'false'
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.71.0
x-stainless-retry-count:
- '0'
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.12.10
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: !!binary |
H4sIAAAAAAAAAwAAAP//dJBNS8QwEIb/irznFNqu3UPOHvYoeGhFJIRk2IZNk5pMRCn979LF4hee
Bt7nmRl4F0zRkoeE8bpYqg5VV43aXUrV1u1tU7ctBJyFxJTPqm7u3OP9sTFD7uJwLkN/6seHQwcB
fp9psyhnfSYIpOi3QOfsMuvAEDAxMAWGfFp2n+ltI9chcSLv400fk7dYnwUyx1kl0jkGSFCwiksK
+ASZXgoFQ5CheC9Qrp/lAhfmworjhUKGbI4CRpuRlEmk2cWgfgr1zhNp+x/bd7f7NI80UdJeddNf
/4s242+6CsTC36NOIFN6dYYUO0qQ2NqyOlms6wcAAAD//wMArYPuQZ8BAAA=
headers:
CF-RAY:
- 9997ac4268d972a4-EWR
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Tue, 04 Nov 2025 22:50:53 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Response-Tracked:
- 'true'
X-Robots-Tag:
- none
anthropic-organization-id:
- 87faf353-e074-4658-b885-bfac7aa5a7b5
anthropic-ratelimit-input-tokens-limit:
- '400000'
anthropic-ratelimit-input-tokens-remaining:
- '400000'
anthropic-ratelimit-input-tokens-reset:
- '2025-11-04T22:50:53Z'
anthropic-ratelimit-output-tokens-limit:
- '80000'
anthropic-ratelimit-output-tokens-remaining:
- '80000'
anthropic-ratelimit-output-tokens-reset:
- '2025-11-04T22:50:53Z'
anthropic-ratelimit-requests-limit:
- '4000'
anthropic-ratelimit-requests-remaining:
- '3999'
anthropic-ratelimit-requests-reset:
- '2025-11-04T22:50:53Z'
anthropic-ratelimit-tokens-limit:
- '480000'
anthropic-ratelimit-tokens-remaining:
- '480000'
anthropic-ratelimit-tokens-reset:
- '2025-11-04T22:50:53Z'
cf-cache-status:
- DYNAMIC
request-id:
- req_011CUofgGvqpCtoqCmKcwXdK
retry-after:
- '7'
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
x-envoy-upstream-service-time:
- '441'
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,107 @@
interactions:
- request:
body: '{"max_tokens":4096,"messages":[{"role":"user","content":"Count from 1 to
3"}],"model":"claude-3-5-haiku-20241022","stream":false}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate, zstd
anthropic-version:
- '2023-06-01'
connection:
- keep-alive
content-length:
- '129'
content-type:
- application/json
host:
- api.anthropic.com
user-agent:
- Anthropic/Python 0.71.0
x-stainless-arch:
- arm64
x-stainless-async:
- 'false'
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 0.71.0
x-stainless-retry-count:
- '0'
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.12.10
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: !!binary |
H4sIAAAAAAAAA3SQy2rDMBBFf8XcTTcy2E5DQbuuUvrIJt3VRQh7EovYI1calbTB/14cGvqiq4F7
zgzDPWLwLfXQaHqbWsoX+TLvrNunvCqqy7KoKii4FhpD3JmiXD2s7++u+HG1kcP77cGuN9tyuIaC
vI00WxSj3REUgu/nwMboolgWKDSehVign45nX+gwk9PQuKFAFzFrfGJxvMu2wQ9ZmYnPFrrmmsua
q5oXmJ4VovjRBLLRMzSIWyMpMD5BpJdE3BA0p75XSKev9BGOxyRG/J44QpdLhcY2HZkmkBXn2fwU
ijMPZNv/2Hl3vk9jRwMF25vl8Nf/omX3m04KPsn3qCoUIoVX15ARRwEac5WtDS2m6QMAAP//AwC8
QSj4vAEAAA==
headers:
CF-RAY:
- 9997ac45ea33de93-EWR
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Tue, 04 Nov 2025 22:50:54 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- 87faf353-e074-4658-b885-bfac7aa5a7b5
anthropic-ratelimit-input-tokens-limit:
- '400000'
anthropic-ratelimit-input-tokens-remaining:
- '400000'
anthropic-ratelimit-input-tokens-reset:
- '2025-11-04T22:50:54Z'
anthropic-ratelimit-output-tokens-limit:
- '80000'
anthropic-ratelimit-output-tokens-remaining:
- '80000'
anthropic-ratelimit-output-tokens-reset:
- '2025-11-04T22:50:54Z'
anthropic-ratelimit-requests-limit:
- '4000'
anthropic-ratelimit-requests-remaining:
- '3999'
anthropic-ratelimit-requests-reset:
- '2025-11-04T22:50:53Z'
anthropic-ratelimit-tokens-limit:
- '480000'
anthropic-ratelimit-tokens-remaining:
- '480000'
anthropic-ratelimit-tokens-reset:
- '2025-11-04T22:50:54Z'
cf-cache-status:
- DYNAMIC
request-id:
- req_011CUofgKLSz1aDmL9qbduuR
retry-after:
- '6'
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
x-envoy-upstream-service-time:
- '801'
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,116 @@
interactions:
- request:
body: '{"messages":[{"role":"user","content":"Reply with just the word: SUCCESS"}],"model":"gpt-4o-mini"}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate, zstd
connection:
- keep-alive
content-length:
- '98'
content-type:
- application/json
host:
- api.openai.com
user-agent:
- OpenAI/Python 1.109.1
x-stainless-arch:
- arm64
x-stainless-async:
- 'false'
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 1.109.1
x-stainless-read-timeout:
- '600'
x-stainless-retry-count:
- '0'
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.12.10
method: POST
uri: https://api.openai.com/v1/chat/completions
response:
body:
string: !!binary |
H4sIAAAAAAAAAwAAAP//jJJNT+MwEIbv+RXWnJtVW/oBvbEVPexyq2AFCEXGnqReHI9lTxAr1P++
clKa8CVxyWGeecfvO5mXTAgwGlYC1E6yqr3N1ze/Z8+Xm+X1+cU1P23qXydSnz5ubvXP8o+FUVLQ
w19U/Kr6oaj2FtmQ67AKKBnT1MlyMZ2eLU6WkxbUpNEmWeU5n1FeG2fy6Xg6y8fLfHJ6UO/IKIyw
EneZEEK8tN/k02l8hpUYj14rNcYoK4TVsUkICGRTBWSMJrJ0DKMeKnKMrrW+vVqvL7bbIQ1YNlEm
h66xdgCkc8QyJWx93R/I/ujEUuUDPcR3UiiNM3FXBJSRXHo1Mnlo6T4T4r5N3LwJAT5Q7blgesT2
ucmsGwf9ngfwwJhY2kF5PvpkWKGRpbFxsDBQUu1Q98p+u7LRhgYgG0T+6OWz2V1s46rvjO+BUugZ
deEDaqPe5u3bAqYj/KrtuOLWMEQMT0ZhwQZD+g0aS9nY7jQg/ouMdVEaV2HwwXT3UfpivhjLcoHz
+Rlk++w/AAAA//8DAGpm+y8tAwAA
headers:
CF-RAY:
- 9997a55e8e85d954-EWR
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Tue, 04 Nov 2025 22:46:11 GMT
Server:
- cloudflare
Set-Cookie:
- __cf_bm=Ljd6Yw.qJgdFyASoXMTCHgeOXz.kPJVf9verbOyhWzg-1762296371-1.0.1.1-HutBZMolyfao56ckVJOnqKZgW8SSm0S_xA1DF2HIE4eYlqsLEi3OtkeTKNc536CxqhcmuTINB23o_A6nID5TAGpXCeNYBEgLJKiggQamQ9w;
path=/; expires=Tue, 04-Nov-25 23:16:11 GMT; domain=.api.openai.com; HttpOnly;
Secure; SameSite=None
- _cfuvid=Tz6VwwwbLcFpqp9Poc_3sUeqc33hmGkTq8YCekrTAns-1762296371669-0.0.1.1-604800000;
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
Strict-Transport-Security:
- max-age=31536000; includeSubDomains; preload
Transfer-Encoding:
- chunked
X-Content-Type-Options:
- nosniff
access-control-expose-headers:
- X-Request-ID
alt-svc:
- h3=":443"; ma=86400
cf-cache-status:
- DYNAMIC
openai-organization:
- crewai-iuxna1
openai-processing-ms:
- '194'
openai-project:
- proj_xitITlrFeen7zjNSzML82h9x
openai-version:
- '2020-10-01'
x-envoy-upstream-service-time:
- '222'
x-openai-proxy-wasm:
- v0.1
x-ratelimit-limit-project-tokens:
- '150000000'
x-ratelimit-limit-requests:
- '30000'
x-ratelimit-limit-tokens:
- '150000000'
x-ratelimit-remaining-project-tokens:
- '149999987'
x-ratelimit-remaining-requests:
- '29999'
x-ratelimit-remaining-tokens:
- '149999987'
x-ratelimit-reset-project-tokens:
- 0s
x-ratelimit-reset-requests:
- 2ms
x-ratelimit-reset-tokens:
- 0s
x-request-id:
- req_5f6372db5713441eb3ba1cc481aeb0fe
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,119 @@
interactions:
- request:
body: '{"messages":[{"role":"user","content":"Say ''Hello World'' and nothing
else"}],"model":"gpt-4o-mini"}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate, zstd
connection:
- keep-alive
content-length:
- '99'
content-type:
- application/json
host:
- api.openai.com
user-agent:
- OpenAI/Python 1.109.1
x-stainless-arch:
- arm64
x-stainless-async:
- 'false'
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 1.109.1
x-stainless-read-timeout:
- '600'
x-stainless-retry-count:
- '0'
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.12.10
method: POST
uri: https://api.openai.com/v1/chat/completions
response:
body:
string: !!binary |
H4sIAAAAAAAAAwAAAP//jJJNT+MwEIbv+RXWnJtVGvoBvXLhS0Jc2EUIRcaepN51PJY9QaxQ/zty
UpoUWGkvPviZd/y+43nLhACjYSNAbSWr1tv8/OF68Xr/wtKfXMaLhb6xv+71Dd1dGV3cwiwp6Pk3
Kv5Q/VDUeotsyA1YBZSMqet8vSrLs9XJet6DljTaJGs85wvKW+NMXhblIi/W+fx0r96SURhhIx4z
IYR468/k02l8hY0oZh83LcYoG4TNoUgICGTTDcgYTWTpGGYjVOQYXW/9Aq0l8ZOC1dOKgHUXZXLp
OmsnQDpHLFPK3tvTnuwObiw1PtBz/CSF2jgTt1VAGcmllyOTh57uMiGe+tTdURDwgVrPFdMf7J+b
L4d2MM56hOWeMbG0E8169k2zSiNLY+NkaKCk2qIeleOEZacNTUA2ifzVy3e9h9jGNf/TfgRKoWfU
lQ+ojTrOO5YFTIv4r7LDiHvDEDG8GIUVGwzpGzTWsrPDekD8GxnbqjauweCDGXak9tVyVch6hcvl
GWS77B0AAP//AwC41MWDMQMAAA==
headers:
CF-RAY:
- 9997a563da0042a3-EWR
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Tue, 04 Nov 2025 22:46:12 GMT
Server:
- cloudflare
Set-Cookie:
- __cf_bm=71X9mE9Hmg7j990_h7K01BESKxBp2D4QYv9j1PmSm6I-1762296372-1.0.1.1-V7pmEV0YDa.OeJ8Pht15YJt2XRqusPvH52QlHRhBCRAoGIkSmqMCG.rYS44HRNCR3Kf2D4UeRaNaUMgws1tL74cvebKOa_aGVjBw_O2okGc;
path=/; expires=Tue, 04-Nov-25 23:16:12 GMT; domain=.api.openai.com; HttpOnly;
Secure; SameSite=None
- _cfuvid=.9w.Y6a8QsaD_7IAK4u3JaHCreibv0u6ujLC7HVF2nY-1762296372265-0.0.1.1-604800000;
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
Strict-Transport-Security:
- max-age=31536000; includeSubDomains; preload
Transfer-Encoding:
- chunked
X-Content-Type-Options:
- nosniff
X-Response-Tracked:
- 'true'
access-control-expose-headers:
- X-Request-ID
alt-svc:
- h3=":443"; ma=86400
cf-cache-status:
- DYNAMIC
openai-organization:
- crewai-iuxna1
openai-processing-ms:
- '332'
openai-project:
- proj_xitITlrFeen7zjNSzML82h9x
openai-version:
- '2020-10-01'
x-envoy-upstream-service-time:
- '349'
x-openai-proxy-wasm:
- v0.1
x-ratelimit-limit-project-tokens:
- '150000000'
x-ratelimit-limit-requests:
- '30000'
x-ratelimit-limit-tokens:
- '150000000'
x-ratelimit-remaining-project-tokens:
- '149999987'
x-ratelimit-remaining-requests:
- '29999'
x-ratelimit-remaining-tokens:
- '149999990'
x-ratelimit-reset-project-tokens:
- 0s
x-ratelimit-reset-requests:
- 2ms
x-ratelimit-reset-tokens:
- 0s
x-request-id:
- req_21f5fd685fdf43e7b06e4ccf5f796b96
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,116 @@
interactions:
- request:
body: '{"messages":[{"role":"user","content":"Count from 1 to 3"}],"model":"gpt-4o-mini"}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate, zstd
connection:
- keep-alive
content-length:
- '82'
content-type:
- application/json
host:
- api.openai.com
user-agent:
- OpenAI/Python 1.109.1
x-stainless-arch:
- arm64
x-stainless-async:
- 'false'
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 1.109.1
x-stainless-read-timeout:
- '600'
x-stainless-retry-count:
- '0'
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.12.10
method: POST
uri: https://api.openai.com/v1/chat/completions
response:
body:
string: !!binary |
H4sIAAAAAAAAAwAAAP//jJJBb9swDIXv/hUCz3YRu4nT5FpgC9DrLttQGIpM29pkUZDobEGR/z7I
TmO364BdfODHR71H8yURAnQNewGqk6x6Z7LHr0/rc85lR8fdIXw7DWd3ePr869Pj6fClgzQq6PgD
Fb+q7hT1ziBrshNWHiVjnJpvy6LYlffbYgQ91WiirHWcrSnrtdVZsSrW2Wqb5Q9XdUdaYYC9+J4I
IcTL+I0+bY2/YS9W6WulxxBki7C/NQkBnkysgAxBB5aWIZ2hIstoR+t5KopU3N8tscdmCDJatIMx
CyCtJZYx4mjs+UouNyuGWufpGN5JodFWh67yKAPZ+GxgcjDSSyLE8xh5eJMCnKfeccX0E8fn8vU0
DuZFz/DhyphYmrlcFOkHw6oaWWoTFhsDJVWH9ayc1yuHWtMCJIvIf3v5aPYUW9v2f8bPQCl0jHXl
PNZavc07t3mMV/ivttuKR8MQ0J+0woo1+vgbamzkYKbbgHAOjH3VaNuid15PB9K4alOuZFPiZrOD
5JL8AQAA//8DAIyvq4AuAwAA
headers:
CF-RAY:
- 9997a567bce9c35e-EWR
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Tue, 04 Nov 2025 22:46:13 GMT
Server:
- cloudflare
Set-Cookie:
- __cf_bm=WbtKMfrbJkHmW8iHwTlAt1O0TT9hmE7i6Jc4CuzPFkk-1762296373-1.0.1.1-H4_jBpfR_9YQFFm2iDhVCcmwtOAfFhVkN6HaUsD3H8frMqxJjj7oiLathDv89L6e412o.pMtaQVL5e5XfVEv0diMAwtUsWsbzbTwF3rgkug;
path=/; expires=Tue, 04-Nov-25 23:16:13 GMT; domain=.api.openai.com; HttpOnly;
Secure; SameSite=None
- _cfuvid=rxHDwk1CRF6MkO5Jc7ikrkXBxkrhhmf.yJD6Z94mvUI-1762296373153-0.0.1.1-604800000;
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
Strict-Transport-Security:
- max-age=31536000; includeSubDomains; preload
Transfer-Encoding:
- chunked
X-Content-Type-Options:
- nosniff
access-control-expose-headers:
- X-Request-ID
alt-svc:
- h3=":443"; ma=86400
cf-cache-status:
- DYNAMIC
openai-organization:
- crewai-iuxna1
openai-processing-ms:
- '552'
openai-project:
- proj_xitITlrFeen7zjNSzML82h9x
openai-version:
- '2020-10-01'
x-envoy-upstream-service-time:
- '601'
x-openai-proxy-wasm:
- v0.1
x-ratelimit-limit-project-tokens:
- '150000000'
x-ratelimit-limit-requests:
- '30000'
x-ratelimit-limit-tokens:
- '150000000'
x-ratelimit-remaining-project-tokens:
- '149999992'
x-ratelimit-remaining-requests:
- '29999'
x-ratelimit-remaining-tokens:
- '149999992'
x-ratelimit-reset-project-tokens:
- 0s
x-ratelimit-reset-requests:
- 2ms
x-ratelimit-reset-tokens:
- 0s
x-request-id:
- req_f67a12044f894859b6b867e583e42e24
status:
code: 200
message: OK
version: 1

View File

@@ -261,11 +261,9 @@ interactions:
Server:
- cloudflare
Set-Cookie:
- __cf_bm=REDACTED;
path=/; expires=Wed, 05-Nov-25 13:58:46 GMT; domain=.api.openai.com; HttpOnly;
Secure; SameSite=None
- _cfuvid=REDACTED;
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
- __cf_bm=REDACTED; path=/; expires=Wed, 05-Nov-25 13:58:46 GMT; domain=.api.openai.com;
HttpOnly; Secure; SameSite=None
- _cfuvid=REDACTED; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
Transfer-Encoding:
- chunked
X-Content-Type-Options:
@@ -327,8 +325,7 @@ interactions:
content-type:
- application/json
cookie:
- __cf_bm=REDACTED;
_cfuvid=REDACTED
- __cf_bm=REDACTED; _cfuvid=REDACTED
host:
- api.openai.com
user-REDACTED:
@@ -613,11 +610,9 @@ interactions:
Server:
- cloudflare
Set-Cookie:
- __cf_bm=REDACTED;
path=/; expires=Wed, 05-Nov-25 13:58:52 GMT; domain=.api.openai.com; HttpOnly;
Secure; SameSite=None
- _cfuvid=REDACTED;
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
- __cf_bm=REDACTED; path=/; expires=Wed, 05-Nov-25 13:58:52 GMT; domain=.api.openai.com;
HttpOnly; Secure; SameSite=None
- _cfuvid=REDACTED; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
Strict-Transport-Security:
- max-age=31536000; includeSubDomains; preload
Transfer-Encoding:
@@ -695,8 +690,7 @@ interactions:
content-type:
- application/json
cookie:
- __cf_bm=REDACTED;
_cfuvid=REDACTED
- __cf_bm=REDACTED; _cfuvid=REDACTED
host:
- api.openai.com
user-REDACTED:
@@ -946,8 +940,7 @@ interactions:
content-type:
- application/json
cookie:
- __cf_bm=REDACTED;
_cfuvid=REDACTED
- __cf_bm=REDACTED; _cfuvid=REDACTED
host:
- api.openai.com
user-REDACTED:
@@ -1103,8 +1096,7 @@ interactions:
content-type:
- application/json
cookie:
- __cf_bm=REDACTED;
_cfuvid=REDACTED
- __cf_bm=REDACTED; _cfuvid=REDACTED
host:
- api.openai.com
user-REDACTED:
@@ -1259,8 +1251,7 @@ interactions:
content-type:
- application/json
cookie:
- __cf_bm=REDACTED;
_cfuvid=REDACTED
- __cf_bm=REDACTED; _cfuvid=REDACTED
host:
- api.openai.com
user-REDACTED:
@@ -1361,4 +1352,435 @@ interactions:
status:
code: 200
message: OK
- request:
body: '{"input":["Counting and Understanding Numbers(Topic): A math topic focusing
on basic counting and number recognition suitable for 6-year-old children."],"model":"text-embedding-3-small","encoding_format":"base64"}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate, zstd
connection:
- keep-alive
content-length:
- '213'
content-type:
- application/json
cookie:
- _cfuvid=REDACTED
host:
- api.openai.com
user-agent:
- OpenAI/Python 1.109.1
x-stainless-arch:
- arm64
x-stainless-async:
- 'false'
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 1.109.1
x-stainless-read-timeout:
- '600'
x-stainless-retry-count:
- '0'
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.12.9
method: POST
uri: https://api.openai.com/v1/embeddings
response:
body:
string: !!binary |
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
headers:
CF-RAY:
- 999df9fdfd6f5e4b-EWR
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Wed, 05 Nov 2025 17:12:32 GMT
Server:
- cloudflare
Set-Cookie:
- __cf_bm=Ipe.2Za34YNu_dh7H.OA5xwdSbGX5iazBRXn_3I5FHc-1762362752-1.0.1.1-HWtQ_T5RQayTNBrBXdCJpZOdvqkhREn3bsNJMdoniV3zlGFPv8HtTax9hHDHeuowqN.YAeUKu.YvNvCArHl4W_jOQB.peQWkEbB3vndJiZU;
path=/; expires=Wed, 05-Nov-25 17:42:32 GMT; domain=.api.openai.com; HttpOnly;
Secure; SameSite=None
- _cfuvid=BseARaiBSIUng8tc7xw8RRLnBuo9liZGd5GtXIyeLX8-1762362752370-0.0.1.1-604800000;
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
Transfer-Encoding:
- chunked
X-Content-Type-Options:
- nosniff
access-control-allow-origin:
- '*'
access-control-expose-headers:
- X-Request-ID
alt-svc:
- h3=":443"; ma=86400
cf-cache-status:
- DYNAMIC
openai-model:
- text-embedding-3-small
openai-organization:
- user-hortuttj2f3qtmxyik2zxf4q
openai-processing-ms:
- '393'
openai-project:
- proj_fL4UBWR1CMpAAdgzaSKqsVvA
openai-version:
- '2020-10-01'
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- envoy-router-7dbb48b87-t6hdx
x-envoy-upstream-service-time:
- '653'
x-openai-proxy-wasm:
- v0.1
x-ratelimit-limit-requests:
- '3000'
x-ratelimit-limit-tokens:
- '1000000'
x-ratelimit-remaining-requests:
- '2999'
x-ratelimit-remaining-tokens:
- '999966'
x-ratelimit-reset-requests:
- 20ms
x-ratelimit-reset-tokens:
- 2ms
x-request-id:
- req_8c16fc27349f4a508ea9a3dbb63b3f04
status:
code: 200
message: OK
- request:
body: '{"input":["Angle(Teaching Approach): Utilizes everyday objects to connect
abstract numbers to tangible items to enhance understanding and engagement."],"model":"text-embedding-3-small","encoding_format":"base64"}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate, zstd
connection:
- keep-alive
content-length:
- '212'
content-type:
- application/json
cookie:
- _cfuvid=BseARaiBSIUng8tc7xw8RRLnBuo9liZGd5GtXIyeLX8-1762362752370-0.0.1.1-604800000;
__cf_bm=Ipe.2Za34YNu_dh7H.OA5xwdSbGX5iazBRXn_3I5FHc-1762362752-1.0.1.1-HWtQ_T5RQayTNBrBXdCJpZOdvqkhREn3bsNJMdoniV3zlGFPv8HtTax9hHDHeuowqN.YAeUKu.YvNvCArHl4W_jOQB.peQWkEbB3vndJiZU
host:
- api.openai.com
user-agent:
- OpenAI/Python 1.109.1
x-stainless-arch:
- arm64
x-stainless-async:
- 'false'
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 1.109.1
x-stainless-read-timeout:
- '600'
x-stainless-retry-count:
- '0'
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.12.9
method: POST
uri: https://api.openai.com/v1/embeddings
response:
body:
string: !!binary |
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
BQAA//8DAFjeumvgIAAA
headers:
CF-RAY:
- 999dfa02dc345e4b-EWR
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Wed, 05 Nov 2025 17:12:32 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Content-Type-Options:
- nosniff
access-control-allow-origin:
- '*'
access-control-expose-headers:
- X-Request-ID
alt-svc:
- h3=":443"; ma=86400
cf-cache-status:
- DYNAMIC
openai-model:
- text-embedding-3-small
openai-organization:
- user-hortuttj2f3qtmxyik2zxf4q
openai-processing-ms:
- '94'
openai-project:
- proj_fL4UBWR1CMpAAdgzaSKqsVvA
openai-version:
- '2020-10-01'
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
via:
- envoy-router-7dbb48b87-ct5vk
x-envoy-upstream-service-time:
- '124'
x-openai-proxy-wasm:
- v0.1
x-ratelimit-limit-requests:
- '3000'
x-ratelimit-limit-tokens:
- '1000000'
x-ratelimit-remaining-requests:
- '2999'
x-ratelimit-remaining-tokens:
- '999966'
x-ratelimit-reset-requests:
- 20ms
x-ratelimit-reset-tokens:
- 2ms
x-request-id:
- req_2dcf36b5ee6549779e7f5891115cbd6c
status:
code: 200
message: OK
version: 1

File diff suppressed because one or more lines are too long

View File

@@ -1,539 +1,17 @@
interactions:
- request:
body: '{"messages": [{"role": "system", "content": "You are Scorer. You''re an
expert scorer, specialized in scoring titles.\nYour personal goal is: Score
the title\nTo give my best complete final answer to the task use the exact following
body: '{"messages":[{"role":"system","content":"You are Scorer. You''re an expert
scorer, specialized in scoring titles.\nYour personal goal is: Score the title\nTo
give my best complete final answer to the task respond using the exact following
format:\n\nThought: I now can give a great answer\nFinal Answer: Your final
answer must be the great and the most complete as possible, it must be outcome
described.\n\nI MUST use these formats, my job depends on it!"}, {"role": "user",
"content": "\nCurrent Task: Give me an integer score between 1-5 for the following
title: ''The impact of AI in the future of work''\n\nThis is the expect criteria
for your final answer: The score of the title.\nyou MUST return the actual complete
content as the final answer, not a summary.\n\nBegin! This is VERY important
to you, use the tools available and give your best Final Answer, your job depends
on it!\n\nThought:"}], "model": "gpt-4o"}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
connection:
- keep-alive
content-length:
- '915'
content-type:
- application/json
cookie:
- __cf_bm=9.8sBYBkvBR8R1K_bVF7xgU..80XKlEIg3N2OBbTSCU-1727214102-1.0.1.1-.qiTLXbPamYUMSuyNsOEB9jhGu.jOifujOrx9E2JZvStbIZ9RTIiE44xKKNfLPxQkOi6qAT3h6htK8lPDGV_5g;
_cfuvid=lbRdAddVWV6W3f5Dm9SaOPWDUOxqtZBSPr_fTW26nEA-1727213194587-0.0.1.1-604800000
host:
- api.openai.com
user-agent:
- OpenAI/Python 1.47.0
x-stainless-arch:
- arm64
x-stainless-async:
- 'false'
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 1.47.0
x-stainless-raw-response:
- 'true'
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.11.7
method: POST
uri: https://api.openai.com/v1/chat/completions
response:
content: "{\n \"id\": \"chatcmpl-AB7gMdbh6Ncs7ekM3mpk0rfbH9oHy\",\n \"object\":
\"chat.completion\",\n \"created\": 1727214502,\n \"model\": \"gpt-4o-2024-05-13\",\n
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
\"assistant\",\n \"content\": \"Thought: I now can give a great answer\\nFinal
Answer: 4\",\n \"refusal\": null\n },\n \"logprobs\": null,\n
\ \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
186,\n \"completion_tokens\": 15,\n \"total_tokens\": 201,\n \"completion_tokens_details\":
{\n \"reasoning_tokens\": 0\n }\n },\n \"system_fingerprint\": \"fp_52a7f40b0b\"\n}\n"
headers:
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 8c85fa6eecc81cf3-GRU
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Tue, 24 Sep 2024 21:48:22 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Content-Type-Options:
- nosniff
access-control-expose-headers:
- X-Request-ID
openai-organization:
- crewai-iuxna1
openai-processing-ms:
- '231'
openai-version:
- '2020-10-01'
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
x-ratelimit-limit-requests:
- '10000'
x-ratelimit-limit-tokens:
- '30000000'
x-ratelimit-remaining-requests:
- '9999'
x-ratelimit-remaining-tokens:
- '29999781'
x-ratelimit-reset-requests:
- 6ms
x-ratelimit-reset-tokens:
- 0s
x-request-id:
- req_04be4057cf9dce611e16f95ffa36a88a
http_version: HTTP/1.1
status_code: 200
- request:
body: '{"messages": [{"role": "user", "content": "4"}, {"role": "system", "content":
"I''m gonna convert this raw text into valid JSON.\n\nThe json should have the
following structure, with the following keys:\n{\n score: int\n}"}], "model":
"gpt-4o", "tool_choice": {"type": "function", "function": {"name": "ScoreOutput"}},
"tools": [{"type": "function", "function": {"name": "ScoreOutput", "description":
"Correctly extracted `ScoreOutput` with all the required parameters with correct
types", "parameters": {"properties": {"score": {"title": "Score", "type": "integer"}},
"required": ["score"], "type": "object"}}}]}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
connection:
- keep-alive
content-length:
- '615'
content-type:
- application/json
cookie:
- __cf_bm=9.8sBYBkvBR8R1K_bVF7xgU..80XKlEIg3N2OBbTSCU-1727214102-1.0.1.1-.qiTLXbPamYUMSuyNsOEB9jhGu.jOifujOrx9E2JZvStbIZ9RTIiE44xKKNfLPxQkOi6qAT3h6htK8lPDGV_5g;
_cfuvid=lbRdAddVWV6W3f5Dm9SaOPWDUOxqtZBSPr_fTW26nEA-1727213194587-0.0.1.1-604800000
host:
- api.openai.com
user-agent:
- OpenAI/Python 1.47.0
x-stainless-arch:
- arm64
x-stainless-async:
- 'false'
x-stainless-lang:
- python
x-stainless-os:
- MacOS
x-stainless-package-version:
- 1.47.0
x-stainless-raw-response:
- 'true'
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.11.7
method: POST
uri: https://api.openai.com/v1/chat/completions
response:
content: "{\n \"id\": \"chatcmpl-AB7gNjFjFYTE9aEM06OLFECfe74NF\",\n \"object\":
\"chat.completion\",\n \"created\": 1727214503,\n \"model\": \"gpt-4o-2024-05-13\",\n
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
\"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n
\ \"id\": \"call_BriklYUCRXEHjLYyyPiFo1w7\",\n \"type\":
\"function\",\n \"function\": {\n \"name\": \"ScoreOutput\",\n
\ \"arguments\": \"{\\\"score\\\":4}\"\n }\n }\n
\ ],\n \"refusal\": null\n },\n \"logprobs\": null,\n
\ \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
100,\n \"completion_tokens\": 5,\n \"total_tokens\": 105,\n \"completion_tokens_details\":
{\n \"reasoning_tokens\": 0\n }\n },\n \"system_fingerprint\": \"fp_e375328146\"\n}\n"
headers:
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 8c85fa72fadb1cf3-GRU
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Tue, 24 Sep 2024 21:48:23 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Content-Type-Options:
- nosniff
access-control-expose-headers:
- X-Request-ID
openai-organization:
- crewai-iuxna1
openai-processing-ms:
- '221'
openai-version:
- '2020-10-01'
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
x-ratelimit-limit-requests:
- '10000'
x-ratelimit-limit-tokens:
- '30000000'
x-ratelimit-remaining-requests:
- '9999'
x-ratelimit-remaining-tokens:
- '29999947'
x-ratelimit-reset-requests:
- 6ms
x-ratelimit-reset-tokens:
- 0s
x-request-id:
- req_d75a37a0ce046c6a74a19fb24a97be79
http_version: HTTP/1.1
status_code: 200
- request:
body: '{"trace_id": "b4e722b9-c407-4653-ba06-1786963c9c4a", "execution_type":
"crew", "user_identifier": null, "execution_context": {"crew_fingerprint": null,
"crew_name": "crew", "flow_name": null, "crewai_version": "0.201.1", "privacy_level":
"standard"}, "execution_metadata": {"expected_duration_estimate": 300, "agent_count":
0, "task_count": 0, "flow_method_count": 0, "execution_started_at": "2025-10-08T18:15:00.412875+00:00"}}'
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate, zstd
Connection:
- keep-alive
Content-Length:
- '428'
Content-Type:
- application/json
User-Agent:
- CrewAI-CLI/0.201.1
X-Crewai-Organization-Id:
- d3a3d10c-35db-423f-a7a4-c026030ba64d
X-Crewai-Version:
- 0.201.1
method: POST
uri: http://localhost:3000/crewai_plus/api/v1/tracing/batches
response:
body:
string: '{"id":"1a36dd2f-483b-4934-a9b6-f7b95cee2824","trace_id":"b4e722b9-c407-4653-ba06-1786963c9c4a","execution_type":"crew","crew_name":"crew","flow_name":null,"status":"running","duration_ms":null,"crewai_version":"0.201.1","privacy_level":"standard","total_events":0,"execution_context":{"crew_fingerprint":null,"crew_name":"crew","flow_name":null,"crewai_version":"0.201.1","privacy_level":"standard"},"created_at":"2025-10-08T18:15:00.934Z","updated_at":"2025-10-08T18:15:00.934Z"}'
headers:
Content-Length:
- '480'
cache-control:
- no-store
content-security-policy:
- 'default-src ''self'' *.crewai.com crewai.com; script-src ''self'' ''unsafe-inline''
*.crewai.com crewai.com https://cdn.jsdelivr.net/npm/apexcharts https://www.gstatic.com
https://run.pstmn.io https://apis.google.com https://apis.google.com/js/api.js
https://accounts.google.com https://accounts.google.com/gsi/client https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css.map
https://*.google.com https://docs.google.com https://slides.google.com https://js.hs-scripts.com
https://js.sentry-cdn.com https://browser.sentry-cdn.com https://www.googletagmanager.com
https://js-na1.hs-scripts.com https://share.descript.com/; style-src ''self''
''unsafe-inline'' *.crewai.com crewai.com https://cdn.jsdelivr.net/npm/apexcharts;
img-src ''self'' data: *.crewai.com crewai.com https://zeus.tools.crewai.com
https://dashboard.tools.crewai.com https://cdn.jsdelivr.net; font-src ''self''
data: *.crewai.com crewai.com; connect-src ''self'' *.crewai.com crewai.com
https://zeus.tools.crewai.com https://connect.useparagon.com/ https://zeus.useparagon.com/*
https://*.useparagon.com/* https://run.pstmn.io https://connect.tools.crewai.com/
https://*.sentry.io https://www.google-analytics.com ws://localhost:3036 wss://localhost:3036;
frame-src ''self'' *.crewai.com crewai.com https://connect.useparagon.com/
https://zeus.tools.crewai.com https://zeus.useparagon.com/* https://connect.tools.crewai.com/
https://docs.google.com https://drive.google.com https://slides.google.com
https://accounts.google.com https://*.google.com https://www.youtube.com https://share.descript.com'
content-type:
- application/json; charset=utf-8
etag:
- W/"31db72e28a68dfa1c4f3568b388bc2f0"
expires:
- '0'
permissions-policy:
- camera=(), microphone=(self), geolocation=()
pragma:
- no-cache
referrer-policy:
- strict-origin-when-cross-origin
server-timing:
- cache_read.active_support;dur=0.18, sql.active_record;dur=73.01, cache_generate.active_support;dur=16.53,
cache_write.active_support;dur=0.22, cache_read_multi.active_support;dur=0.33,
start_processing.action_controller;dur=0.01, instantiation.active_record;dur=1.29,
feature_operation.flipper;dur=0.50, start_transaction.active_record;dur=0.01,
transaction.active_record;dur=21.52, process_action.action_controller;dur=459.22
vary:
- Accept
x-content-type-options:
- nosniff
x-frame-options:
- SAMEORIGIN
x-permitted-cross-domain-policies:
- none
x-request-id:
- ebc8e3ab-5979-48b7-8816-667a1fd98ce2
x-runtime:
- '0.524429'
x-xss-protection:
- 1; mode=block
status:
code: 201
message: Created
- request:
body: '{"events": [{"event_id": "4a27c1d9-f908-42e4-b4dc-7091db74915b", "timestamp":
"2025-10-08T18:15:00.950855+00:00", "type": "crew_kickoff_started", "event_data":
{"timestamp": "2025-10-08T18:15:00.412055+00:00", "type": "crew_kickoff_started",
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
"crew_name": "crew", "crew": null, "inputs": null}}, {"event_id": "eaa85c90-02d2-444c-bf52-1178d68bae6d",
"timestamp": "2025-10-08T18:15:00.952277+00:00", "type": "task_started", "event_data":
{"task_description": "Give me an integer score between 1-5 for the following
title: ''The impact of AI in the future of work''", "expected_output": "The
score of the title.", "task_name": "Give me an integer score between 1-5 for
the following title: ''The impact of AI in the future of work''", "context":
"", "agent_role": "Scorer", "task_id": "3dca2ae4-e374-42e6-a6de-ecae1e8ac310"}},
{"event_id": "8f1cce5b-7a60-4b53-aac1-05a9d7c3335e", "timestamp": "2025-10-08T18:15:00.952865+00:00",
"type": "agent_execution_started", "event_data": {"agent_role": "Scorer", "agent_goal":
"Score the title", "agent_backstory": "You''re an expert scorer, specialized
in scoring titles."}}, {"event_id": "754a8fb5-bb3a-4204-839e-7b622eb3d6dd",
"timestamp": "2025-10-08T18:15:00.953005+00:00", "type": "llm_call_started",
"event_data": {"timestamp": "2025-10-08T18:15:00.952957+00:00", "type": "llm_call_started",
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
"task_name": "Give me an integer score between 1-5 for the following title:
''The impact of AI in the future of work''", "task_id": "3dca2ae4-e374-42e6-a6de-ecae1e8ac310",
"agent_id": "2ba6f80d-a1da-409f-bd89-13a286b7dfb7", "agent_role": "Scorer",
"from_task": null, "from_agent": null, "model": "gpt-4o-mini", "messages": [{"role":
"system", "content": "You are Scorer. You''re an expert scorer, specialized
in scoring titles.\nYour personal goal is: Score the title\nTo give my best
complete final answer to the task respond using the exact following format:\n\nThought:
I now can give a great answer\nFinal Answer: Your final answer must be the great
and the most complete as possible, it must be outcome described.\n\nI MUST use
these formats, my job depends on it!"}, {"role": "user", "content": "\nCurrent
described.\n\nI MUST use these formats, my job depends on it!"},{"role":"user","content":"\nCurrent
Task: Give me an integer score between 1-5 for the following title: ''The impact
of AI in the future of work''\n\nThis is the expected criteria for your final
answer: The score of the title.\nyou MUST return the actual complete content
as the final answer, not a summary.\nEnsure your final answer contains only
the content in the following format: {\n \"score\": int\n}\n\nEnsure the final
output does not include any code block markers like ```json or ```python.\n\nBegin!
This is VERY important to you, use the tools available and give your best Final
Answer, your job depends on it!\n\nThought:"}], "tools": null, "callbacks":
["<crewai.utilities.token_counter_callback.TokenCalcHandler object at 0x300f52060>"],
"available_functions": null}}, {"event_id": "1a15dcc3-8827-4803-ac61-ab70d5be90f3",
"timestamp": "2025-10-08T18:15:01.085142+00:00", "type": "llm_call_completed",
"event_data": {"timestamp": "2025-10-08T18:15:01.084844+00:00", "type": "llm_call_completed",
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
"task_name": "Give me an integer score between 1-5 for the following title:
''The impact of AI in the future of work''", "task_id": "3dca2ae4-e374-42e6-a6de-ecae1e8ac310",
"agent_id": "2ba6f80d-a1da-409f-bd89-13a286b7dfb7", "agent_role": "Scorer",
"from_task": null, "from_agent": null, "messages": [{"role": "system", "content":
"You are Scorer. You''re an expert scorer, specialized in scoring titles.\nYour
personal goal is: Score the title\nTo give my best complete final answer to
the task respond using the exact following format:\n\nThought: I now can give
a great answer\nFinal Answer: Your final answer must be the great and the most
complete as possible, it must be outcome described.\n\nI MUST use these formats,
my job depends on it!"}, {"role": "user", "content": "\nCurrent Task: Give me
an integer score between 1-5 for the following title: ''The impact of AI in
the future of work''\n\nThis is the expected criteria for your final answer:
The score of the title.\nyou MUST return the actual complete content as the
final answer, not a summary.\nEnsure your final answer contains only the content
in the following format: {\n \"score\": int\n}\n\nEnsure the final output does
not include any code block markers like ```json or ```python.\n\nBegin! This
is VERY important to you, use the tools available and give your best Final Answer,
your job depends on it!\n\nThought:"}], "response": "Thought: I now can give
a great answer\nFinal Answer: 4", "call_type": "<LLMCallType.LLM_CALL: ''llm_call''>",
"model": "gpt-4o-mini"}}, {"event_id": "c6742bd6-5a92-41e8-94f6-49ba267d785f",
"timestamp": "2025-10-08T18:15:01.085480+00:00", "type": "agent_execution_completed",
"event_data": {"agent_role": "Scorer", "agent_goal": "Score the title", "agent_backstory":
"You''re an expert scorer, specialized in scoring titles."}}, {"event_id": "486c254c-57b6-477b-aadc-d5be745613fb",
"timestamp": "2025-10-08T18:15:01.085639+00:00", "type": "task_failed", "event_data":
{"serialization_error": "Circular reference detected (id repeated)", "object_type":
"TaskFailedEvent"}}, {"event_id": "b2ce4ceb-74f6-4379-b65e-8d6dc371f956", "timestamp":
"2025-10-08T18:15:01.086242+00:00", "type": "crew_kickoff_failed", "event_data":
{"timestamp": "2025-10-08T18:15:01.086226+00:00", "type": "crew_kickoff_failed",
"source_fingerprint": null, "source_type": null, "fingerprint_metadata": null,
"crew_name": "crew", "crew": null, "error": "Failed to convert text into a Pydantic
model due to error: ''NoneType'' object has no attribute ''supports_function_calling''"}}],
"batch_metadata": {"events_count": 8, "batch_sequence": 1, "is_final_batch":
false}}'
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate, zstd
Connection:
- keep-alive
Content-Length:
- '5982'
Content-Type:
- application/json
User-Agent:
- CrewAI-CLI/0.201.1
X-Crewai-Organization-Id:
- d3a3d10c-35db-423f-a7a4-c026030ba64d
X-Crewai-Version:
- 0.201.1
method: POST
uri: http://localhost:3000/crewai_plus/api/v1/tracing/batches/b4e722b9-c407-4653-ba06-1786963c9c4a/events
response:
body:
string: '{"events_created":8,"trace_batch_id":"1a36dd2f-483b-4934-a9b6-f7b95cee2824"}'
headers:
Content-Length:
- '76'
cache-control:
- no-store
content-security-policy:
- 'default-src ''self'' *.crewai.com crewai.com; script-src ''self'' ''unsafe-inline''
*.crewai.com crewai.com https://cdn.jsdelivr.net/npm/apexcharts https://www.gstatic.com
https://run.pstmn.io https://apis.google.com https://apis.google.com/js/api.js
https://accounts.google.com https://accounts.google.com/gsi/client https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css.map
https://*.google.com https://docs.google.com https://slides.google.com https://js.hs-scripts.com
https://js.sentry-cdn.com https://browser.sentry-cdn.com https://www.googletagmanager.com
https://js-na1.hs-scripts.com https://share.descript.com/; style-src ''self''
''unsafe-inline'' *.crewai.com crewai.com https://cdn.jsdelivr.net/npm/apexcharts;
img-src ''self'' data: *.crewai.com crewai.com https://zeus.tools.crewai.com
https://dashboard.tools.crewai.com https://cdn.jsdelivr.net; font-src ''self''
data: *.crewai.com crewai.com; connect-src ''self'' *.crewai.com crewai.com
https://zeus.tools.crewai.com https://connect.useparagon.com/ https://zeus.useparagon.com/*
https://*.useparagon.com/* https://run.pstmn.io https://connect.tools.crewai.com/
https://*.sentry.io https://www.google-analytics.com ws://localhost:3036 wss://localhost:3036;
frame-src ''self'' *.crewai.com crewai.com https://connect.useparagon.com/
https://zeus.tools.crewai.com https://zeus.useparagon.com/* https://connect.tools.crewai.com/
https://docs.google.com https://drive.google.com https://slides.google.com
https://accounts.google.com https://*.google.com https://www.youtube.com https://share.descript.com'
content-type:
- application/json; charset=utf-8
etag:
- W/"ec084df3e365d72581f5734016786212"
expires:
- '0'
permissions-policy:
- camera=(), microphone=(self), geolocation=()
pragma:
- no-cache
referrer-policy:
- strict-origin-when-cross-origin
server-timing:
- cache_read.active_support;dur=0.05, sql.active_record;dur=60.65, cache_generate.active_support;dur=2.12,
cache_write.active_support;dur=0.12, cache_read_multi.active_support;dur=0.09,
start_processing.action_controller;dur=0.00, instantiation.active_record;dur=0.51,
start_transaction.active_record;dur=0.00, transaction.active_record;dur=115.06,
process_action.action_controller;dur=475.82
vary:
- Accept
x-content-type-options:
- nosniff
x-frame-options:
- SAMEORIGIN
x-permitted-cross-domain-policies:
- none
x-request-id:
- 96f38d13-8f41-4b6f-b41a-cc526f821efd
x-runtime:
- '0.520997'
x-xss-protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: '{"status": "completed", "duration_ms": 1218, "final_event_count": 8}'
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate, zstd
Connection:
- keep-alive
Content-Length:
- '68'
Content-Type:
- application/json
User-Agent:
- CrewAI-CLI/0.201.1
X-Crewai-Organization-Id:
- d3a3d10c-35db-423f-a7a4-c026030ba64d
X-Crewai-Version:
- 0.201.1
method: PATCH
uri: http://localhost:3000/crewai_plus/api/v1/tracing/batches/b4e722b9-c407-4653-ba06-1786963c9c4a/finalize
response:
body:
string: '{"id":"1a36dd2f-483b-4934-a9b6-f7b95cee2824","trace_id":"b4e722b9-c407-4653-ba06-1786963c9c4a","execution_type":"crew","crew_name":"crew","flow_name":null,"status":"completed","duration_ms":1218,"crewai_version":"0.201.1","privacy_level":"standard","total_events":8,"execution_context":{"crew_name":"crew","flow_name":null,"privacy_level":"standard","crewai_version":"0.201.1","crew_fingerprint":null},"created_at":"2025-10-08T18:15:00.934Z","updated_at":"2025-10-08T18:15:02.539Z"}'
headers:
Content-Length:
- '482'
cache-control:
- no-store
content-security-policy:
- 'default-src ''self'' *.crewai.com crewai.com; script-src ''self'' ''unsafe-inline''
*.crewai.com crewai.com https://cdn.jsdelivr.net/npm/apexcharts https://www.gstatic.com
https://run.pstmn.io https://apis.google.com https://apis.google.com/js/api.js
https://accounts.google.com https://accounts.google.com/gsi/client https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css.map
https://*.google.com https://docs.google.com https://slides.google.com https://js.hs-scripts.com
https://js.sentry-cdn.com https://browser.sentry-cdn.com https://www.googletagmanager.com
https://js-na1.hs-scripts.com https://share.descript.com/; style-src ''self''
''unsafe-inline'' *.crewai.com crewai.com https://cdn.jsdelivr.net/npm/apexcharts;
img-src ''self'' data: *.crewai.com crewai.com https://zeus.tools.crewai.com
https://dashboard.tools.crewai.com https://cdn.jsdelivr.net; font-src ''self''
data: *.crewai.com crewai.com; connect-src ''self'' *.crewai.com crewai.com
https://zeus.tools.crewai.com https://connect.useparagon.com/ https://zeus.useparagon.com/*
https://*.useparagon.com/* https://run.pstmn.io https://connect.tools.crewai.com/
https://*.sentry.io https://www.google-analytics.com ws://localhost:3036 wss://localhost:3036;
frame-src ''self'' *.crewai.com crewai.com https://connect.useparagon.com/
https://zeus.tools.crewai.com https://zeus.useparagon.com/* https://connect.tools.crewai.com/
https://docs.google.com https://drive.google.com https://slides.google.com
https://accounts.google.com https://*.google.com https://www.youtube.com https://share.descript.com'
content-type:
- application/json; charset=utf-8
etag:
- W/"f69bd753b6206f7d8f00bfae64391d7a"
expires:
- '0'
permissions-policy:
- camera=(), microphone=(self), geolocation=()
pragma:
- no-cache
referrer-policy:
- strict-origin-when-cross-origin
server-timing:
- cache_read.active_support;dur=0.13, sql.active_record;dur=20.82, cache_generate.active_support;dur=2.02,
cache_write.active_support;dur=0.18, cache_read_multi.active_support;dur=0.08,
start_processing.action_controller;dur=0.00, instantiation.active_record;dur=1.08,
unpermitted_parameters.action_controller;dur=0.00, start_transaction.active_record;dur=0.00,
transaction.active_record;dur=2.90, process_action.action_controller;dur=844.67
vary:
- Accept
x-content-type-options:
- nosniff
x-frame-options:
- SAMEORIGIN
x-permitted-cross-domain-policies:
- none
x-request-id:
- 80da6a7c-c07d-4a00-b5d9-fb85448ef76a
x-runtime:
- '0.904849'
x-xss-protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: '{"messages":[{"role":"system","content":"Please convert the following text
into valid JSON.\n\nOutput ONLY the valid JSON and nothing else.\n\nThe JSON
must follow this schema exactly:\n```json\n{\n score: int\n}\n```"},{"role":"user","content":"4"}],"model":"gpt-4.1-mini"}'
as the final answer, not a summary.\n\nBegin! This is VERY important to you,
use the tools available and give your best Final Answer, your job depends on
it!\n\nThought:"}],"model":"gpt-4.1-mini"}'
headers:
accept:
- application/json
@@ -542,7 +20,7 @@ interactions:
connection:
- keep-alive
content-length:
- '277'
- '923'
content-type:
- application/json
host:
@@ -566,23 +44,23 @@ interactions:
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.12.10
- 3.12.9
method: POST
uri: https://api.openai.com/v1/chat/completions
response:
body:
string: !!binary |
H4sIAAAAAAAAAwAAAP//jJIxb9swEIV3/QriZiuwFNlxtBadig6ZGrQKJJo8SbQpkiWpwoHh/16Q
ci0lTYEuGu679/TueOeEEBAcSgKsp54NRqafvj2ffh4PX56e2n37+vW7Gj8/n/qjE/64OcAqKPT+
gMz/Ud0xPRiJXmg1YWaRegyu2cM22z3crx/XEQyaowyyzvi0uMvSQSiR5ut8k66LNCuu8l4Lhg5K
8iMhhJBz/IagiuMJShLNYmVA52iHUN6aCAGrZagAdU44T5WH1QyZVh5VzN40zcFpValzpQKrwDFt
sYKSFJW6VKppmqXUYjs6GvKrUcoFoEppT8P8MfTLlVxuMaXujNV7904KrVDC9bVF6rQKkZzXBiK9
JIS8xHWMbyYEY/VgfO31EePvisfJDuZXmGF2f4Veeyrn+jZffeBWc/RUSLdYJzDKeuSzct49HbnQ
C5AsZv47zEfe09xCdf9jPwPG0HjktbHIBXs78NxmMdzov9puO46BwaH9JRjWXqAN78CxpaOcDgfc
q/M41K1QHVpjxXQ9rakLlu82Wbvb5pBckt8AAAD//wMA5Zmg4EwDAAA=
H4sIAAAAAAAAA4xSTU/cMBC951eMfN6gTch+KLeCQIDaC+XAtkWR15kkUxzbsh22Ldr/jpwsm1Co
1EukzJv3/N7MPEcAjEqWAxMN96I1Mj7fbM+vrs+qG3fbXdHtt+zz/X3z9cufzd3mYsdmgaG3P1H4
V9aJ0K2R6EmrARYWucegmqyW6ekyTVanPdDqEmWg1cbH2UkSt6QoTufpIp5ncZId6I0mgY7l8D0C
AHjuv8GoKvEXy2E+e6206ByvkeXHJgBmtQwVxp0j57nybDaCQiuPqvd+1+iubnwO16D0DgRXUNMT
Aoc6BACu3A7tD3VJikv41P/lkE3VLFad4yGS6qScAFwp7XkYSZ/j4YDsj86lro3VW/cXlVWkyDWF
Re60Ci6d14b16D4CeOgn1L0JzYzVrfGF14/YP5esV4MeGzczQRcH0GvP5VhP5+nsA72iRM9JusmM
meCiwXKkjgvhXUl6AkST1O/dfKQ9JCdV/4/8CAiBxmNZGIslibeJxzaL4XD/1Xaccm+YObRPJLDw
hDZsosSKd3K4JuZ+O49tUZGq0RpLw0lVpshEul4k1XqZsmgfvQAAAP//AwDqWlfDYQMAAA==
headers:
CF-RAY:
- 996f475b7e3fedda-MXP
- 999debdd89d62223-EWR
Connection:
- keep-alive
Content-Encoding:
@@ -590,14 +68,14 @@ interactions:
Content-Type:
- application/json
Date:
- Fri, 31 Oct 2025 01:11:31 GMT
- Wed, 05 Nov 2025 17:02:53 GMT
Server:
- cloudflare
Set-Cookie:
- __cf_bm=9OwoBJAn84Nsq0RZdCIu06cNB6RLqor4C1.Q58nU28U-1761873091-1.0.1.1-p82_h8Vnxe0NfH5Iv6MFt.SderZj.v9VnCx_ro6ti2MGhlJOLFsPd6XhBxPsnmuV7Vt_4_uqAbE57E5f1Epl1cmGBT.0844N3CLnTwZFWQI;
path=/; expires=Fri, 31-Oct-25 01:41:31 GMT; domain=.api.openai.com; HttpOnly;
- __cf_bm=tfabkCaSRhhrfazOeTzwx40DBXVo2PjV9viPz7rNRSA-1762362173-1.0.1.1-448VqZWH1ZRKLLTCSb9Qezp9.0dIiDqeZ0CVT8Q.dmfbePuniUR.EDrrXv4lBIWqNgZ_yF1ety_tftCaKIm0Uq4ydaAKdcHVUoOMUjhtvY4;
path=/; expires=Wed, 05-Nov-25 17:32:53 GMT; domain=.api.openai.com; HttpOnly;
Secure; SameSite=None
- _cfuvid=E4.xW3I8m58fngo4vkTKo8hmBumar1HkV.yU8KKjlZg-1761873091967-0.0.1.1-604800000;
- _cfuvid=l9MNhSJeUsRd3C2.MkHdNv0aEoH19qe6RpeKp4Gf.do-1762362173542-0.0.1.1-604800000;
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
Strict-Transport-Security:
- max-age=31536000; includeSubDomains; preload
@@ -612,44 +90,48 @@ interactions:
cf-cache-status:
- DYNAMIC
openai-organization:
- crewai-iuxna1
- user-hortuttj2f3qtmxyik2zxf4q
openai-processing-ms:
- '1770'
- '408'
openai-project:
- proj_xitITlrFeen7zjNSzML82h9x
- proj_fL4UBWR1CMpAAdgzaSKqsVvA
openai-version:
- '2020-10-01'
x-envoy-upstream-service-time:
- '1998'
- '421'
x-openai-proxy-wasm:
- v0.1
x-ratelimit-limit-project-tokens:
- '150000000'
x-ratelimit-limit-requests:
- '30000'
- '500'
x-ratelimit-limit-tokens:
- '150000000'
x-ratelimit-remaining-project-tokens:
- '149999955'
- '200000'
x-ratelimit-remaining-requests:
- '29999'
- '499'
x-ratelimit-remaining-tokens:
- '149999952'
x-ratelimit-reset-project-tokens:
- 0s
- '199794'
x-ratelimit-reset-requests:
- 2ms
- 120ms
x-ratelimit-reset-tokens:
- 0s
- 61ms
x-request-id:
- req_ba7a12cb40744f648d17844196f9c2c6
- req_f1d89dcf14e44e1590e646ecef55c6ff
status:
code: 200
message: OK
- request:
body: '{"messages":[{"role":"system","content":"Please convert the following text
into valid JSON.\n\nOutput ONLY the valid JSON and nothing else.\n\nThe JSON
must follow this schema exactly:\n```json\n{\n score: int\n}\n```"},{"role":"user","content":"4"}],"model":"gpt-4.1-mini","response_format":{"type":"json_schema","json_schema":{"schema":{"properties":{"score":{"title":"Score","type":"integer"}},"required":["score"],"title":"ScoreOutput","type":"object","additionalProperties":false},"name":"ScoreOutput","strict":true}},"stream":false}'
body: '{"messages":[{"role":"system","content":"You are Scorer. You''re an expert
scorer, specialized in scoring titles.\nYour personal goal is: Score the title\nTo
give my best complete final answer to the task respond using the exact following
format:\n\nThought: I now can give a great answer\nFinal Answer: Your final
answer must be the great and the most complete as possible, it must be outcome
described.\n\nI MUST use these formats, my job depends on it!"},{"role":"user","content":"\nCurrent
Task: Give me an integer score between 1-5 for the following title: ''The impact
of AI in the future of work''\n\nThis is the expected criteria for your final
answer: The score of the title.\nyou MUST return the actual complete content
as the final answer, not a summary.\n\nBegin! This is VERY important to you,
use the tools available and give your best Final Answer, your job depends on
it!\n\nThought:"},{"role":"assistant","content":"Thought: I now can give a great
answer\nFinal Answer: 4"}],"model":"gpt-4.1-mini","response_format":{"type":"json_schema","json_schema":{"schema":{"properties":{"score":{"title":"Score","type":"integer"}},"required":["score"],"title":"ScoreOutput","type":"object","additionalProperties":false},"name":"ScoreOutput","strict":true}},"stream":false}'
headers:
accept:
- application/json
@@ -658,12 +140,12 @@ interactions:
connection:
- keep-alive
content-length:
- '541'
- '1276'
content-type:
- application/json
cookie:
- __cf_bm=9OwoBJAn84Nsq0RZdCIu06cNB6RLqor4C1.Q58nU28U-1761873091-1.0.1.1-p82_h8Vnxe0NfH5Iv6MFt.SderZj.v9VnCx_ro6ti2MGhlJOLFsPd6XhBxPsnmuV7Vt_4_uqAbE57E5f1Epl1cmGBT.0844N3CLnTwZFWQI;
_cfuvid=E4.xW3I8m58fngo4vkTKo8hmBumar1HkV.yU8KKjlZg-1761873091967-0.0.1.1-604800000
- __cf_bm=tfabkCaSRhhrfazOeTzwx40DBXVo2PjV9viPz7rNRSA-1762362173-1.0.1.1-448VqZWH1ZRKLLTCSb9Qezp9.0dIiDqeZ0CVT8Q.dmfbePuniUR.EDrrXv4lBIWqNgZ_yF1ety_tftCaKIm0Uq4ydaAKdcHVUoOMUjhtvY4;
_cfuvid=l9MNhSJeUsRd3C2.MkHdNv0aEoH19qe6RpeKp4Gf.do-1762362173542-0.0.1.1-604800000
host:
- api.openai.com
user-agent:
@@ -687,23 +169,23 @@ interactions:
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.12.10
- 3.12.9
method: POST
uri: https://api.openai.com/v1/chat/completions
response:
body:
string: !!binary |
H4sIAAAAAAAAAwAAAP//jFLBbqMwFLzzFdY7hypQklKuVS899NpK2wo59gPcGtuyH92uovz7ypAE
0t1KvXB482Y8M7x9whgoCRUD0XESvdPp3dPzZ/gty77xO7fZPj4MMs/ldYf3Lt/CKjLs7g0FnVhX
wvZOIylrJlh45IRRNbvZZuXN9fo2H4HeStSR1jpKi6ss7ZVRab7ON+m6SLPiSO+sEhigYr8Sxhjb
j99o1Ej8hIqtV6dJjyHwFqE6LzEG3uo4AR6CCsQNwWoGhTWEZvS+f4EgrMcXqIrDcsdjMwQejZpB
6wXAjbHEY9DR3esROZz9aNs6b3fhCxUaZVToao88WBPfDmQdjOghYex1zD1cRAHnbe+oJvuO43Pl
ZpKDue4ZPGFkiet5fHus6lKslkhc6bCoDQQXHcqZOXfMB6nsAkgWkf/18j/tKbYy7U/kZ0AIdISy
dh6lEpd55zWP8Ra/WztXPBqGgP5DCaxJoY+/QWLDBz0dCIQ/gbCvG2Va9M6r6UoaVxciLzdZU25z
SA7JXwAAAP//AwAXjqY4NAMAAA==
H4sIAAAAAAAAAwAAAP//jFKxbtswFNz1FcSbrcBSJDvR6g5FliJj2wQCTT5JbCmSIJ9aF4b/vaBk
W3KSAl048N4d747vmDAGSkLFQHScRO90uvu6330+dNm37fr3ITx+eUYaxKen8rk77ApYRYbd/0BB
F9adsL3TSMqaCRYeOWFUzbab/H6TZ9v7EeitRB1praO0uMvSXhmV5uu8TNdFmp3VRWeVwAAV+54w
xthxPKNRI/EAFVuvLjc9hsBbhOo6xBh4q+MN8BBUIG4IVjMorCE0o/fjCwRhPb5AVZyWMx6bIfBo
1AxaLwBujCUeg47uXs/I6epH29Z5uw9vqNAoo0JXe+TBmvh2IOtgRE8JY69j7uEmCjhve0c12Z84
PpcX5aQHc98zesHIEtcLUnku61aulkhc6bAoDgQXHcqZOrfMB6nsAkgWod+b+Uh7Cq5M+z/yMyAE
OkJZO49SidvA85jHuI3/GruWPBqGgP6XEliTQh8/QmLDBz2tCIQ/gbCvG2Va9M6raU8aVxcifyiz
5mGTQ3JK/gIAAP//AwCqSfwtNgMAAA==
headers:
CF-RAY:
- 996f47692b63edda-MXP
- 999debe15a932223-EWR
Connection:
- keep-alive
Content-Encoding:
@@ -711,7 +193,7 @@ interactions:
Content-Type:
- application/json
Date:
- Fri, 31 Oct 2025 01:11:33 GMT
- Wed, 05 Nov 2025 17:02:54 GMT
Server:
- cloudflare
Strict-Transport-Security:
@@ -727,37 +209,31 @@ interactions:
cf-cache-status:
- DYNAMIC
openai-organization:
- crewai-iuxna1
- user-hortuttj2f3qtmxyik2zxf4q
openai-processing-ms:
- '929'
- '283'
openai-project:
- proj_xitITlrFeen7zjNSzML82h9x
- proj_fL4UBWR1CMpAAdgzaSKqsVvA
openai-version:
- '2020-10-01'
x-envoy-upstream-service-time:
- '991'
- '445'
x-openai-proxy-wasm:
- v0.1
x-ratelimit-limit-project-tokens:
- '150000000'
x-ratelimit-limit-requests:
- '30000'
- '500'
x-ratelimit-limit-tokens:
- '150000000'
x-ratelimit-remaining-project-tokens:
- '149999955'
- '200000'
x-ratelimit-remaining-requests:
- '29999'
- '499'
x-ratelimit-remaining-tokens:
- '149999955'
x-ratelimit-reset-project-tokens:
- 0s
- '199779'
x-ratelimit-reset-requests:
- 2ms
- 120ms
x-ratelimit-reset-tokens:
- 0s
- 66ms
x-request-id:
- req_892607f68e764ba3846c431954608c36
- req_85126ec40c9942809be5f2cf8a5927dc
status:
code: 200
message: OK

View File

@@ -13,9 +13,7 @@ load_result = load_dotenv(override=True)
@pytest.fixture(autouse=True)
def setup_test_environment():
"""Set up test environment with a temporary directory for SQLite storage."""
import shutil
with tempfile.TemporaryDirectory() as temp_dir:
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temp_dir:
# Create the directory with proper permissions
storage_dir = Path(temp_dir) / "crewai_test_storage"
storage_dir.mkdir(parents=True, exist_ok=True)
@@ -42,9 +40,7 @@ def setup_test_environment():
yield
os.environ.pop("CREWAI_TESTING", None)
# Force cleanup of storage directory before tempfile context exits
if storage_dir.exists():
shutil.rmtree(storage_dir, ignore_errors=True)
# TemporaryDirectory handles cleanup automatically with ignore_cleanup_errors=True
def pytest_configure(config):

View File

@@ -62,18 +62,23 @@ class TestAgentEvaluator:
agents=mock_crew.agents, evaluators=[GoalAlignmentEvaluator()]
)
task_completed_event = threading.Event()
task_completed_condition = threading.Condition()
task_completed = False
@crewai_event_bus.on(TaskCompletedEvent)
async def on_task_completed(source, event):
# TaskCompletedEvent fires AFTER evaluation results are stored
task_completed_event.set()
nonlocal task_completed
with task_completed_condition:
task_completed = True
task_completed_condition.notify()
mock_crew.kickoff()
assert task_completed_event.wait(timeout=5), (
"Timeout waiting for task completion"
)
with task_completed_condition:
assert task_completed_condition.wait_for(
lambda: task_completed, timeout=5
), "Timeout waiting for task completion"
results = agent_evaluator.get_evaluation_results()

View File

@@ -0,0 +1 @@
"""Tests for LLM interceptor hooks functionality."""

View File

@@ -0,0 +1,311 @@
"""Tests for Anthropic provider with interceptor integration."""
import os
import httpx
import pytest
from crewai.llm import LLM
from crewai.llms.hooks.base import BaseInterceptor
@pytest.fixture(autouse=True)
def setup_anthropic_api_key(monkeypatch):
"""Set dummy Anthropic API key for tests that don't make real API calls."""
if "ANTHROPIC_API_KEY" not in os.environ:
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test-key-dummy")
class AnthropicTestInterceptor(BaseInterceptor[httpx.Request, httpx.Response]):
"""Test interceptor for Anthropic provider."""
def __init__(self) -> None:
"""Initialize tracking and modification state."""
self.outbound_calls: list[httpx.Request] = []
self.inbound_calls: list[httpx.Response] = []
self.custom_header_value = "anthropic-test-value"
def on_outbound(self, message: httpx.Request) -> httpx.Request:
"""Track and modify outbound Anthropic requests.
Args:
message: The outbound request.
Returns:
Modified request with custom headers.
"""
self.outbound_calls.append(message)
message.headers["X-Anthropic-Interceptor"] = self.custom_header_value
message.headers["X-Request-ID"] = "test-request-456"
return message
def on_inbound(self, message: httpx.Response) -> httpx.Response:
"""Track inbound Anthropic responses.
Args:
message: The inbound response.
Returns:
The response with tracking header.
"""
self.inbound_calls.append(message)
message.headers["X-Response-Tracked"] = "true"
return message
class TestAnthropicInterceptorIntegration:
"""Test suite for Anthropic provider with interceptor."""
def test_anthropic_llm_accepts_interceptor(self) -> None:
"""Test that Anthropic LLM accepts interceptor parameter."""
interceptor = AnthropicTestInterceptor()
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022", interceptor=interceptor)
assert llm.interceptor is interceptor
@pytest.mark.vcr(filter_headers=["authorization", "x-api-key"])
def test_anthropic_call_with_interceptor_tracks_requests(self) -> None:
"""Test that interceptor tracks Anthropic API requests."""
interceptor = AnthropicTestInterceptor()
llm = LLM(model="anthropic/claude-3-5-haiku-20241022", interceptor=interceptor)
# Make a simple completion call
result = llm.call(
messages=[{"role": "user", "content": "Say 'Hello World' and nothing else"}]
)
# Verify custom headers were added
for request in interceptor.outbound_calls:
assert "X-Anthropic-Interceptor" in request.headers
assert request.headers["X-Anthropic-Interceptor"] == "anthropic-test-value"
assert "X-Request-ID" in request.headers
assert request.headers["X-Request-ID"] == "test-request-456"
# Verify response was tracked
for response in interceptor.inbound_calls:
assert "X-Response-Tracked" in response.headers
assert response.headers["X-Response-Tracked"] == "true"
# Verify result is valid
assert result is not None
assert isinstance(result, str)
assert len(result) > 0
def test_anthropic_without_interceptor_works(self) -> None:
"""Test that Anthropic LLM works without interceptor."""
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022")
assert llm.interceptor is None
def test_multiple_anthropic_llms_different_interceptors(self) -> None:
"""Test that multiple Anthropic LLMs can have different interceptors."""
interceptor1 = AnthropicTestInterceptor()
interceptor1.custom_header_value = "claude-opus-value"
interceptor2 = AnthropicTestInterceptor()
interceptor2.custom_header_value = "claude-sonnet-value"
llm1 = LLM(model="anthropic/claude-3-opus-20240229", interceptor=interceptor1)
llm2 = LLM(model="anthropic/claude-3-5-sonnet-20241022", interceptor=interceptor2)
assert llm1.interceptor is interceptor1
assert llm2.interceptor is interceptor2
assert llm1.interceptor.custom_header_value == "claude-opus-value"
assert llm2.interceptor.custom_header_value == "claude-sonnet-value"
class AnthropicLoggingInterceptor(BaseInterceptor[httpx.Request, httpx.Response]):
"""Interceptor that logs Anthropic request/response details."""
def __init__(self) -> None:
"""Initialize logging lists."""
self.request_urls: list[str] = []
self.request_methods: list[str] = []
self.response_status_codes: list[int] = []
self.anthropic_version_headers: list[str] = []
def on_outbound(self, message: httpx.Request) -> httpx.Request:
"""Log outbound request details.
Args:
message: The outbound request.
Returns:
The request unchanged.
"""
self.request_urls.append(str(message.url))
self.request_methods.append(message.method)
if "anthropic-version" in message.headers:
self.anthropic_version_headers.append(message.headers["anthropic-version"])
return message
def on_inbound(self, message: httpx.Response) -> httpx.Response:
"""Log inbound response details.
Args:
message: The inbound response.
Returns:
The response unchanged.
"""
self.response_status_codes.append(message.status_code)
return message
class TestAnthropicLoggingInterceptor:
"""Test suite for logging interceptor with Anthropic."""
def test_logging_interceptor_instantiation(self) -> None:
"""Test that logging interceptor can be created with Anthropic LLM."""
interceptor = AnthropicLoggingInterceptor()
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022", interceptor=interceptor)
assert llm.interceptor is interceptor
assert isinstance(llm.interceptor, AnthropicLoggingInterceptor)
@pytest.mark.vcr(filter_headers=["authorization", "x-api-key"])
def test_logging_interceptor_tracks_details(self) -> None:
"""Test that logging interceptor tracks request/response details."""
interceptor = AnthropicLoggingInterceptor()
llm = LLM(model="anthropic/claude-3-5-haiku-20241022", interceptor=interceptor)
# Make a completion call
result = llm.call(messages=[{"role": "user", "content": "Count from 1 to 3"}])
# Verify URL points to Anthropic API
for url in interceptor.request_urls:
assert "anthropic" in url.lower() or "api" in url.lower()
# Verify methods are POST (messages endpoint uses POST)
for method in interceptor.request_methods:
assert method == "POST"
# Verify successful status codes
for status_code in interceptor.response_status_codes:
assert 200 <= status_code < 300
# Verify result is valid
assert result is not None
class AnthropicHeaderInterceptor(BaseInterceptor[httpx.Request, httpx.Response]):
"""Interceptor that adds Anthropic-specific headers."""
def __init__(self, workspace_id: str, user_id: str) -> None:
"""Initialize with Anthropic-specific metadata.
Args:
workspace_id: The workspace ID to inject.
user_id: The user ID to inject.
"""
self.workspace_id = workspace_id
self.user_id = user_id
def on_outbound(self, message: httpx.Request) -> httpx.Request:
"""Add custom metadata headers to request.
Args:
message: The outbound request.
Returns:
Request with metadata headers.
"""
message.headers["X-Workspace-ID"] = self.workspace_id
message.headers["X-User-ID"] = self.user_id
message.headers["X-Custom-Client"] = "crewai-interceptor"
return message
def on_inbound(self, message: httpx.Response) -> httpx.Response:
"""Pass through inbound response.
Args:
message: The inbound response.
Returns:
The response unchanged.
"""
return message
class TestAnthropicHeaderInterceptor:
"""Test suite for header interceptor with Anthropic."""
def test_header_interceptor_with_anthropic(self) -> None:
"""Test that header interceptor can be used with Anthropic LLM."""
interceptor = AnthropicHeaderInterceptor(
workspace_id="ws-789", user_id="user-012"
)
llm = LLM(model="anthropic/claude-3-5-sonnet-20241022", interceptor=interceptor)
assert llm.interceptor is interceptor
assert llm.interceptor.workspace_id == "ws-789"
assert llm.interceptor.user_id == "user-012"
def test_header_interceptor_adds_headers(self) -> None:
"""Test that header interceptor adds custom headers to requests."""
interceptor = AnthropicHeaderInterceptor(workspace_id="ws-123", user_id="u-456")
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
modified_request = interceptor.on_outbound(request)
assert "X-Workspace-ID" in modified_request.headers
assert modified_request.headers["X-Workspace-ID"] == "ws-123"
assert "X-User-ID" in modified_request.headers
assert modified_request.headers["X-User-ID"] == "u-456"
assert "X-Custom-Client" in modified_request.headers
assert modified_request.headers["X-Custom-Client"] == "crewai-interceptor"
@pytest.mark.vcr(filter_headers=["authorization", "x-api-key"])
def test_header_interceptor_with_real_call(self) -> None:
"""Test that header interceptor works with real Anthropic API call."""
interceptor = AnthropicHeaderInterceptor(workspace_id="ws-999", user_id="u-888")
llm = LLM(model="anthropic/claude-3-5-haiku-20241022", interceptor=interceptor)
# Make a simple call
result = llm.call(
messages=[{"role": "user", "content": "Reply with just the word: SUCCESS"}]
)
# Verify the call succeeded
assert result is not None
assert len(result) > 0
# Verify the interceptor was configured
assert llm.interceptor is interceptor
class TestMixedProviderInterceptors:
"""Test suite for using interceptors with different providers."""
def test_openai_and_anthropic_different_interceptors(self) -> None:
"""Test that OpenAI and Anthropic LLMs can have different interceptors."""
openai_interceptor = AnthropicTestInterceptor()
openai_interceptor.custom_header_value = "openai-specific"
anthropic_interceptor = AnthropicTestInterceptor()
anthropic_interceptor.custom_header_value = "anthropic-specific"
openai_llm = LLM(model="gpt-4", interceptor=openai_interceptor)
anthropic_llm = LLM(
model="anthropic/claude-3-5-sonnet-20241022", interceptor=anthropic_interceptor
)
assert openai_llm.interceptor is openai_interceptor
assert anthropic_llm.interceptor is anthropic_interceptor
assert openai_llm.interceptor.custom_header_value == "openai-specific"
assert anthropic_llm.interceptor.custom_header_value == "anthropic-specific"
def test_same_interceptor_different_providers(self) -> None:
"""Test that same interceptor instance can be used with multiple providers."""
shared_interceptor = AnthropicTestInterceptor()
openai_llm = LLM(model="gpt-4", interceptor=shared_interceptor)
anthropic_llm = LLM(
model="anthropic/claude-3-5-sonnet-20241022", interceptor=shared_interceptor
)
assert openai_llm.interceptor is shared_interceptor
assert anthropic_llm.interceptor is shared_interceptor
assert openai_llm.interceptor is anthropic_llm.interceptor

View File

@@ -0,0 +1,287 @@
"""Tests for base interceptor functionality."""
import httpx
import pytest
from crewai.llms.hooks.base import BaseInterceptor
class SimpleInterceptor(BaseInterceptor[httpx.Request, httpx.Response]):
"""Simple test interceptor implementation."""
def __init__(self) -> None:
"""Initialize tracking lists."""
self.outbound_calls: list[httpx.Request] = []
self.inbound_calls: list[httpx.Response] = []
def on_outbound(self, message: httpx.Request) -> httpx.Request:
"""Track outbound calls.
Args:
message: The outbound request.
Returns:
The request unchanged.
"""
self.outbound_calls.append(message)
return message
def on_inbound(self, message: httpx.Response) -> httpx.Response:
"""Track inbound calls.
Args:
message: The inbound response.
Returns:
The response unchanged.
"""
self.inbound_calls.append(message)
return message
class ModifyingInterceptor(BaseInterceptor[httpx.Request, httpx.Response]):
"""Interceptor that modifies requests and responses."""
def on_outbound(self, message: httpx.Request) -> httpx.Request:
"""Add custom header to outbound request.
Args:
message: The outbound request.
Returns:
Modified request with custom header.
"""
message.headers["X-Custom-Header"] = "test-value"
message.headers["X-Intercepted"] = "true"
return message
def on_inbound(self, message: httpx.Response) -> httpx.Response:
"""Add custom header to inbound response.
Args:
message: The inbound response.
Returns:
Modified response with custom header.
"""
message.headers["X-Response-Intercepted"] = "true"
return message
class AsyncInterceptor(BaseInterceptor[httpx.Request, httpx.Response]):
"""Interceptor with async support."""
def __init__(self) -> None:
"""Initialize tracking lists."""
self.async_outbound_calls: list[httpx.Request] = []
self.async_inbound_calls: list[httpx.Response] = []
def on_outbound(self, message: httpx.Request) -> httpx.Request:
"""Handle sync outbound.
Args:
message: The outbound request.
Returns:
The request unchanged.
"""
return message
def on_inbound(self, message: httpx.Response) -> httpx.Response:
"""Handle sync inbound.
Args:
message: The inbound response.
Returns:
The response unchanged.
"""
return message
async def aon_outbound(self, message: httpx.Request) -> httpx.Request:
"""Handle async outbound.
Args:
message: The outbound request.
Returns:
Modified request with async header.
"""
self.async_outbound_calls.append(message)
message.headers["X-Async-Outbound"] = "true"
return message
async def aon_inbound(self, message: httpx.Response) -> httpx.Response:
"""Handle async inbound.
Args:
message: The inbound response.
Returns:
Modified response with async header.
"""
self.async_inbound_calls.append(message)
message.headers["X-Async-Inbound"] = "true"
return message
class TestBaseInterceptor:
"""Test suite for BaseInterceptor class."""
def test_interceptor_instantiation(self) -> None:
"""Test that interceptor can be instantiated."""
interceptor = SimpleInterceptor()
assert interceptor is not None
assert isinstance(interceptor, BaseInterceptor)
def test_on_outbound_called(self) -> None:
"""Test that on_outbound is called and tracks requests."""
interceptor = SimpleInterceptor()
request = httpx.Request("GET", "https://api.example.com/test")
result = interceptor.on_outbound(request)
assert len(interceptor.outbound_calls) == 1
assert interceptor.outbound_calls[0] is request
assert result is request
def test_on_inbound_called(self) -> None:
"""Test that on_inbound is called and tracks responses."""
interceptor = SimpleInterceptor()
response = httpx.Response(200, json={"status": "ok"})
result = interceptor.on_inbound(response)
assert len(interceptor.inbound_calls) == 1
assert interceptor.inbound_calls[0] is response
assert result is response
def test_multiple_outbound_calls(self) -> None:
"""Test that interceptor tracks multiple outbound calls."""
interceptor = SimpleInterceptor()
requests = [
httpx.Request("GET", "https://api.example.com/1"),
httpx.Request("POST", "https://api.example.com/2"),
httpx.Request("PUT", "https://api.example.com/3"),
]
for req in requests:
interceptor.on_outbound(req)
assert len(interceptor.outbound_calls) == 3
assert interceptor.outbound_calls == requests
def test_multiple_inbound_calls(self) -> None:
"""Test that interceptor tracks multiple inbound calls."""
interceptor = SimpleInterceptor()
responses = [
httpx.Response(200, json={"id": 1}),
httpx.Response(201, json={"id": 2}),
httpx.Response(404, json={"error": "not found"}),
]
for resp in responses:
interceptor.on_inbound(resp)
assert len(interceptor.inbound_calls) == 3
assert interceptor.inbound_calls == responses
class TestModifyingInterceptor:
"""Test suite for interceptor that modifies messages."""
def test_outbound_header_modification(self) -> None:
"""Test that interceptor can add headers to outbound requests."""
interceptor = ModifyingInterceptor()
request = httpx.Request("GET", "https://api.example.com/test")
result = interceptor.on_outbound(request)
assert result is request
assert "X-Custom-Header" in result.headers
assert result.headers["X-Custom-Header"] == "test-value"
assert "X-Intercepted" in result.headers
assert result.headers["X-Intercepted"] == "true"
def test_inbound_header_modification(self) -> None:
"""Test that interceptor can add headers to inbound responses."""
interceptor = ModifyingInterceptor()
response = httpx.Response(200, json={"status": "ok"})
result = interceptor.on_inbound(response)
assert result is response
assert "X-Response-Intercepted" in result.headers
assert result.headers["X-Response-Intercepted"] == "true"
def test_preserves_existing_headers(self) -> None:
"""Test that interceptor preserves existing headers."""
interceptor = ModifyingInterceptor()
request = httpx.Request(
"GET",
"https://api.example.com/test",
headers={"Authorization": "Bearer token123", "Content-Type": "application/json"},
)
result = interceptor.on_outbound(request)
assert result.headers["Authorization"] == "Bearer token123"
assert result.headers["Content-Type"] == "application/json"
assert result.headers["X-Custom-Header"] == "test-value"
class TestAsyncInterceptor:
"""Test suite for async interceptor functionality."""
def test_sync_methods_work(self) -> None:
"""Test that sync methods still work on async interceptor."""
interceptor = AsyncInterceptor()
request = httpx.Request("GET", "https://api.example.com/test")
response = httpx.Response(200)
req_result = interceptor.on_outbound(request)
resp_result = interceptor.on_inbound(response)
assert req_result is request
assert resp_result is response
@pytest.mark.asyncio
async def test_async_outbound(self) -> None:
"""Test async outbound hook."""
interceptor = AsyncInterceptor()
request = httpx.Request("GET", "https://api.example.com/test")
result = await interceptor.aon_outbound(request)
assert result is request
assert len(interceptor.async_outbound_calls) == 1
assert interceptor.async_outbound_calls[0] is request
assert "X-Async-Outbound" in result.headers
assert result.headers["X-Async-Outbound"] == "true"
@pytest.mark.asyncio
async def test_async_inbound(self) -> None:
"""Test async inbound hook."""
interceptor = AsyncInterceptor()
response = httpx.Response(200, json={"status": "ok"})
result = await interceptor.aon_inbound(response)
assert result is response
assert len(interceptor.async_inbound_calls) == 1
assert interceptor.async_inbound_calls[0] is response
assert "X-Async-Inbound" in result.headers
assert result.headers["X-Async-Inbound"] == "true"
@pytest.mark.asyncio
async def test_default_async_not_implemented(self) -> None:
"""Test that default async methods raise NotImplementedError."""
interceptor = SimpleInterceptor()
request = httpx.Request("GET", "https://api.example.com/test")
response = httpx.Response(200)
with pytest.raises(NotImplementedError):
await interceptor.aon_outbound(request)
with pytest.raises(NotImplementedError):
await interceptor.aon_inbound(response)

View File

@@ -0,0 +1,262 @@
"""Tests for OpenAI provider with interceptor integration."""
import httpx
import pytest
from crewai.llm import LLM
from crewai.llms.hooks.base import BaseInterceptor
class OpenAITestInterceptor(BaseInterceptor[httpx.Request, httpx.Response]):
"""Test interceptor for OpenAI provider."""
def __init__(self) -> None:
"""Initialize tracking and modification state."""
self.outbound_calls: list[httpx.Request] = []
self.inbound_calls: list[httpx.Response] = []
self.custom_header_value = "openai-test-value"
def on_outbound(self, message: httpx.Request) -> httpx.Request:
"""Track and modify outbound OpenAI requests.
Args:
message: The outbound request.
Returns:
Modified request with custom headers.
"""
self.outbound_calls.append(message)
message.headers["X-OpenAI-Interceptor"] = self.custom_header_value
message.headers["X-Request-ID"] = "test-request-123"
return message
def on_inbound(self, message: httpx.Response) -> httpx.Response:
"""Track inbound OpenAI responses.
Args:
message: The inbound response.
Returns:
The response with tracking header.
"""
self.inbound_calls.append(message)
message.headers["X-Response-Tracked"] = "true"
return message
class TestOpenAIInterceptorIntegration:
"""Test suite for OpenAI provider with interceptor."""
def test_openai_llm_accepts_interceptor(self) -> None:
"""Test that OpenAI LLM accepts interceptor parameter."""
interceptor = OpenAITestInterceptor()
llm = LLM(model="gpt-4", interceptor=interceptor)
assert llm.interceptor is interceptor
@pytest.mark.vcr(filter_headers=["authorization"])
def test_openai_call_with_interceptor_tracks_requests(self) -> None:
"""Test that interceptor tracks OpenAI API requests."""
interceptor = OpenAITestInterceptor()
llm = LLM(model="gpt-4o-mini", interceptor=interceptor)
# Make a simple completion call
result = llm.call(
messages=[{"role": "user", "content": "Say 'Hello World' and nothing else"}]
)
# Verify custom headers were added
for request in interceptor.outbound_calls:
assert "X-OpenAI-Interceptor" in request.headers
assert request.headers["X-OpenAI-Interceptor"] == "openai-test-value"
assert "X-Request-ID" in request.headers
assert request.headers["X-Request-ID"] == "test-request-123"
# Verify response was tracked
for response in interceptor.inbound_calls:
assert "X-Response-Tracked" in response.headers
assert response.headers["X-Response-Tracked"] == "true"
# Verify result is valid
assert result is not None
assert isinstance(result, str)
assert len(result) > 0
def test_openai_without_interceptor_works(self) -> None:
"""Test that OpenAI LLM works without interceptor."""
llm = LLM(model="gpt-4")
assert llm.interceptor is None
def test_multiple_openai_llms_different_interceptors(self) -> None:
"""Test that multiple OpenAI LLMs can have different interceptors."""
interceptor1 = OpenAITestInterceptor()
interceptor1.custom_header_value = "llm1-value"
interceptor2 = OpenAITestInterceptor()
interceptor2.custom_header_value = "llm2-value"
llm1 = LLM(model="gpt-4", interceptor=interceptor1)
llm2 = LLM(model="gpt-3.5-turbo", interceptor=interceptor2)
assert llm1.interceptor is interceptor1
assert llm2.interceptor is interceptor2
assert llm1.interceptor.custom_header_value == "llm1-value"
assert llm2.interceptor.custom_header_value == "llm2-value"
class LoggingInterceptor(BaseInterceptor[httpx.Request, httpx.Response]):
"""Interceptor that logs request/response details for testing."""
def __init__(self) -> None:
"""Initialize logging lists."""
self.request_urls: list[str] = []
self.request_methods: list[str] = []
self.response_status_codes: list[int] = []
def on_outbound(self, message: httpx.Request) -> httpx.Request:
"""Log outbound request details.
Args:
message: The outbound request.
Returns:
The request unchanged.
"""
self.request_urls.append(str(message.url))
self.request_methods.append(message.method)
return message
def on_inbound(self, message: httpx.Response) -> httpx.Response:
"""Log inbound response details.
Args:
message: The inbound response.
Returns:
The response unchanged.
"""
self.response_status_codes.append(message.status_code)
return message
class TestOpenAILoggingInterceptor:
"""Test suite for logging interceptor with OpenAI."""
def test_logging_interceptor_instantiation(self) -> None:
"""Test that logging interceptor can be created with OpenAI LLM."""
interceptor = LoggingInterceptor()
llm = LLM(model="gpt-4", interceptor=interceptor)
assert llm.interceptor is interceptor
assert isinstance(llm.interceptor, LoggingInterceptor)
@pytest.mark.vcr(filter_headers=["authorization"])
def test_logging_interceptor_tracks_details(self) -> None:
"""Test that logging interceptor tracks request/response details."""
interceptor = LoggingInterceptor()
llm = LLM(model="gpt-4o-mini", interceptor=interceptor)
# Make a completion call
result = llm.call(
messages=[{"role": "user", "content": "Count from 1 to 3"}]
)
# Verify URL points to OpenAI API
for url in interceptor.request_urls:
assert "openai" in url.lower() or "api" in url.lower()
# Verify methods are POST (chat completions use POST)
for method in interceptor.request_methods:
assert method == "POST"
# Verify successful status codes
for status_code in interceptor.response_status_codes:
assert 200 <= status_code < 300
# Verify result is valid
assert result is not None
class AuthInterceptor(BaseInterceptor[httpx.Request, httpx.Response]):
"""Interceptor that adds authentication headers."""
def __init__(self, api_key: str, org_id: str) -> None:
"""Initialize with auth credentials.
Args:
api_key: The API key to inject.
org_id: The organization ID to inject.
"""
self.api_key = api_key
self.org_id = org_id
def on_outbound(self, message: httpx.Request) -> httpx.Request:
"""Add authentication headers to request.
Args:
message: The outbound request.
Returns:
Request with auth headers.
"""
message.headers["X-Custom-API-Key"] = self.api_key
message.headers["X-Organization-ID"] = self.org_id
return message
def on_inbound(self, message: httpx.Response) -> httpx.Response:
"""Pass through inbound response.
Args:
message: The inbound response.
Returns:
The response unchanged.
"""
return message
class TestOpenAIAuthInterceptor:
"""Test suite for authentication interceptor with OpenAI."""
def test_auth_interceptor_with_openai(self) -> None:
"""Test that auth interceptor can be used with OpenAI LLM."""
interceptor = AuthInterceptor(api_key="custom-key-123", org_id="org-456")
llm = LLM(model="gpt-4", interceptor=interceptor)
assert llm.interceptor is interceptor
assert llm.interceptor.api_key == "custom-key-123"
assert llm.interceptor.org_id == "org-456"
def test_auth_interceptor_adds_headers(self) -> None:
"""Test that auth interceptor adds custom headers to requests."""
interceptor = AuthInterceptor(api_key="test-key", org_id="test-org")
request = httpx.Request("POST", "https://api.openai.com/v1/chat/completions")
modified_request = interceptor.on_outbound(request)
assert "X-Custom-API-Key" in modified_request.headers
assert modified_request.headers["X-Custom-API-Key"] == "test-key"
assert "X-Organization-ID" in modified_request.headers
assert modified_request.headers["X-Organization-ID"] == "test-org"
@pytest.mark.vcr(filter_headers=["authorization"])
def test_auth_interceptor_with_real_call(self) -> None:
"""Test that auth interceptor works with real OpenAI API call."""
interceptor = AuthInterceptor(api_key="custom-123", org_id="org-789")
llm = LLM(model="gpt-4o-mini", interceptor=interceptor)
# Make a simple call
result = llm.call(
messages=[{"role": "user", "content": "Reply with just the word: SUCCESS"}]
)
# Verify the call succeeded
assert result is not None
assert len(result) > 0
# Verify headers were added to outbound requests
# (We can't directly inspect the request sent to OpenAI in this test,
# but we verify the interceptor was configured and the call succeeded)
assert llm.interceptor is interceptor

View File

@@ -0,0 +1,248 @@
"""Tests for transport layer with interceptor integration."""
from unittest.mock import Mock
import httpx
import pytest
from crewai.llms.hooks.base import BaseInterceptor
from crewai.llms.hooks.transport import AsyncHTTPransport, HTTPTransport
class TrackingInterceptor(BaseInterceptor[httpx.Request, httpx.Response]):
"""Test interceptor that tracks all calls."""
def __init__(self) -> None:
"""Initialize tracking lists."""
self.outbound_calls: list[httpx.Request] = []
self.inbound_calls: list[httpx.Response] = []
self.async_outbound_calls: list[httpx.Request] = []
self.async_inbound_calls: list[httpx.Response] = []
def on_outbound(self, message: httpx.Request) -> httpx.Request:
"""Track outbound calls and add header.
Args:
message: The outbound request.
Returns:
Modified request with tracking header.
"""
self.outbound_calls.append(message)
message.headers["X-Intercepted-Sync"] = "true"
return message
def on_inbound(self, message: httpx.Response) -> httpx.Response:
"""Track inbound calls.
Args:
message: The inbound response.
Returns:
The response with tracking header.
"""
self.inbound_calls.append(message)
message.headers["X-Response-Intercepted-Sync"] = "true"
return message
async def aon_outbound(self, message: httpx.Request) -> httpx.Request:
"""Track async outbound calls and add header.
Args:
message: The outbound request.
Returns:
Modified request with tracking header.
"""
self.async_outbound_calls.append(message)
message.headers["X-Intercepted-Async"] = "true"
return message
async def aon_inbound(self, message: httpx.Response) -> httpx.Response:
"""Track async inbound calls.
Args:
message: The inbound response.
Returns:
The response with tracking header.
"""
self.async_inbound_calls.append(message)
message.headers["X-Response-Intercepted-Async"] = "true"
return message
class TestHTTPTransport:
"""Test suite for sync HTTPTransport with interceptor."""
def test_transport_instantiation(self) -> None:
"""Test that transport can be instantiated with interceptor."""
interceptor = TrackingInterceptor()
transport = HTTPTransport(interceptor=interceptor)
assert transport.interceptor is interceptor
def test_transport_requires_interceptor(self) -> None:
"""Test that transport requires interceptor parameter."""
# HTTPTransport requires an interceptor parameter
with pytest.raises(TypeError):
HTTPTransport()
def test_interceptor_called_on_request(self) -> None:
"""Test that interceptor hooks are called during request handling."""
interceptor = TrackingInterceptor()
transport = HTTPTransport(interceptor=interceptor)
# Create a mock parent transport that returns a response
mock_response = httpx.Response(200, json={"success": True})
mock_parent_handle = Mock(return_value=mock_response)
# Monkey-patch the parent's handle_request
original_handle = httpx.HTTPTransport.handle_request
httpx.HTTPTransport.handle_request = mock_parent_handle
try:
request = httpx.Request("GET", "https://api.example.com/test")
response = transport.handle_request(request)
# Verify interceptor was called
assert len(interceptor.outbound_calls) == 1
assert len(interceptor.inbound_calls) == 1
assert interceptor.outbound_calls[0] is request
assert interceptor.inbound_calls[0] is response
# Verify headers were added
assert "X-Intercepted-Sync" in request.headers
assert request.headers["X-Intercepted-Sync"] == "true"
assert "X-Response-Intercepted-Sync" in response.headers
assert response.headers["X-Response-Intercepted-Sync"] == "true"
finally:
# Restore original method
httpx.HTTPTransport.handle_request = original_handle
class TestAsyncHTTPTransport:
"""Test suite for async AsyncHTTPransport with interceptor."""
def test_async_transport_instantiation(self) -> None:
"""Test that async transport can be instantiated with interceptor."""
interceptor = TrackingInterceptor()
transport = AsyncHTTPransport(interceptor=interceptor)
assert transport.interceptor is interceptor
def test_async_transport_requires_interceptor(self) -> None:
"""Test that async transport requires interceptor parameter."""
# AsyncHTTPransport requires an interceptor parameter
with pytest.raises(TypeError):
AsyncHTTPransport()
@pytest.mark.asyncio
async def test_async_interceptor_called_on_request(self) -> None:
"""Test that async interceptor hooks are called during request handling."""
interceptor = TrackingInterceptor()
transport = AsyncHTTPransport(interceptor=interceptor)
# Create a mock parent transport that returns a response
mock_response = httpx.Response(200, json={"success": True})
async def mock_handle(*args, **kwargs):
return mock_response
mock_parent_handle = Mock(side_effect=mock_handle)
# Monkey-patch the parent's handle_async_request
original_handle = httpx.AsyncHTTPTransport.handle_async_request
httpx.AsyncHTTPTransport.handle_async_request = mock_parent_handle
try:
request = httpx.Request("GET", "https://api.example.com/test")
response = await transport.handle_async_request(request)
# Verify async interceptor was called
assert len(interceptor.async_outbound_calls) == 1
assert len(interceptor.async_inbound_calls) == 1
assert interceptor.async_outbound_calls[0] is request
assert interceptor.async_inbound_calls[0] is response
# Verify sync interceptor was NOT called
assert len(interceptor.outbound_calls) == 0
assert len(interceptor.inbound_calls) == 0
# Verify async headers were added
assert "X-Intercepted-Async" in request.headers
assert request.headers["X-Intercepted-Async"] == "true"
assert "X-Response-Intercepted-Async" in response.headers
assert response.headers["X-Response-Intercepted-Async"] == "true"
finally:
# Restore original method
httpx.AsyncHTTPTransport.handle_async_request = original_handle
class TestTransportIntegration:
"""Test suite for transport integration scenarios."""
def test_multiple_requests_same_interceptor(self) -> None:
"""Test that multiple requests through same interceptor are tracked."""
interceptor = TrackingInterceptor()
transport = HTTPTransport(interceptor=interceptor)
mock_response = httpx.Response(200)
mock_parent_handle = Mock(return_value=mock_response)
original_handle = httpx.HTTPTransport.handle_request
httpx.HTTPTransport.handle_request = mock_parent_handle
try:
# Make multiple requests
requests = [
httpx.Request("GET", "https://api.example.com/1"),
httpx.Request("POST", "https://api.example.com/2"),
httpx.Request("PUT", "https://api.example.com/3"),
]
for req in requests:
transport.handle_request(req)
# Verify all requests were intercepted
assert len(interceptor.outbound_calls) == 3
assert len(interceptor.inbound_calls) == 3
assert interceptor.outbound_calls == requests
finally:
httpx.HTTPTransport.handle_request = original_handle
@pytest.mark.asyncio
async def test_multiple_async_requests_same_interceptor(self) -> None:
"""Test that multiple async requests through same interceptor are tracked."""
interceptor = TrackingInterceptor()
transport = AsyncHTTPransport(interceptor=interceptor)
mock_response = httpx.Response(200)
async def mock_handle(*args, **kwargs):
return mock_response
mock_parent_handle = Mock(side_effect=mock_handle)
original_handle = httpx.AsyncHTTPTransport.handle_async_request
httpx.AsyncHTTPTransport.handle_async_request = mock_parent_handle
try:
# Make multiple async requests
requests = [
httpx.Request("GET", "https://api.example.com/1"),
httpx.Request("POST", "https://api.example.com/2"),
httpx.Request("DELETE", "https://api.example.com/3"),
]
for req in requests:
await transport.handle_async_request(req)
# Verify all requests were intercepted
assert len(interceptor.async_outbound_calls) == 3
assert len(interceptor.async_inbound_calls) == 3
assert interceptor.async_outbound_calls == requests
finally:
httpx.AsyncHTTPTransport.handle_async_request = original_handle

View File

@@ -0,0 +1,319 @@
"""Tests for interceptor behavior with unsupported providers."""
import os
import httpx
import pytest
from crewai.llm import LLM
from crewai.llms.hooks.base import BaseInterceptor
@pytest.fixture(autouse=True)
def setup_provider_api_keys(monkeypatch):
"""Set dummy API keys for providers that require them."""
if "OPENAI_API_KEY" not in os.environ:
monkeypatch.setenv("OPENAI_API_KEY", "sk-test-key-dummy")
if "ANTHROPIC_API_KEY" not in os.environ:
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test-key-dummy")
if "GOOGLE_API_KEY" not in os.environ:
monkeypatch.setenv("GOOGLE_API_KEY", "test-google-key-dummy")
class DummyInterceptor(BaseInterceptor[httpx.Request, httpx.Response]):
"""Simple dummy interceptor for testing."""
def on_outbound(self, message: httpx.Request) -> httpx.Request:
"""Pass through outbound request.
Args:
message: The outbound request.
Returns:
The request unchanged.
"""
message.headers["X-Dummy"] = "true"
return message
def on_inbound(self, message: httpx.Response) -> httpx.Response:
"""Pass through inbound response.
Args:
message: The inbound response.
Returns:
The response unchanged.
"""
return message
class TestAzureProviderInterceptor:
"""Test suite for Azure provider with interceptor (unsupported)."""
def test_azure_llm_accepts_interceptor_parameter(self) -> None:
"""Test that Azure LLM raises NotImplementedError with interceptor."""
interceptor = DummyInterceptor()
# Azure provider should raise NotImplementedError
with pytest.raises(NotImplementedError) as exc_info:
LLM(
model="azure/gpt-4",
interceptor=interceptor,
api_key="test-key",
endpoint="https://test.openai.azure.com/openai/deployments/gpt-4",
)
assert "interceptor" in str(exc_info.value).lower()
def test_azure_raises_not_implemented_on_initialization(self) -> None:
"""Test that Azure raises NotImplementedError when interceptor is used."""
interceptor = DummyInterceptor()
with pytest.raises(NotImplementedError) as exc_info:
LLM(
model="azure/gpt-4",
interceptor=interceptor,
api_key="test-key",
endpoint="https://test.openai.azure.com/openai/deployments/gpt-4",
)
error_msg = str(exc_info.value).lower()
assert "interceptor" in error_msg
assert "azure" in error_msg
def test_azure_without_interceptor_works(self) -> None:
"""Test that Azure LLM works without interceptor."""
llm = LLM(
model="azure/gpt-4",
api_key="test-key",
endpoint="https://test.openai.azure.com/openai/deployments/gpt-4",
)
# Azure provider doesn't have interceptor attribute
assert not hasattr(llm, 'interceptor') or llm.interceptor is None
class TestBedrockProviderInterceptor:
"""Test suite for Bedrock provider with interceptor (unsupported)."""
def test_bedrock_llm_accepts_interceptor_parameter(self) -> None:
"""Test that Bedrock LLM raises NotImplementedError with interceptor."""
interceptor = DummyInterceptor()
# Bedrock provider should raise NotImplementedError
with pytest.raises(NotImplementedError) as exc_info:
LLM(
model="bedrock/anthropic.claude-3-sonnet-20240229-v1:0",
interceptor=interceptor,
aws_access_key_id="test-access-key",
aws_secret_access_key="test-secret-key",
aws_region_name="us-east-1",
)
error_msg = str(exc_info.value).lower()
assert "interceptor" in error_msg
assert "bedrock" in error_msg
def test_bedrock_raises_not_implemented_on_initialization(self) -> None:
"""Test that Bedrock raises NotImplementedError when interceptor is used."""
interceptor = DummyInterceptor()
with pytest.raises(NotImplementedError) as exc_info:
LLM(
model="bedrock/anthropic.claude-3-sonnet-20240229-v1:0",
interceptor=interceptor,
aws_access_key_id="test-access-key",
aws_secret_access_key="test-secret-key",
aws_region_name="us-east-1",
)
error_msg = str(exc_info.value).lower()
assert "interceptor" in error_msg
assert "bedrock" in error_msg
def test_bedrock_without_interceptor_works(self) -> None:
"""Test that Bedrock LLM works without interceptor."""
llm = LLM(
model="bedrock/anthropic.claude-3-sonnet-20240229-v1:0",
aws_access_key_id="test-access-key",
aws_secret_access_key="test-secret-key",
aws_region_name="us-east-1",
)
# Bedrock provider doesn't have interceptor attribute
assert not hasattr(llm, 'interceptor') or llm.interceptor is None
class TestGeminiProviderInterceptor:
"""Test suite for Gemini provider with interceptor (unsupported)."""
def test_gemini_llm_accepts_interceptor_parameter(self) -> None:
"""Test that Gemini LLM raises NotImplementedError with interceptor."""
interceptor = DummyInterceptor()
# Gemini provider should raise NotImplementedError
with pytest.raises(NotImplementedError) as exc_info:
LLM(
model="gemini/gemini-pro",
interceptor=interceptor,
api_key="test-gemini-key",
)
error_msg = str(exc_info.value).lower()
assert "interceptor" in error_msg
assert "gemini" in error_msg
def test_gemini_raises_not_implemented_on_initialization(self) -> None:
"""Test that Gemini raises NotImplementedError when interceptor is used."""
interceptor = DummyInterceptor()
with pytest.raises(NotImplementedError) as exc_info:
LLM(
model="gemini/gemini-pro",
interceptor=interceptor,
api_key="test-gemini-key",
)
error_msg = str(exc_info.value).lower()
assert "interceptor" in error_msg
assert "gemini" in error_msg
def test_gemini_without_interceptor_works(self) -> None:
"""Test that Gemini LLM works without interceptor."""
llm = LLM(
model="gemini/gemini-pro",
api_key="test-gemini-key",
)
# Gemini provider doesn't have interceptor attribute
assert not hasattr(llm, 'interceptor') or llm.interceptor is None
class TestUnsupportedProviderMessages:
"""Test suite for error messages from unsupported providers."""
def test_azure_error_message_is_clear(self) -> None:
"""Test that Azure error message clearly states lack of support."""
interceptor = DummyInterceptor()
with pytest.raises(NotImplementedError) as exc_info:
LLM(
model="azure/gpt-4",
interceptor=interceptor,
api_key="test-key",
endpoint="https://test.openai.azure.com/openai/deployments/gpt-4",
)
error_message = str(exc_info.value).lower()
assert "azure" in error_message
assert "interceptor" in error_message
def test_bedrock_error_message_is_clear(self) -> None:
"""Test that Bedrock error message clearly states lack of support."""
interceptor = DummyInterceptor()
with pytest.raises(NotImplementedError) as exc_info:
LLM(
model="bedrock/anthropic.claude-3-sonnet-20240229-v1:0",
interceptor=interceptor,
aws_access_key_id="test-access-key",
aws_secret_access_key="test-secret-key",
aws_region_name="us-east-1",
)
error_message = str(exc_info.value).lower()
assert "bedrock" in error_message
assert "interceptor" in error_message
def test_gemini_error_message_is_clear(self) -> None:
"""Test that Gemini error message clearly states lack of support."""
interceptor = DummyInterceptor()
with pytest.raises(NotImplementedError) as exc_info:
LLM(
model="gemini/gemini-pro",
interceptor=interceptor,
api_key="test-gemini-key",
)
error_message = str(exc_info.value).lower()
assert "gemini" in error_message
assert "interceptor" in error_message
class TestProviderSupportMatrix:
"""Test suite to document which providers support interceptors."""
def test_supported_providers_accept_interceptor(self) -> None:
"""Test that supported providers accept and use interceptors."""
interceptor = DummyInterceptor()
# OpenAI - SUPPORTED
openai_llm = LLM(model="gpt-4", interceptor=interceptor)
assert openai_llm.interceptor is interceptor
# Anthropic - SUPPORTED
anthropic_llm = LLM(model="anthropic/claude-3-opus-20240229", interceptor=interceptor)
assert anthropic_llm.interceptor is interceptor
def test_unsupported_providers_raise_error(self) -> None:
"""Test that unsupported providers raise NotImplementedError."""
interceptor = DummyInterceptor()
# Azure - NOT SUPPORTED
with pytest.raises(NotImplementedError):
LLM(
model="azure/gpt-4",
interceptor=interceptor,
api_key="test",
endpoint="https://test.openai.azure.com/openai/deployments/gpt-4",
)
# Bedrock - NOT SUPPORTED
with pytest.raises(NotImplementedError):
LLM(
model="bedrock/anthropic.claude-3-sonnet-20240229-v1:0",
interceptor=interceptor,
aws_access_key_id="test",
aws_secret_access_key="test",
aws_region_name="us-east-1",
)
# Gemini - NOT SUPPORTED
with pytest.raises(NotImplementedError):
LLM(
model="gemini/gemini-pro",
interceptor=interceptor,
api_key="test",
)
def test_all_providers_work_without_interceptor(self) -> None:
"""Test that all providers work normally without interceptor."""
# OpenAI
openai_llm = LLM(model="gpt-4")
assert openai_llm.interceptor is None
# Anthropic
anthropic_llm = LLM(model="anthropic/claude-3-opus-20240229")
assert anthropic_llm.interceptor is None
# Azure - doesn't have interceptor attribute
azure_llm = LLM(
model="azure/gpt-4",
api_key="test",
endpoint="https://test.openai.azure.com/openai/deployments/gpt-4",
)
assert not hasattr(azure_llm, 'interceptor') or azure_llm.interceptor is None
# Bedrock - doesn't have interceptor attribute
bedrock_llm = LLM(
model="bedrock/anthropic.claude-3-sonnet-20240229-v1:0",
aws_access_key_id="test",
aws_secret_access_key="test",
aws_region_name="us-east-1",
)
assert not hasattr(bedrock_llm, 'interceptor') or bedrock_llm.interceptor is None
# Gemini - doesn't have interceptor attribute
gemini_llm = LLM(model="gemini/gemini-pro", api_key="test")
assert not hasattr(gemini_llm, 'interceptor') or gemini_llm.interceptor is None

View File

@@ -0,0 +1,94 @@
"""Test callback decorator with TaskOutput arguments."""
from unittest.mock import MagicMock, patch
from crewai import Agent, Crew, Task
from crewai.project import CrewBase, callback, task
from crewai.tasks.output_format import OutputFormat
from crewai.tasks.task_output import TaskOutput
def test_callback_decorator_with_taskoutput() -> None:
"""Test that @callback decorator works with TaskOutput arguments."""
@CrewBase
class TestCrew:
"""Test crew with callback."""
callback_called = False
callback_output = None
@callback
def task_callback(self, output: TaskOutput) -> None:
"""Test callback that receives TaskOutput."""
self.callback_called = True
self.callback_output = output
@task
def test_task(self) -> Task:
"""Test task with callback."""
return Task(
description="Test task",
expected_output="Test output",
callback=self.task_callback,
)
test_crew = TestCrew()
task_instance = test_crew.test_task()
test_output = TaskOutput(
description="Test task",
agent="Test Agent",
raw="test result",
output_format=OutputFormat.RAW,
)
task_instance.callback(test_output)
assert test_crew.callback_called
assert test_crew.callback_output == test_output
def test_callback_decorator_with_taskoutput_integration() -> None:
"""Integration test for callback with actual task execution."""
@CrewBase
class TestCrew:
"""Test crew with callback integration."""
callback_called = False
received_output: TaskOutput | None = None
@callback
def task_callback(self, output: TaskOutput) -> None:
"""Callback executed after task completion."""
self.callback_called = True
self.received_output = output
@task
def test_task(self) -> Task:
"""Test task."""
return Task(
description="Test task",
expected_output="Test output",
callback=self.task_callback,
)
test_crew = TestCrew()
agent = Agent(
role="Test Agent",
goal="Test goal",
backstory="Test backstory",
)
task_instance = test_crew.test_task()
task_instance.agent = agent
with patch.object(Agent, "execute_task") as mock_execute:
mock_execute.return_value = "test result"
task_instance.execute_sync()
assert test_crew.callback_called
assert test_crew.received_output is not None
assert test_crew.received_output.raw == "test result"