mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-07 21:54:10 -05:00
feat: add streaming tool call events; fix provider id tracking; add tests and cassettes
Adds support for streaming tool call events with test coverage, fixes tool-stream ID tracking (including OpenAI-style tracking for Azure), improves Gemini tool calling + streaming tests, adds Anthropic tests, generates Azure cassettes, and fixes Azure cassette URIs.
This commit is contained in:
@@ -354,8 +354,17 @@ class BaseLLM(ABC):
|
||||
from_task: Task | None = None,
|
||||
from_agent: Agent | None = None,
|
||||
tool_call: dict[str, Any] | None = None,
|
||||
call_type: LLMCallType | None = None,
|
||||
) -> None:
|
||||
"""Emit stream chunk event."""
|
||||
"""Emit stream chunk event.
|
||||
|
||||
Args:
|
||||
chunk: The text content of the chunk.
|
||||
from_task: The task that initiated the call.
|
||||
from_agent: The agent that initiated the call.
|
||||
tool_call: Tool call information if this is a tool call chunk.
|
||||
call_type: The type of LLM call (LLM_CALL or TOOL_CALL).
|
||||
"""
|
||||
if not hasattr(crewai_event_bus, "emit"):
|
||||
raise ValueError("crewai_event_bus does not have an emit method") from None
|
||||
|
||||
@@ -366,6 +375,7 @@ class BaseLLM(ABC):
|
||||
tool_call=tool_call,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
call_type=call_type,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -598,6 +598,8 @@ class AnthropicCompletion(BaseLLM):
|
||||
# (the SDK sets it internally)
|
||||
stream_params = {k: v for k, v in params.items() if k != "stream"}
|
||||
|
||||
current_tool_calls: dict[int, dict[str, Any]] = {}
|
||||
|
||||
# Make streaming API call
|
||||
with self.client.messages.stream(**stream_params) as stream:
|
||||
for event in stream:
|
||||
@@ -610,6 +612,55 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_agent=from_agent,
|
||||
)
|
||||
|
||||
if event.type == "content_block_start":
|
||||
block = event.content_block
|
||||
if block.type == "tool_use":
|
||||
block_index = event.index
|
||||
current_tool_calls[block_index] = {
|
||||
"id": block.id,
|
||||
"name": block.name,
|
||||
"arguments": "",
|
||||
"index": block_index,
|
||||
}
|
||||
self._emit_stream_chunk_event(
|
||||
chunk="",
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
tool_call={
|
||||
"id": block.id,
|
||||
"function": {
|
||||
"name": block.name,
|
||||
"arguments": "",
|
||||
},
|
||||
"type": "function",
|
||||
"index": block_index,
|
||||
},
|
||||
call_type=LLMCallType.TOOL_CALL,
|
||||
)
|
||||
elif event.type == "content_block_delta":
|
||||
if event.delta.type == "input_json_delta":
|
||||
block_index = event.index
|
||||
partial_json = event.delta.partial_json
|
||||
if block_index in current_tool_calls and partial_json:
|
||||
current_tool_calls[block_index]["arguments"] += partial_json
|
||||
self._emit_stream_chunk_event(
|
||||
chunk=partial_json,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
tool_call={
|
||||
"id": current_tool_calls[block_index]["id"],
|
||||
"function": {
|
||||
"name": current_tool_calls[block_index]["name"],
|
||||
"arguments": current_tool_calls[block_index][
|
||||
"arguments"
|
||||
],
|
||||
},
|
||||
"type": "function",
|
||||
"index": block_index,
|
||||
},
|
||||
call_type=LLMCallType.TOOL_CALL,
|
||||
)
|
||||
|
||||
final_message: Message = stream.get_final_message()
|
||||
|
||||
thinking_blocks: list[ThinkingBlock] = []
|
||||
@@ -941,6 +992,8 @@ class AnthropicCompletion(BaseLLM):
|
||||
|
||||
stream_params = {k: v for k, v in params.items() if k != "stream"}
|
||||
|
||||
current_tool_calls: dict[int, dict[str, Any]] = {}
|
||||
|
||||
async with self.async_client.messages.stream(**stream_params) as stream:
|
||||
async for event in stream:
|
||||
if hasattr(event, "delta") and hasattr(event.delta, "text"):
|
||||
@@ -952,6 +1005,55 @@ class AnthropicCompletion(BaseLLM):
|
||||
from_agent=from_agent,
|
||||
)
|
||||
|
||||
if event.type == "content_block_start":
|
||||
block = event.content_block
|
||||
if block.type == "tool_use":
|
||||
block_index = event.index
|
||||
current_tool_calls[block_index] = {
|
||||
"id": block.id,
|
||||
"name": block.name,
|
||||
"arguments": "",
|
||||
"index": block_index,
|
||||
}
|
||||
self._emit_stream_chunk_event(
|
||||
chunk="",
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
tool_call={
|
||||
"id": block.id,
|
||||
"function": {
|
||||
"name": block.name,
|
||||
"arguments": "",
|
||||
},
|
||||
"type": "function",
|
||||
"index": block_index,
|
||||
},
|
||||
call_type=LLMCallType.TOOL_CALL,
|
||||
)
|
||||
elif event.type == "content_block_delta":
|
||||
if event.delta.type == "input_json_delta":
|
||||
block_index = event.index
|
||||
partial_json = event.delta.partial_json
|
||||
if block_index in current_tool_calls and partial_json:
|
||||
current_tool_calls[block_index]["arguments"] += partial_json
|
||||
self._emit_stream_chunk_event(
|
||||
chunk=partial_json,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
tool_call={
|
||||
"id": current_tool_calls[block_index]["id"],
|
||||
"function": {
|
||||
"name": current_tool_calls[block_index]["name"],
|
||||
"arguments": current_tool_calls[block_index][
|
||||
"arguments"
|
||||
],
|
||||
},
|
||||
"type": "function",
|
||||
"index": block_index,
|
||||
},
|
||||
call_type=LLMCallType.TOOL_CALL,
|
||||
)
|
||||
|
||||
final_message: Message = await stream.get_final_message()
|
||||
|
||||
usage = self._extract_anthropic_token_usage(final_message)
|
||||
|
||||
@@ -674,7 +674,7 @@ class AzureCompletion(BaseLLM):
|
||||
self,
|
||||
update: StreamingChatCompletionsUpdate,
|
||||
full_response: str,
|
||||
tool_calls: dict[str, dict[str, str]],
|
||||
tool_calls: dict[int, dict[str, Any]],
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
) -> str:
|
||||
@@ -702,25 +702,45 @@ class AzureCompletion(BaseLLM):
|
||||
)
|
||||
|
||||
if choice.delta and choice.delta.tool_calls:
|
||||
for tool_call in choice.delta.tool_calls:
|
||||
call_id = tool_call.id or "default"
|
||||
if call_id not in tool_calls:
|
||||
tool_calls[call_id] = {
|
||||
for idx, tool_call in enumerate(choice.delta.tool_calls):
|
||||
if idx not in tool_calls:
|
||||
tool_calls[idx] = {
|
||||
"id": tool_call.id,
|
||||
"name": "",
|
||||
"arguments": "",
|
||||
}
|
||||
elif tool_call.id and not tool_calls[idx]["id"]:
|
||||
tool_calls[idx]["id"] = tool_call.id
|
||||
|
||||
if tool_call.function and tool_call.function.name:
|
||||
tool_calls[call_id]["name"] = tool_call.function.name
|
||||
tool_calls[idx]["name"] = tool_call.function.name
|
||||
if tool_call.function and tool_call.function.arguments:
|
||||
tool_calls[call_id]["arguments"] += tool_call.function.arguments
|
||||
tool_calls[idx]["arguments"] += tool_call.function.arguments
|
||||
|
||||
self._emit_stream_chunk_event(
|
||||
chunk=tool_call.function.arguments
|
||||
if tool_call.function and tool_call.function.arguments
|
||||
else "",
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
tool_call={
|
||||
"id": tool_calls[idx]["id"],
|
||||
"function": {
|
||||
"name": tool_calls[idx]["name"],
|
||||
"arguments": tool_calls[idx]["arguments"],
|
||||
},
|
||||
"type": "function",
|
||||
"index": idx,
|
||||
},
|
||||
call_type=LLMCallType.TOOL_CALL,
|
||||
)
|
||||
|
||||
return full_response
|
||||
|
||||
def _finalize_streaming_response(
|
||||
self,
|
||||
full_response: str,
|
||||
tool_calls: dict[str, dict[str, str]],
|
||||
tool_calls: dict[int, dict[str, Any]],
|
||||
usage_data: dict[str, int],
|
||||
params: AzureCompletionParams,
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
@@ -804,7 +824,7 @@ class AzureCompletion(BaseLLM):
|
||||
) -> str | Any:
|
||||
"""Handle streaming chat completion."""
|
||||
full_response = ""
|
||||
tool_calls: dict[str, dict[str, Any]] = {}
|
||||
tool_calls: dict[int, dict[str, Any]] = {}
|
||||
|
||||
usage_data = {"total_tokens": 0}
|
||||
for update in self.client.complete(**params): # type: ignore[arg-type]
|
||||
@@ -870,7 +890,7 @@ class AzureCompletion(BaseLLM):
|
||||
) -> str | Any:
|
||||
"""Handle streaming chat completion asynchronously."""
|
||||
full_response = ""
|
||||
tool_calls: dict[str, dict[str, Any]] = {}
|
||||
tool_calls: dict[int, dict[str, Any]] = {}
|
||||
|
||||
usage_data = {"total_tokens": 0}
|
||||
|
||||
|
||||
@@ -315,9 +315,7 @@ class BedrockCompletion(BaseLLM):
|
||||
messages
|
||||
)
|
||||
|
||||
if not self._invoke_before_llm_call_hooks(
|
||||
cast(list[LLMMessage], formatted_messages), from_agent
|
||||
):
|
||||
if not self._invoke_before_llm_call_hooks(formatted_messages, from_agent):
|
||||
raise ValueError("LLM call blocked by before_llm_call hook")
|
||||
|
||||
# Prepare request body
|
||||
@@ -361,7 +359,7 @@ class BedrockCompletion(BaseLLM):
|
||||
|
||||
if self.stream:
|
||||
return self._handle_streaming_converse(
|
||||
cast(list[LLMMessage], formatted_messages),
|
||||
formatted_messages,
|
||||
body,
|
||||
available_functions,
|
||||
from_task,
|
||||
@@ -369,7 +367,7 @@ class BedrockCompletion(BaseLLM):
|
||||
)
|
||||
|
||||
return self._handle_converse(
|
||||
cast(list[LLMMessage], formatted_messages),
|
||||
formatted_messages,
|
||||
body,
|
||||
available_functions,
|
||||
from_task,
|
||||
@@ -433,7 +431,7 @@ class BedrockCompletion(BaseLLM):
|
||||
)
|
||||
|
||||
formatted_messages, system_message = self._format_messages_for_converse(
|
||||
messages # type: ignore[arg-type]
|
||||
messages
|
||||
)
|
||||
|
||||
body: BedrockConverseRequestBody = {
|
||||
@@ -687,8 +685,10 @@ class BedrockCompletion(BaseLLM):
|
||||
) -> str:
|
||||
"""Handle streaming converse API call with comprehensive event handling."""
|
||||
full_response = ""
|
||||
current_tool_use = None
|
||||
tool_use_id = None
|
||||
current_tool_use: dict[str, Any] | None = None
|
||||
tool_use_id: str | None = None
|
||||
tool_use_index = 0
|
||||
accumulated_tool_input = ""
|
||||
|
||||
try:
|
||||
response = self.client.converse_stream(
|
||||
@@ -709,9 +709,30 @@ class BedrockCompletion(BaseLLM):
|
||||
|
||||
elif "contentBlockStart" in event:
|
||||
start = event["contentBlockStart"].get("start", {})
|
||||
content_block_index = event["contentBlockStart"].get(
|
||||
"contentBlockIndex", 0
|
||||
)
|
||||
if "toolUse" in start:
|
||||
current_tool_use = start["toolUse"]
|
||||
tool_use_block = start["toolUse"]
|
||||
current_tool_use = cast(dict[str, Any], tool_use_block)
|
||||
tool_use_id = current_tool_use.get("toolUseId")
|
||||
tool_use_index = content_block_index
|
||||
accumulated_tool_input = ""
|
||||
self._emit_stream_chunk_event(
|
||||
chunk="",
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
tool_call={
|
||||
"id": tool_use_id or "",
|
||||
"function": {
|
||||
"name": current_tool_use.get("name", ""),
|
||||
"arguments": "",
|
||||
},
|
||||
"type": "function",
|
||||
"index": tool_use_index,
|
||||
},
|
||||
call_type=LLMCallType.TOOL_CALL,
|
||||
)
|
||||
logging.debug(
|
||||
f"Tool use started in stream: {json.dumps(current_tool_use)} (ID: {tool_use_id})"
|
||||
)
|
||||
@@ -730,7 +751,23 @@ class BedrockCompletion(BaseLLM):
|
||||
elif "toolUse" in delta and current_tool_use:
|
||||
tool_input = delta["toolUse"].get("input", "")
|
||||
if tool_input:
|
||||
accumulated_tool_input += tool_input
|
||||
logging.debug(f"Tool input delta: {tool_input}")
|
||||
self._emit_stream_chunk_event(
|
||||
chunk=tool_input,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
tool_call={
|
||||
"id": tool_use_id or "",
|
||||
"function": {
|
||||
"name": current_tool_use.get("name", ""),
|
||||
"arguments": accumulated_tool_input,
|
||||
},
|
||||
"type": "function",
|
||||
"index": tool_use_index,
|
||||
},
|
||||
call_type=LLMCallType.TOOL_CALL,
|
||||
)
|
||||
elif "contentBlockStop" in event:
|
||||
logging.debug("Content block stopped in stream")
|
||||
if current_tool_use and available_functions:
|
||||
@@ -848,7 +885,7 @@ class BedrockCompletion(BaseLLM):
|
||||
|
||||
async def _ahandle_converse(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
messages: list[LLMMessage],
|
||||
body: BedrockConverseRequestBody,
|
||||
available_functions: Mapping[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
@@ -1013,7 +1050,7 @@ class BedrockCompletion(BaseLLM):
|
||||
|
||||
async def _ahandle_streaming_converse(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
messages: list[LLMMessage],
|
||||
body: BedrockConverseRequestBody,
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
from_task: Any | None = None,
|
||||
@@ -1021,8 +1058,10 @@ class BedrockCompletion(BaseLLM):
|
||||
) -> str:
|
||||
"""Handle async streaming converse API call."""
|
||||
full_response = ""
|
||||
current_tool_use = None
|
||||
tool_use_id = None
|
||||
current_tool_use: dict[str, Any] | None = None
|
||||
tool_use_id: str | None = None
|
||||
tool_use_index = 0
|
||||
accumulated_tool_input = ""
|
||||
|
||||
try:
|
||||
async_client = await self._ensure_async_client()
|
||||
@@ -1044,9 +1083,30 @@ class BedrockCompletion(BaseLLM):
|
||||
|
||||
elif "contentBlockStart" in event:
|
||||
start = event["contentBlockStart"].get("start", {})
|
||||
content_block_index = event["contentBlockStart"].get(
|
||||
"contentBlockIndex", 0
|
||||
)
|
||||
if "toolUse" in start:
|
||||
current_tool_use = start["toolUse"]
|
||||
tool_use_block = start["toolUse"]
|
||||
current_tool_use = cast(dict[str, Any], tool_use_block)
|
||||
tool_use_id = current_tool_use.get("toolUseId")
|
||||
tool_use_index = content_block_index
|
||||
accumulated_tool_input = ""
|
||||
self._emit_stream_chunk_event(
|
||||
chunk="",
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
tool_call={
|
||||
"id": tool_use_id or "",
|
||||
"function": {
|
||||
"name": current_tool_use.get("name", ""),
|
||||
"arguments": "",
|
||||
},
|
||||
"type": "function",
|
||||
"index": tool_use_index,
|
||||
},
|
||||
call_type=LLMCallType.TOOL_CALL,
|
||||
)
|
||||
logging.debug(
|
||||
f"Tool use started in stream: {current_tool_use.get('name')} (ID: {tool_use_id})"
|
||||
)
|
||||
@@ -1065,7 +1125,23 @@ class BedrockCompletion(BaseLLM):
|
||||
elif "toolUse" in delta and current_tool_use:
|
||||
tool_input = delta["toolUse"].get("input", "")
|
||||
if tool_input:
|
||||
accumulated_tool_input += tool_input
|
||||
logging.debug(f"Tool input delta: {tool_input}")
|
||||
self._emit_stream_chunk_event(
|
||||
chunk=tool_input,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
tool_call={
|
||||
"id": tool_use_id or "",
|
||||
"function": {
|
||||
"name": current_tool_use.get("name", ""),
|
||||
"arguments": accumulated_tool_input,
|
||||
},
|
||||
"type": "function",
|
||||
"index": tool_use_index,
|
||||
},
|
||||
call_type=LLMCallType.TOOL_CALL,
|
||||
)
|
||||
|
||||
elif "contentBlockStop" in event:
|
||||
logging.debug("Content block stopped in stream")
|
||||
@@ -1174,7 +1250,7 @@ class BedrockCompletion(BaseLLM):
|
||||
|
||||
def _format_messages_for_converse(
|
||||
self, messages: str | list[LLMMessage]
|
||||
) -> tuple[list[dict[str, Any]], str | None]:
|
||||
) -> tuple[list[LLMMessage], str | None]:
|
||||
"""Format messages for Converse API following AWS documentation.
|
||||
|
||||
Note: Returns dict[str, Any] instead of LLMMessage because Bedrock uses
|
||||
@@ -1184,7 +1260,7 @@ class BedrockCompletion(BaseLLM):
|
||||
# Use base class formatting first
|
||||
formatted_messages = self._format_messages(messages)
|
||||
|
||||
converse_messages: list[dict[str, Any]] = []
|
||||
converse_messages: list[LLMMessage] = []
|
||||
system_message: str | None = None
|
||||
|
||||
for message in formatted_messages:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@@ -24,7 +25,7 @@ try:
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
from google.genai.errors import APIError
|
||||
from google.genai.types import GenerateContentResponse, Schema
|
||||
from google.genai.types import GenerateContentResponse
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
'Google Gen AI native provider not available, to install: uv add "crewai[google-genai]"'
|
||||
@@ -434,12 +435,9 @@ class GeminiCompletion(BaseLLM):
|
||||
function_declaration = types.FunctionDeclaration(
|
||||
name=name,
|
||||
description=description,
|
||||
parameters=parameters if parameters else None,
|
||||
)
|
||||
|
||||
# Add parameters if present - ensure parameters is a dict
|
||||
if parameters and isinstance(parameters, Schema):
|
||||
function_declaration.parameters = parameters
|
||||
|
||||
gemini_tool = types.Tool(function_declarations=[function_declaration])
|
||||
gemini_tools.append(gemini_tool)
|
||||
|
||||
@@ -609,7 +607,7 @@ class GeminiCompletion(BaseLLM):
|
||||
candidate = response.candidates[0]
|
||||
if candidate.content and candidate.content.parts:
|
||||
for part in candidate.content.parts:
|
||||
if hasattr(part, "function_call") and part.function_call:
|
||||
if part.function_call:
|
||||
function_name = part.function_call.name
|
||||
if function_name is None:
|
||||
continue
|
||||
@@ -645,17 +643,17 @@ class GeminiCompletion(BaseLLM):
|
||||
self,
|
||||
chunk: GenerateContentResponse,
|
||||
full_response: str,
|
||||
function_calls: dict[str, dict[str, Any]],
|
||||
function_calls: dict[int, dict[str, Any]],
|
||||
usage_data: dict[str, int],
|
||||
from_task: Any | None = None,
|
||||
from_agent: Any | None = None,
|
||||
) -> tuple[str, dict[str, dict[str, Any]], dict[str, int]]:
|
||||
) -> tuple[str, dict[int, dict[str, Any]], dict[str, int]]:
|
||||
"""Process a single streaming chunk.
|
||||
|
||||
Args:
|
||||
chunk: The streaming chunk response
|
||||
full_response: Accumulated response text
|
||||
function_calls: Accumulated function calls
|
||||
function_calls: Accumulated function calls keyed by sequential index
|
||||
usage_data: Accumulated usage data
|
||||
from_task: Task that initiated the call
|
||||
from_agent: Agent that initiated the call
|
||||
@@ -678,22 +676,44 @@ class GeminiCompletion(BaseLLM):
|
||||
candidate = chunk.candidates[0]
|
||||
if candidate.content and candidate.content.parts:
|
||||
for part in candidate.content.parts:
|
||||
if hasattr(part, "function_call") and part.function_call:
|
||||
call_id = part.function_call.name or "default"
|
||||
if call_id not in function_calls:
|
||||
function_calls[call_id] = {
|
||||
"name": part.function_call.name,
|
||||
"args": dict(part.function_call.args)
|
||||
if part.function_call.args
|
||||
else {},
|
||||
}
|
||||
if part.function_call:
|
||||
call_index = len(function_calls)
|
||||
call_id = f"call_{call_index}"
|
||||
args_dict = (
|
||||
dict(part.function_call.args)
|
||||
if part.function_call.args
|
||||
else {}
|
||||
)
|
||||
args_json = json.dumps(args_dict)
|
||||
|
||||
function_calls[call_index] = {
|
||||
"id": call_id,
|
||||
"name": part.function_call.name,
|
||||
"args": args_dict,
|
||||
}
|
||||
|
||||
self._emit_stream_chunk_event(
|
||||
chunk=args_json,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
tool_call={
|
||||
"id": call_id,
|
||||
"function": {
|
||||
"name": part.function_call.name or "",
|
||||
"arguments": args_json,
|
||||
},
|
||||
"type": "function",
|
||||
"index": call_index,
|
||||
},
|
||||
call_type=LLMCallType.TOOL_CALL,
|
||||
)
|
||||
|
||||
return full_response, function_calls, usage_data
|
||||
|
||||
def _finalize_streaming_response(
|
||||
self,
|
||||
full_response: str,
|
||||
function_calls: dict[str, dict[str, Any]],
|
||||
function_calls: dict[int, dict[str, Any]],
|
||||
usage_data: dict[str, int],
|
||||
contents: list[types.Content],
|
||||
available_functions: dict[str, Any] | None = None,
|
||||
@@ -800,7 +820,7 @@ class GeminiCompletion(BaseLLM):
|
||||
) -> str:
|
||||
"""Handle streaming content generation."""
|
||||
full_response = ""
|
||||
function_calls: dict[str, dict[str, Any]] = {}
|
||||
function_calls: dict[int, dict[str, Any]] = {}
|
||||
usage_data = {"total_tokens": 0}
|
||||
|
||||
# The API accepts list[Content] but mypy is overly strict about variance
|
||||
@@ -878,7 +898,7 @@ class GeminiCompletion(BaseLLM):
|
||||
) -> str:
|
||||
"""Handle async streaming content generation."""
|
||||
full_response = ""
|
||||
function_calls: dict[str, dict[str, Any]] = {}
|
||||
function_calls: dict[int, dict[str, Any]] = {}
|
||||
usage_data = {"total_tokens": 0}
|
||||
|
||||
# The API accepts list[Content] but mypy is overly strict about variance
|
||||
|
||||
@@ -521,7 +521,7 @@ class OpenAICompletion(BaseLLM):
|
||||
) -> str:
|
||||
"""Handle streaming chat completion."""
|
||||
full_response = ""
|
||||
tool_calls = {}
|
||||
tool_calls: dict[int, dict[str, Any]] = {}
|
||||
|
||||
if response_model:
|
||||
parse_params = {
|
||||
@@ -591,17 +591,41 @@ class OpenAICompletion(BaseLLM):
|
||||
|
||||
if chunk_delta.tool_calls:
|
||||
for tool_call in chunk_delta.tool_calls:
|
||||
call_id = tool_call.id or "default"
|
||||
if call_id not in tool_calls:
|
||||
tool_calls[call_id] = {
|
||||
tool_index = tool_call.index if tool_call.index is not None else 0
|
||||
if tool_index not in tool_calls:
|
||||
tool_calls[tool_index] = {
|
||||
"id": tool_call.id,
|
||||
"name": "",
|
||||
"arguments": "",
|
||||
"index": tool_index,
|
||||
}
|
||||
elif tool_call.id and not tool_calls[tool_index]["id"]:
|
||||
tool_calls[tool_index]["id"] = tool_call.id
|
||||
|
||||
if tool_call.function and tool_call.function.name:
|
||||
tool_calls[call_id]["name"] = tool_call.function.name
|
||||
tool_calls[tool_index]["name"] = tool_call.function.name
|
||||
if tool_call.function and tool_call.function.arguments:
|
||||
tool_calls[call_id]["arguments"] += tool_call.function.arguments
|
||||
tool_calls[tool_index]["arguments"] += (
|
||||
tool_call.function.arguments
|
||||
)
|
||||
|
||||
self._emit_stream_chunk_event(
|
||||
chunk=tool_call.function.arguments
|
||||
if tool_call.function and tool_call.function.arguments
|
||||
else "",
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
tool_call={
|
||||
"id": tool_calls[tool_index]["id"],
|
||||
"function": {
|
||||
"name": tool_calls[tool_index]["name"],
|
||||
"arguments": tool_calls[tool_index]["arguments"],
|
||||
},
|
||||
"type": "function",
|
||||
"index": tool_calls[tool_index]["index"],
|
||||
},
|
||||
call_type=LLMCallType.TOOL_CALL,
|
||||
)
|
||||
|
||||
self._track_token_usage_internal(usage_data)
|
||||
|
||||
@@ -789,7 +813,7 @@ class OpenAICompletion(BaseLLM):
|
||||
) -> str:
|
||||
"""Handle async streaming chat completion."""
|
||||
full_response = ""
|
||||
tool_calls = {}
|
||||
tool_calls: dict[int, dict[str, Any]] = {}
|
||||
|
||||
if response_model:
|
||||
completion_stream: AsyncIterator[
|
||||
@@ -870,17 +894,41 @@ class OpenAICompletion(BaseLLM):
|
||||
|
||||
if chunk_delta.tool_calls:
|
||||
for tool_call in chunk_delta.tool_calls:
|
||||
call_id = tool_call.id or "default"
|
||||
if call_id not in tool_calls:
|
||||
tool_calls[call_id] = {
|
||||
tool_index = tool_call.index if tool_call.index is not None else 0
|
||||
if tool_index not in tool_calls:
|
||||
tool_calls[tool_index] = {
|
||||
"id": tool_call.id,
|
||||
"name": "",
|
||||
"arguments": "",
|
||||
"index": tool_index,
|
||||
}
|
||||
elif tool_call.id and not tool_calls[tool_index]["id"]:
|
||||
tool_calls[tool_index]["id"] = tool_call.id
|
||||
|
||||
if tool_call.function and tool_call.function.name:
|
||||
tool_calls[call_id]["name"] = tool_call.function.name
|
||||
tool_calls[tool_index]["name"] = tool_call.function.name
|
||||
if tool_call.function and tool_call.function.arguments:
|
||||
tool_calls[call_id]["arguments"] += tool_call.function.arguments
|
||||
tool_calls[tool_index]["arguments"] += (
|
||||
tool_call.function.arguments
|
||||
)
|
||||
|
||||
self._emit_stream_chunk_event(
|
||||
chunk=tool_call.function.arguments
|
||||
if tool_call.function and tool_call.function.arguments
|
||||
else "",
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
tool_call={
|
||||
"id": tool_calls[tool_index]["id"],
|
||||
"function": {
|
||||
"name": tool_calls[tool_index]["name"],
|
||||
"arguments": tool_calls[tool_index]["arguments"],
|
||||
},
|
||||
"type": "function",
|
||||
"index": tool_calls[tool_index]["index"],
|
||||
},
|
||||
call_type=LLMCallType.TOOL_CALL,
|
||||
)
|
||||
|
||||
self._track_token_usage_internal(usage_data)
|
||||
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"max_tokens":4096,"messages":[{"role":"user","content":"What is the temperature
|
||||
in San Francisco?"}],"model":"claude-3-5-haiku-latest","tools":[{"name":"get_current_temperature","description":"Get
|
||||
the current temperature in a city.","input_schema":{"type":"object","properties":{"city":{"type":"string","description":"The
|
||||
name of the city to get the temperature for."}},"required":["city"]}}],"stream":true}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
anthropic-version:
|
||||
- '2023-06-01'
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '408'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.anthropic.com
|
||||
x-api-key:
|
||||
- X-API-KEY-XXX
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 0.71.1
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.12.10
|
||||
x-stainless-stream-helper:
|
||||
- messages
|
||||
x-stainless-timeout:
|
||||
- NOT_GIVEN
|
||||
method: POST
|
||||
uri: https://api.anthropic.com/v1/messages
|
||||
response:
|
||||
body:
|
||||
string: 'event: message_start
|
||||
|
||||
data: {"type":"message_start","message":{"model":"claude-3-5-haiku-20241022","id":"msg_01JCJXSfyzkcecJUydp157cS","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":351,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}} }
|
||||
|
||||
|
||||
event: content_block_start
|
||||
|
||||
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"I"} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"''ll"} }
|
||||
|
||||
|
||||
event: ping
|
||||
|
||||
data: {"type": "ping"}
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"
|
||||
help"} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"
|
||||
you find out"} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"
|
||||
the current temperature in San"} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"
|
||||
Francisco. I"} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"''ll"} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"
|
||||
use the get"} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"_current_temperature
|
||||
function"} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"
|
||||
to"} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"
|
||||
retrieve"} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"
|
||||
this"} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"
|
||||
information."} }
|
||||
|
||||
|
||||
event: content_block_stop
|
||||
|
||||
data: {"type":"content_block_stop","index":0}
|
||||
|
||||
|
||||
event: content_block_start
|
||||
|
||||
data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01Lfr3kUnHMZApePPRWMv1uS","name":"get_current_temperature","input":{}} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"c"} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"ity\":"} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"
|
||||
\"San Franci"} }
|
||||
|
||||
|
||||
event: content_block_delta
|
||||
|
||||
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"sco\"}"} }
|
||||
|
||||
|
||||
event: content_block_stop
|
||||
|
||||
data: {"type":"content_block_stop","index":1 }
|
||||
|
||||
|
||||
event: message_delta
|
||||
|
||||
data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":351,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":85}}
|
||||
|
||||
|
||||
event: message_stop
|
||||
|
||||
data: {"type":"message_stop" }
|
||||
|
||||
|
||||
'
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Cache-Control:
|
||||
- no-cache
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- text/event-stream; charset=utf-8
|
||||
Date:
|
||||
- Mon, 05 Jan 2026 16:04:31 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Robots-Tag:
|
||||
- none
|
||||
anthropic-organization-id:
|
||||
- ANTHROPIC-ORGANIZATION-ID-XXX
|
||||
anthropic-ratelimit-input-tokens-limit:
|
||||
- ANTHROPIC-RATELIMIT-INPUT-TOKENS-LIMIT-XXX
|
||||
anthropic-ratelimit-input-tokens-remaining:
|
||||
- ANTHROPIC-RATELIMIT-INPUT-TOKENS-REMAINING-XXX
|
||||
anthropic-ratelimit-input-tokens-reset:
|
||||
- ANTHROPIC-RATELIMIT-INPUT-TOKENS-RESET-XXX
|
||||
anthropic-ratelimit-output-tokens-limit:
|
||||
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-LIMIT-XXX
|
||||
anthropic-ratelimit-output-tokens-remaining:
|
||||
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-REMAINING-XXX
|
||||
anthropic-ratelimit-output-tokens-reset:
|
||||
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-RESET-XXX
|
||||
anthropic-ratelimit-requests-limit:
|
||||
- '4000'
|
||||
anthropic-ratelimit-requests-remaining:
|
||||
- '3999'
|
||||
anthropic-ratelimit-requests-reset:
|
||||
- '2026-01-05T16:04:30Z'
|
||||
anthropic-ratelimit-tokens-limit:
|
||||
- ANTHROPIC-RATELIMIT-TOKENS-LIMIT-XXX
|
||||
anthropic-ratelimit-tokens-remaining:
|
||||
- ANTHROPIC-RATELIMIT-TOKENS-REMAINING-XXX
|
||||
anthropic-ratelimit-tokens-reset:
|
||||
- ANTHROPIC-RATELIMIT-TOKENS-RESET-XXX
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
request-id:
|
||||
- REQUEST-ID-XXX
|
||||
strict-transport-security:
|
||||
- STS-XXX
|
||||
x-envoy-upstream-service-time:
|
||||
- '690'
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: "{\"max_tokens\":4096,\"messages\":[{\"role\":\"user\",\"content\":\"What
|
||||
is the temperature in San Francisco?\"},{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"I'll
|
||||
help you find out the current temperature in San Francisco. I'll use the get_current_temperature
|
||||
function to retrieve this information.\"},{\"type\":\"tool_use\",\"id\":\"toolu_01Lfr3kUnHMZApePPRWMv1uS\",\"name\":\"get_current_temperature\",\"input\":{\"city\":\"San
|
||||
Francisco\"}}]},{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"toolu_01Lfr3kUnHMZApePPRWMv1uS\",\"content\":\"The
|
||||
temperature in San Francisco is 72\xB0F\"}]}],\"model\":\"claude-3-5-haiku-latest\",\"stream\":true,\"tools\":[{\"name\":\"get_current_temperature\",\"description\":\"Get
|
||||
the current temperature in a city.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\",\"description\":\"The
|
||||
name of the city to get the temperature for.\"}},\"required\":[\"city\"]}}]}"
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
anthropic-version:
|
||||
- '2023-06-01'
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '883'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.anthropic.com
|
||||
x-api-key:
|
||||
- X-API-KEY-XXX
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 0.71.1
|
||||
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: "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-3-5-haiku-20241022\",\"id\":\"msg_01XbRN6xwSPSLv6pWtB15EZs\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":457,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":2,\"service_tier\":\"standard\"}}
|
||||
\ }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}
|
||||
}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"The\"}
|
||||
\ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"
|
||||
current\"} }\n\nevent: ping\ndata: {\"type\": \"ping\"}\n\nevent:
|
||||
content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"
|
||||
temperature in San Francisco is\"} }\n\nevent: content_block_delta\ndata:
|
||||
{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"
|
||||
72\xB0F.\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"
|
||||
It\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"
|
||||
sounds\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"
|
||||
like a\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"
|
||||
pleasant\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"
|
||||
day!\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"
|
||||
Is\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"
|
||||
there anything else I can\"} }\n\nevent: content_block_delta\ndata:
|
||||
{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"
|
||||
help\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"
|
||||
you with?\"} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0
|
||||
\ }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"usage\":{\"input_tokens\":457,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":33}
|
||||
\ }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n"
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Cache-Control:
|
||||
- no-cache
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- text/event-stream; charset=utf-8
|
||||
Date:
|
||||
- Mon, 05 Jan 2026 16:04:33 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Robots-Tag:
|
||||
- none
|
||||
anthropic-organization-id:
|
||||
- ANTHROPIC-ORGANIZATION-ID-XXX
|
||||
anthropic-ratelimit-input-tokens-limit:
|
||||
- ANTHROPIC-RATELIMIT-INPUT-TOKENS-LIMIT-XXX
|
||||
anthropic-ratelimit-input-tokens-remaining:
|
||||
- ANTHROPIC-RATELIMIT-INPUT-TOKENS-REMAINING-XXX
|
||||
anthropic-ratelimit-input-tokens-reset:
|
||||
- ANTHROPIC-RATELIMIT-INPUT-TOKENS-RESET-XXX
|
||||
anthropic-ratelimit-output-tokens-limit:
|
||||
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-LIMIT-XXX
|
||||
anthropic-ratelimit-output-tokens-remaining:
|
||||
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-REMAINING-XXX
|
||||
anthropic-ratelimit-output-tokens-reset:
|
||||
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-RESET-XXX
|
||||
anthropic-ratelimit-requests-limit:
|
||||
- '4000'
|
||||
anthropic-ratelimit-requests-remaining:
|
||||
- '3999'
|
||||
anthropic-ratelimit-requests-reset:
|
||||
- '2026-01-05T16:04:32Z'
|
||||
anthropic-ratelimit-tokens-limit:
|
||||
- ANTHROPIC-RATELIMIT-TOKENS-LIMIT-XXX
|
||||
anthropic-ratelimit-tokens-remaining:
|
||||
- ANTHROPIC-RATELIMIT-TOKENS-REMAINING-XXX
|
||||
anthropic-ratelimit-tokens-reset:
|
||||
- ANTHROPIC-RATELIMIT-TOKENS-RESET-XXX
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
request-id:
|
||||
- REQUEST-ID-XXX
|
||||
strict-transport-security:
|
||||
- STS-XXX
|
||||
x-envoy-upstream-service-time:
|
||||
- '532'
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -0,0 +1,108 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages": [{"role": "user", "content": "What is the temperature in San
|
||||
Francisco?"}], "stream": true, "tool_choice": "auto", "tools": [{"function":
|
||||
{"name": "get_current_temperature", "description": "Get the current temperature
|
||||
in a city.", "parameters": {"type": "object", "properties": {"city": {"type":
|
||||
"string", "description": "The name of the city to get the temperature for."}},
|
||||
"required": ["city"]}}, "type": "function"}], "stream_options": {"include_usage":
|
||||
true}}'
|
||||
headers:
|
||||
Accept:
|
||||
- application/json
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '476'
|
||||
Content-Type:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
api-key:
|
||||
- X-API-KEY-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
extra-parameters:
|
||||
- pass-through
|
||||
x-ms-client-request-id:
|
||||
- X-MS-CLIENT-REQUEST-ID-XXX
|
||||
method: POST
|
||||
uri: https://fake-azure-endpoint.openai.azure.com/openai/deployments/gpt-4o-mini/chat/completions?api-version=2024-02-15-preview
|
||||
response:
|
||||
body:
|
||||
string: 'data: {"choices":[],"created":0,"id":"","model":"","object":"","prompt_filter_results":[{"prompt_index":0,"content_filter_results":{"hate":{"filtered":false,"severity":"safe"},"jailbreak":{"filtered":false,"detected":false},"self_harm":{"filtered":false,"severity":"safe"},"sexual":{"filtered":false,"severity":"safe"},"violence":{"filtered":false,"severity":"safe"}}}]}
|
||||
|
||||
|
||||
data: {"choices":[{"content_filter_results":{},"delta":{"content":null,"refusal":null,"role":"assistant","tool_calls":[{"function":{"arguments":"","name":"get_current_temperature"},"id":"call_e6RnREl4LBGp0PdkIf6bBioH","index":0,"type":"function"}]},"finish_reason":null,"index":0,"logprobs":null}],"created":1767630292,"id":"chatcmpl-Cuhfwc9oYO2rZ1Y2xInKelrARv7iC","model":"gpt-4o-mini-2024-07-18","obfuscation":"a","object":"chat.completion.chunk","system_fingerprint":"fp_f97eff32c5","usage":null}
|
||||
|
||||
|
||||
data: {"choices":[{"content_filter_results":{},"delta":{"tool_calls":[{"function":{"arguments":"{\""},"index":0}]},"finish_reason":null,"index":0,"logprobs":null}],"created":1767630292,"id":"chatcmpl-Cuhfwc9oYO2rZ1Y2xInKelrARv7iC","model":"gpt-4o-mini-2024-07-18","obfuscation":"scYzCqI","object":"chat.completion.chunk","system_fingerprint":"fp_f97eff32c5","usage":null}
|
||||
|
||||
|
||||
data: {"choices":[{"content_filter_results":{},"delta":{"tool_calls":[{"function":{"arguments":"city"},"index":0}]},"finish_reason":null,"index":0,"logprobs":null}],"created":1767630292,"id":"chatcmpl-Cuhfwc9oYO2rZ1Y2xInKelrARv7iC","model":"gpt-4o-mini-2024-07-18","obfuscation":"gtrknf","object":"chat.completion.chunk","system_fingerprint":"fp_f97eff32c5","usage":null}
|
||||
|
||||
|
||||
data: {"choices":[{"content_filter_results":{},"delta":{"tool_calls":[{"function":{"arguments":"\":\""},"index":0}]},"finish_reason":null,"index":0,"logprobs":null}],"created":1767630292,"id":"chatcmpl-Cuhfwc9oYO2rZ1Y2xInKelrARv7iC","model":"gpt-4o-mini-2024-07-18","obfuscation":"Fgf3u","object":"chat.completion.chunk","system_fingerprint":"fp_f97eff32c5","usage":null}
|
||||
|
||||
|
||||
data: {"choices":[{"content_filter_results":{},"delta":{"tool_calls":[{"function":{"arguments":"San"},"index":0}]},"finish_reason":null,"index":0,"logprobs":null}],"created":1767630292,"id":"chatcmpl-Cuhfwc9oYO2rZ1Y2xInKelrARv7iC","model":"gpt-4o-mini-2024-07-18","obfuscation":"Y11NWOp","object":"chat.completion.chunk","system_fingerprint":"fp_f97eff32c5","usage":null}
|
||||
|
||||
|
||||
data: {"choices":[{"content_filter_results":{},"delta":{"tool_calls":[{"function":{"arguments":"
|
||||
Francisco"},"index":0}]},"finish_reason":null,"index":0,"logprobs":null}],"created":1767630292,"id":"chatcmpl-Cuhfwc9oYO2rZ1Y2xInKelrARv7iC","model":"gpt-4o-mini-2024-07-18","obfuscation":"","object":"chat.completion.chunk","system_fingerprint":"fp_f97eff32c5","usage":null}
|
||||
|
||||
|
||||
data: {"choices":[{"content_filter_results":{},"delta":{"tool_calls":[{"function":{"arguments":"\"}"},"index":0}]},"finish_reason":null,"index":0,"logprobs":null}],"created":1767630292,"id":"chatcmpl-Cuhfwc9oYO2rZ1Y2xInKelrARv7iC","model":"gpt-4o-mini-2024-07-18","obfuscation":"21nwlWJ","object":"chat.completion.chunk","system_fingerprint":"fp_f97eff32c5","usage":null}
|
||||
|
||||
|
||||
data: {"choices":[{"content_filter_results":{},"delta":{},"finish_reason":"tool_calls","index":0,"logprobs":null}],"created":1767630292,"id":"chatcmpl-Cuhfwc9oYO2rZ1Y2xInKelrARv7iC","model":"gpt-4o-mini-2024-07-18","obfuscation":"lX7hrh76","object":"chat.completion.chunk","system_fingerprint":"fp_f97eff32c5","usage":null}
|
||||
|
||||
|
||||
data: {"choices":[],"created":1767630292,"id":"chatcmpl-Cuhfwc9oYO2rZ1Y2xInKelrARv7iC","model":"gpt-4o-mini-2024-07-18","obfuscation":"hA2","object":"chat.completion.chunk","system_fingerprint":"fp_f97eff32c5","usage":{"completion_tokens":17,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens":66,"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0},"total_tokens":83}}
|
||||
|
||||
|
||||
data: [DONE]
|
||||
|
||||
|
||||
'
|
||||
headers:
|
||||
Content-Type:
|
||||
- text/event-stream; charset=utf-8
|
||||
Date:
|
||||
- Mon, 05 Jan 2026 16:24:52 GMT
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
apim-request-id:
|
||||
- APIM-REQUEST-ID-XXX
|
||||
azureml-model-session:
|
||||
- AZUREML-MODEL-SESSION-XXX
|
||||
x-accel-buffering:
|
||||
- 'no'
|
||||
x-content-type-options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
x-ms-client-request-id:
|
||||
- X-MS-CLIENT-REQUEST-ID-XXX
|
||||
x-ms-deployment-name:
|
||||
- gpt-4o-mini
|
||||
x-ms-rai-invoked:
|
||||
- 'true'
|
||||
x-ms-region:
|
||||
- X-MS-REGION-XXX
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -0,0 +1,67 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"contents": [{"parts": [{"text": "What is the temperature in San Francisco?"}],
|
||||
"role": "user"}], "tools": [{"functionDeclarations": [{"description": "Get the
|
||||
current temperature in a city.", "name": "get_current_temperature", "parameters":
|
||||
{"properties": {"city": {"description": "The name of the city to get the temperature
|
||||
for.", "type": "STRING"}}, "required": ["city"], "type": "OBJECT"}}]}], "generationConfig":
|
||||
{}}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- '*/*'
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '422'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- generativelanguage.googleapis.com
|
||||
x-goog-api-client:
|
||||
- google-genai-sdk/1.49.0 gl-python/3.12.10
|
||||
x-goog-api-key:
|
||||
- X-GOOG-API-KEY-XXX
|
||||
method: POST
|
||||
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent?alt=sse
|
||||
response:
|
||||
body:
|
||||
string: "data: {\"candidates\": [{\"content\": {\"parts\": [{\"functionCall\":
|
||||
{\"name\": \"get_current_temperature\",\"args\": {\"city\": \"San Francisco\"}}}],\"role\":
|
||||
\"model\"},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\":
|
||||
36,\"candidatesTokenCount\": 8,\"totalTokenCount\": 44,\"promptTokensDetails\":
|
||||
[{\"modality\": \"TEXT\",\"tokenCount\": 36}],\"candidatesTokensDetails\":
|
||||
[{\"modality\": \"TEXT\",\"tokenCount\": 8}]},\"modelVersion\": \"gemini-2.0-flash\",\"responseId\":
|
||||
\"h99badGPDrP-x_APraXUmAM\"}\r\n\r\n"
|
||||
headers:
|
||||
Alt-Svc:
|
||||
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
|
||||
Content-Disposition:
|
||||
- attachment
|
||||
Content-Type:
|
||||
- text/event-stream
|
||||
Date:
|
||||
- Mon, 05 Jan 2026 15:57:59 GMT
|
||||
Server:
|
||||
- scaffolding on HTTPServer2
|
||||
Server-Timing:
|
||||
- gfet4t7; dur=583
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Vary:
|
||||
- Origin
|
||||
- X-Origin
|
||||
- Referer
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
X-Frame-Options:
|
||||
- X-FRAME-OPTIONS-XXX
|
||||
X-XSS-Protection:
|
||||
- '0'
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -0,0 +1,68 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"contents": [{"parts": [{"text": "What is the temperature in Paris and
|
||||
London?"}], "role": "user"}], "tools": [{"functionDeclarations": [{"description":
|
||||
"Get the current temperature in a city.", "name": "get_current_temperature",
|
||||
"parameters": {"properties": {"city": {"description": "The name of the city
|
||||
to get the temperature for.", "type": "STRING"}}, "required": ["city"], "type":
|
||||
"OBJECT"}}]}], "generationConfig": {}}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- '*/*'
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '425'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- generativelanguage.googleapis.com
|
||||
x-goog-api-client:
|
||||
- google-genai-sdk/1.49.0 gl-python/3.12.10
|
||||
x-goog-api-key:
|
||||
- X-GOOG-API-KEY-XXX
|
||||
method: POST
|
||||
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent?alt=sse
|
||||
response:
|
||||
body:
|
||||
string: "data: {\"candidates\": [{\"content\": {\"parts\": [{\"functionCall\":
|
||||
{\"name\": \"get_current_temperature\",\"args\": {\"city\": \"Paris\"}}},{\"functionCall\":
|
||||
{\"name\": \"get_current_temperature\",\"args\": {\"city\": \"London\"}}}],\"role\":
|
||||
\"model\"},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\":
|
||||
37,\"candidatesTokenCount\": 14,\"totalTokenCount\": 51,\"promptTokensDetails\":
|
||||
[{\"modality\": \"TEXT\",\"tokenCount\": 37}],\"candidatesTokensDetails\":
|
||||
[{\"modality\": \"TEXT\",\"tokenCount\": 14}]},\"modelVersion\": \"gemini-2.0-flash\",\"responseId\":
|
||||
\"h99baZTLOoSShMIPgYaAgQw\"}\r\n\r\n"
|
||||
headers:
|
||||
Alt-Svc:
|
||||
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
|
||||
Content-Disposition:
|
||||
- attachment
|
||||
Content-Type:
|
||||
- text/event-stream
|
||||
Date:
|
||||
- Mon, 05 Jan 2026 15:58:00 GMT
|
||||
Server:
|
||||
- scaffolding on HTTPServer2
|
||||
Server-Timing:
|
||||
- gfet4t7; dur=960
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
Vary:
|
||||
- Origin
|
||||
- X-Origin
|
||||
- Referer
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
X-Frame-Options:
|
||||
- X-FRAME-OPTIONS-XXX
|
||||
X-XSS-Protection:
|
||||
- '0'
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -0,0 +1,131 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages":[{"role":"user","content":"What is the temperature in San Francisco?"}],"model":"gpt-4o-mini","stream":true,"stream_options":{"include_usage":true},"tool_choice":"auto","tools":[{"type":"function","function":{"name":"get_current_temperature","description":"Get
|
||||
the current temperature in a city.","parameters":{"type":"object","properties":{"city":{"type":"string","description":"The
|
||||
name of the city to get the temperature for."}},"required":["city"]}}}]}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '468'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.openai.com
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 1.83.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
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: 'data: {"id":"chatcmpl-CugUbFnMkpXISLZPDmla5Pi4j8yng","object":"chat.completion.chunk","created":1767625745,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_3kB8meBh6OQYxf3Ch6K6aS7X","type":"function","function":{"name":"get_current_temperature","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"E1uB1Z7e"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUbFnMkpXISLZPDmla5Pi4j8yng","object":"chat.completion.chunk","created":1767625745,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"WfA8lJUdnDG3wX"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUbFnMkpXISLZPDmla5Pi4j8yng","object":"chat.completion.chunk","created":1767625745,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"city"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"q8i16eqGFPM92"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUbFnMkpXISLZPDmla5Pi4j8yng","object":"chat.completion.chunk","created":1767625745,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"BTem15zkzsoy"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUbFnMkpXISLZPDmla5Pi4j8yng","object":"chat.completion.chunk","created":1767625745,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"San"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"cTpMhY3sI6NiHw"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUbFnMkpXISLZPDmla5Pi4j8yng","object":"chat.completion.chunk","created":1767625745,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"
|
||||
Francisco"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"RHpsStT"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUbFnMkpXISLZPDmla5Pi4j8yng","object":"chat.completion.chunk","created":1767625745,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"SXQ7dOpJWPNo41"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUbFnMkpXISLZPDmla5Pi4j8yng","object":"chat.completion.chunk","created":1767625745,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null,"obfuscation":"r8asNT7VjB8B67A"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUbFnMkpXISLZPDmla5Pi4j8yng","object":"chat.completion.chunk","created":1767625745,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[],"usage":{"prompt_tokens":66,"completion_tokens":16,"total_tokens":82,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"D6wMV9IHqp"}
|
||||
|
||||
|
||||
data: [DONE]
|
||||
|
||||
|
||||
'
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- text/event-stream; charset=utf-8
|
||||
Date:
|
||||
- Mon, 05 Jan 2026 15:09:05 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- SET-COOKIE-XXX
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
access-control-expose-headers:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '474'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '488'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -0,0 +1,131 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages":[{"role":"user","content":"What is the temperature in San Francisco?"}],"model":"gpt-4o-mini","stream":true,"stream_options":{"include_usage":true},"tool_choice":"auto","tools":[{"type":"function","function":{"name":"get_current_temperature","description":"Get
|
||||
the current temperature in a city.","parameters":{"type":"object","properties":{"city":{"type":"string","description":"The
|
||||
name of the city to get the temperature for."}},"required":["city"]}}}]}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '468'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.openai.com
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 1.83.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
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: 'data: {"id":"chatcmpl-CugUcrVnIGFI01Ty76IqBP4iwcdk1","object":"chat.completion.chunk","created":1767625746,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_QXZLQbxriC1eBnOMXLPMopfe","type":"function","function":{"name":"get_current_temperature","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ncD7jNXK"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUcrVnIGFI01Ty76IqBP4iwcdk1","object":"chat.completion.chunk","created":1767625746,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"AmzMaEKhB232Mr"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUcrVnIGFI01Ty76IqBP4iwcdk1","object":"chat.completion.chunk","created":1767625746,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"city"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xfJ8TboQmMJCA"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUcrVnIGFI01Ty76IqBP4iwcdk1","object":"chat.completion.chunk","created":1767625746,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"iS6dOaTHSzht"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUcrVnIGFI01Ty76IqBP4iwcdk1","object":"chat.completion.chunk","created":1767625746,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"San"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"F7li6njWQE87IY"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUcrVnIGFI01Ty76IqBP4iwcdk1","object":"chat.completion.chunk","created":1767625746,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"
|
||||
Francisco"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"EaofAx0"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUcrVnIGFI01Ty76IqBP4iwcdk1","object":"chat.completion.chunk","created":1767625746,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"YNoAewLIjPGgbm"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUcrVnIGFI01Ty76IqBP4iwcdk1","object":"chat.completion.chunk","created":1767625746,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null,"obfuscation":"DyMKmTz1cyhwt3H"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUcrVnIGFI01Ty76IqBP4iwcdk1","object":"chat.completion.chunk","created":1767625746,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[],"usage":{"prompt_tokens":66,"completion_tokens":16,"total_tokens":82,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"svCxdxouSj"}
|
||||
|
||||
|
||||
data: [DONE]
|
||||
|
||||
|
||||
'
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- text/event-stream; charset=utf-8
|
||||
Date:
|
||||
- Mon, 05 Jan 2026 15:09:07 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- SET-COOKIE-XXX
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
access-control-expose-headers:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '650'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '692'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -0,0 +1,131 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages":[{"role":"user","content":"What is the temperature in San Francisco?"}],"model":"gpt-4o-mini","stream":true,"stream_options":{"include_usage":true},"tool_choice":"auto","tools":[{"type":"function","function":{"name":"get_current_temperature","description":"Get
|
||||
the current temperature in a city.","parameters":{"type":"object","properties":{"city":{"type":"string","description":"The
|
||||
name of the city to get the temperature for."}},"required":["city"]}}}]}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '468'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.openai.com
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 1.83.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
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: 'data: {"id":"chatcmpl-CugUfGwROKOfstuAzKnqcsX3yWA90","object":"chat.completion.chunk","created":1767625749,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_ZxE8mQ4FdO733hdMe8iW7mBH","type":"function","function":{"name":"get_current_temperature","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"2yD9IR8j"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUfGwROKOfstuAzKnqcsX3yWA90","object":"chat.completion.chunk","created":1767625749,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"HT2u4m0HdAcZFq"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUfGwROKOfstuAzKnqcsX3yWA90","object":"chat.completion.chunk","created":1767625749,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"city"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"O5f277ricHatr"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUfGwROKOfstuAzKnqcsX3yWA90","object":"chat.completion.chunk","created":1767625749,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"mLTrMr1JtCBJ"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUfGwROKOfstuAzKnqcsX3yWA90","object":"chat.completion.chunk","created":1767625749,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"San"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"siz0LLU1Gv7jC1"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUfGwROKOfstuAzKnqcsX3yWA90","object":"chat.completion.chunk","created":1767625749,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"
|
||||
Francisco"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"OGOJJYA"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUfGwROKOfstuAzKnqcsX3yWA90","object":"chat.completion.chunk","created":1767625749,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wZT1SejqluCrAY"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUfGwROKOfstuAzKnqcsX3yWA90","object":"chat.completion.chunk","created":1767625749,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null,"obfuscation":"YNlwGCa5JWewnZy"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUfGwROKOfstuAzKnqcsX3yWA90","object":"chat.completion.chunk","created":1767625749,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[],"usage":{"prompt_tokens":66,"completion_tokens":16,"total_tokens":82,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"4Fk4xNw3lV"}
|
||||
|
||||
|
||||
data: [DONE]
|
||||
|
||||
|
||||
'
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- text/event-stream; charset=utf-8
|
||||
Date:
|
||||
- Mon, 05 Jan 2026 15:09:10 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- SET-COOKIE-XXX
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
access-control-expose-headers:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '683'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '698'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -0,0 +1,131 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages":[{"role":"user","content":"What is the temperature in San Francisco?"}],"model":"gpt-4o-mini","stream":true,"stream_options":{"include_usage":true},"tool_choice":"auto","tools":[{"type":"function","function":{"name":"get_current_temperature","description":"Get
|
||||
the current temperature in a city.","parameters":{"type":"object","properties":{"city":{"type":"string","description":"The
|
||||
name of the city to get the temperature for."}},"required":["city"]}}}]}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '468'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.openai.com
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 1.83.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
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: 'data: {"id":"chatcmpl-CugUdqZiFd9Y6Kq1E9zniCoa0uwHM","object":"chat.completion.chunk","created":1767625747,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_ACVuyKtLn299YJUkoH9RWxks","type":"function","function":{"name":"get_current_temperature","arguments":""}}],"refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"p96OKjJc"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUdqZiFd9Y6Kq1E9zniCoa0uwHM","object":"chat.completion.chunk","created":1767625747,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"RoT4saRoTqVqK9"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUdqZiFd9Y6Kq1E9zniCoa0uwHM","object":"chat.completion.chunk","created":1767625747,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"city"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"UnjRIiaNmkXxG"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUdqZiFd9Y6Kq1E9zniCoa0uwHM","object":"chat.completion.chunk","created":1767625747,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\":\""}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"OUJpwmX8Y5xm"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUdqZiFd9Y6Kq1E9zniCoa0uwHM","object":"chat.completion.chunk","created":1767625747,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"San"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"DBXFz5gGQyitfE"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUdqZiFd9Y6Kq1E9zniCoa0uwHM","object":"chat.completion.chunk","created":1767625747,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"
|
||||
Francisco"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"LSJ3CF3"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUdqZiFd9Y6Kq1E9zniCoa0uwHM","object":"chat.completion.chunk","created":1767625747,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"}"}}]},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"KUrpUnjMA8Rwhi"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUdqZiFd9Y6Kq1E9zniCoa0uwHM","object":"chat.completion.chunk","created":1767625747,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"tool_calls"}],"usage":null,"obfuscation":"Kycqgm00aFnjf9a"}
|
||||
|
||||
|
||||
data: {"id":"chatcmpl-CugUdqZiFd9Y6Kq1E9zniCoa0uwHM","object":"chat.completion.chunk","created":1767625747,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_29330a9688","choices":[],"usage":{"prompt_tokens":66,"completion_tokens":16,"total_tokens":82,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"UoTa3DaYLG"}
|
||||
|
||||
|
||||
data: [DONE]
|
||||
|
||||
|
||||
'
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- text/event-stream; charset=utf-8
|
||||
Date:
|
||||
- Mon, 05 Jan 2026 15:09:08 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- SET-COOKIE-XXX
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
access-control-expose-headers:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '509'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
x-envoy-upstream-service-time:
|
||||
- '524'
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
324
lib/crewai/tests/llms/test_tool_call_streaming.py
Normal file
324
lib/crewai/tests/llms/test_tool_call_streaming.py
Normal file
@@ -0,0 +1,324 @@
|
||||
"""Tests for tool call streaming events across LLM providers.
|
||||
|
||||
These tests verify that when streaming is enabled and the LLM makes a tool call,
|
||||
the stream chunk events include proper tool call information with
|
||||
call_type=LLMCallType.TOOL_CALL.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.events.types.llm_events import LLMCallType, LLMStreamChunkEvent, ToolCall
|
||||
from crewai.llm import LLM
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def get_temperature_tool_schema() -> dict[str, Any]:
|
||||
"""Create a temperature tool schema for native function calling."""
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_current_temperature",
|
||||
"description": "Get the current temperature in a city.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {
|
||||
"type": "string",
|
||||
"description": "The name of the city to get the temperature for.",
|
||||
}
|
||||
},
|
||||
"required": ["city"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_emit() -> MagicMock:
|
||||
"""Mock the event bus emit function."""
|
||||
from crewai.events.event_bus import CrewAIEventsBus
|
||||
|
||||
with patch.object(CrewAIEventsBus, "emit") as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
def get_tool_call_events(mock_emit: MagicMock) -> list[LLMStreamChunkEvent]:
|
||||
"""Extract tool call streaming events from mock emit calls."""
|
||||
tool_call_events = []
|
||||
for call in mock_emit.call_args_list:
|
||||
event = call[1].get("event") if len(call) > 1 else None
|
||||
if isinstance(event, LLMStreamChunkEvent) and event.call_type == LLMCallType.TOOL_CALL:
|
||||
tool_call_events.append(event)
|
||||
return tool_call_events
|
||||
|
||||
|
||||
def get_all_stream_events(mock_emit: MagicMock) -> list[LLMStreamChunkEvent]:
|
||||
"""Extract all streaming events from mock emit calls."""
|
||||
stream_events = []
|
||||
for call in mock_emit.call_args_list:
|
||||
event = call[1].get("event") if len(call) > 1 else None
|
||||
if isinstance(event, LLMStreamChunkEvent):
|
||||
stream_events.append(event)
|
||||
return stream_events
|
||||
|
||||
|
||||
class TestOpenAIToolCallStreaming:
|
||||
"""Tests for OpenAI provider tool call streaming events."""
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_openai_streaming_emits_tool_call_events(
|
||||
self, get_temperature_tool_schema: dict[str, Any], mock_emit: MagicMock
|
||||
) -> None:
|
||||
"""Test that OpenAI streaming emits tool call events with correct call_type."""
|
||||
llm = LLM(model="openai/gpt-4o-mini", stream=True)
|
||||
|
||||
llm.call(
|
||||
messages=[
|
||||
{"role": "user", "content": "What is the temperature in San Francisco?"},
|
||||
],
|
||||
tools=[get_temperature_tool_schema],
|
||||
available_functions={
|
||||
"get_current_temperature": lambda city: f"The temperature in {city} is 72°F"
|
||||
},
|
||||
)
|
||||
|
||||
tool_call_events = get_tool_call_events(mock_emit)
|
||||
|
||||
assert len(tool_call_events) > 0, "Should receive tool call streaming events"
|
||||
|
||||
first_tool_call_event = tool_call_events[0]
|
||||
assert first_tool_call_event.call_type == LLMCallType.TOOL_CALL
|
||||
assert first_tool_call_event.tool_call is not None
|
||||
assert isinstance(first_tool_call_event.tool_call, ToolCall)
|
||||
assert first_tool_call_event.tool_call.function is not None
|
||||
assert first_tool_call_event.tool_call.function.name == "get_current_temperature"
|
||||
assert first_tool_call_event.tool_call.type == "function"
|
||||
assert first_tool_call_event.tool_call.index >= 0
|
||||
|
||||
|
||||
class TestToolCallStreamingEventStructure:
|
||||
"""Tests for the structure and content of tool call streaming events."""
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_tool_call_event_accumulates_arguments(
|
||||
self, get_temperature_tool_schema: dict[str, Any], mock_emit: MagicMock
|
||||
) -> None:
|
||||
"""Test that tool call events accumulate arguments progressively."""
|
||||
llm = LLM(model="openai/gpt-4o-mini", stream=True)
|
||||
|
||||
llm.call(
|
||||
messages=[
|
||||
{"role": "user", "content": "What is the temperature in San Francisco?"},
|
||||
],
|
||||
tools=[get_temperature_tool_schema],
|
||||
available_functions={
|
||||
"get_current_temperature": lambda city: f"The temperature in {city} is 72°F"
|
||||
},
|
||||
)
|
||||
|
||||
tool_call_events = get_tool_call_events(mock_emit)
|
||||
|
||||
assert len(tool_call_events) >= 2, "Should receive multiple tool call streaming events"
|
||||
|
||||
for evt in tool_call_events:
|
||||
assert evt.tool_call is not None
|
||||
assert evt.tool_call.function is not None
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_tool_call_events_have_consistent_tool_id(
|
||||
self, get_temperature_tool_schema: dict[str, Any], mock_emit: MagicMock
|
||||
) -> None:
|
||||
"""Test that all events for the same tool call have the same tool ID."""
|
||||
llm = LLM(model="openai/gpt-4o-mini", stream=True)
|
||||
|
||||
llm.call(
|
||||
messages=[
|
||||
{"role": "user", "content": "What is the temperature in San Francisco?"},
|
||||
],
|
||||
tools=[get_temperature_tool_schema],
|
||||
available_functions={
|
||||
"get_current_temperature": lambda city: f"The temperature in {city} is 72°F"
|
||||
},
|
||||
)
|
||||
|
||||
tool_call_events = get_tool_call_events(mock_emit)
|
||||
|
||||
assert len(tool_call_events) >= 1, "Should receive tool call streaming events"
|
||||
|
||||
if len(tool_call_events) > 1:
|
||||
events_by_index: dict[int, list[LLMStreamChunkEvent]] = {}
|
||||
for evt in tool_call_events:
|
||||
if evt.tool_call is not None:
|
||||
idx = evt.tool_call.index
|
||||
if idx not in events_by_index:
|
||||
events_by_index[idx] = []
|
||||
events_by_index[idx].append(evt)
|
||||
|
||||
for idx, evts in events_by_index.items():
|
||||
ids = [
|
||||
e.tool_call.id
|
||||
for e in evts
|
||||
if e.tool_call is not None and e.tool_call.id
|
||||
]
|
||||
if ids:
|
||||
assert len(set(ids)) == 1, f"Tool call ID should be consistent for index {idx}"
|
||||
|
||||
|
||||
class TestMixedStreamingEvents:
|
||||
"""Tests for scenarios with both text and tool call streaming events."""
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_streaming_distinguishes_text_and_tool_calls(
|
||||
self, get_temperature_tool_schema: dict[str, Any], mock_emit: MagicMock
|
||||
) -> None:
|
||||
"""Test that streaming correctly distinguishes between text chunks and tool calls."""
|
||||
llm = LLM(model="openai/gpt-4o-mini", stream=True)
|
||||
|
||||
llm.call(
|
||||
messages=[
|
||||
{"role": "user", "content": "What is the temperature in San Francisco?"},
|
||||
],
|
||||
tools=[get_temperature_tool_schema],
|
||||
available_functions={
|
||||
"get_current_temperature": lambda city: f"The temperature in {city} is 72°F"
|
||||
},
|
||||
)
|
||||
|
||||
all_events = get_all_stream_events(mock_emit)
|
||||
tool_call_events = get_tool_call_events(mock_emit)
|
||||
|
||||
assert len(all_events) >= 1, "Should receive streaming events"
|
||||
|
||||
for event in tool_call_events:
|
||||
assert event.call_type == LLMCallType.TOOL_CALL
|
||||
assert event.tool_call is not None
|
||||
|
||||
|
||||
class TestGeminiToolCallStreaming:
|
||||
"""Tests for Gemini provider tool call streaming events."""
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_gemini_streaming_emits_tool_call_events(
|
||||
self, get_temperature_tool_schema: dict[str, Any], mock_emit: MagicMock
|
||||
) -> None:
|
||||
"""Test that Gemini streaming emits tool call events with correct call_type."""
|
||||
llm = LLM(model="gemini/gemini-2.0-flash", stream=True)
|
||||
|
||||
llm.call(
|
||||
messages=[
|
||||
{"role": "user", "content": "What is the temperature in San Francisco?"},
|
||||
],
|
||||
tools=[get_temperature_tool_schema],
|
||||
available_functions={
|
||||
"get_current_temperature": lambda city: f"The temperature in {city} is 72°F"
|
||||
},
|
||||
)
|
||||
|
||||
tool_call_events = get_tool_call_events(mock_emit)
|
||||
|
||||
assert len(tool_call_events) > 0, "Should receive tool call streaming events"
|
||||
|
||||
first_tool_call_event = tool_call_events[0]
|
||||
assert first_tool_call_event.call_type == LLMCallType.TOOL_CALL
|
||||
assert first_tool_call_event.tool_call is not None
|
||||
assert isinstance(first_tool_call_event.tool_call, ToolCall)
|
||||
assert first_tool_call_event.tool_call.function is not None
|
||||
assert first_tool_call_event.tool_call.function.name == "get_current_temperature"
|
||||
assert first_tool_call_event.tool_call.type == "function"
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_gemini_streaming_multiple_tool_calls_unique_ids(
|
||||
self, get_temperature_tool_schema: dict[str, Any], mock_emit: MagicMock
|
||||
) -> None:
|
||||
"""Test that Gemini streaming assigns unique IDs to multiple tool calls."""
|
||||
llm = LLM(model="gemini/gemini-2.0-flash", stream=True)
|
||||
|
||||
llm.call(
|
||||
messages=[
|
||||
{"role": "user", "content": "What is the temperature in Paris and London?"},
|
||||
],
|
||||
tools=[get_temperature_tool_schema],
|
||||
available_functions={
|
||||
"get_current_temperature": lambda city: f"The temperature in {city} is 72°F"
|
||||
},
|
||||
)
|
||||
|
||||
tool_call_events = get_tool_call_events(mock_emit)
|
||||
|
||||
assert len(tool_call_events) >= 2, "Should receive at least 2 tool call events"
|
||||
|
||||
tool_ids = [
|
||||
evt.tool_call.id
|
||||
for evt in tool_call_events
|
||||
if evt.tool_call is not None and evt.tool_call.id
|
||||
]
|
||||
assert len(set(tool_ids)) >= 2, "Each tool call should have a unique ID"
|
||||
|
||||
|
||||
class TestAzureToolCallStreaming:
|
||||
"""Tests for Azure provider tool call streaming events."""
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_azure_streaming_emits_tool_call_events(
|
||||
self, get_temperature_tool_schema: dict[str, Any], mock_emit: MagicMock
|
||||
) -> None:
|
||||
"""Test that Azure streaming emits tool call events with correct call_type."""
|
||||
llm = LLM(model="azure/gpt-4o-mini", stream=True)
|
||||
|
||||
llm.call(
|
||||
messages=[
|
||||
{"role": "user", "content": "What is the temperature in San Francisco?"},
|
||||
],
|
||||
tools=[get_temperature_tool_schema],
|
||||
available_functions={
|
||||
"get_current_temperature": lambda city: f"The temperature in {city} is 72°F"
|
||||
},
|
||||
)
|
||||
|
||||
tool_call_events = get_tool_call_events(mock_emit)
|
||||
|
||||
assert len(tool_call_events) > 0, "Should receive tool call streaming events"
|
||||
|
||||
first_tool_call_event = tool_call_events[0]
|
||||
assert first_tool_call_event.call_type == LLMCallType.TOOL_CALL
|
||||
assert first_tool_call_event.tool_call is not None
|
||||
assert isinstance(first_tool_call_event.tool_call, ToolCall)
|
||||
assert first_tool_call_event.tool_call.function is not None
|
||||
assert first_tool_call_event.tool_call.function.name == "get_current_temperature"
|
||||
assert first_tool_call_event.tool_call.type == "function"
|
||||
|
||||
|
||||
class TestAnthropicToolCallStreaming:
|
||||
"""Tests for Anthropic provider tool call streaming events."""
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_anthropic_streaming_emits_tool_call_events(
|
||||
self, get_temperature_tool_schema: dict[str, Any], mock_emit: MagicMock
|
||||
) -> None:
|
||||
"""Test that Anthropic streaming emits tool call events with correct call_type."""
|
||||
llm = LLM(model="anthropic/claude-3-5-haiku-latest", stream=True)
|
||||
|
||||
llm.call(
|
||||
messages=[
|
||||
{"role": "user", "content": "What is the temperature in San Francisco?"},
|
||||
],
|
||||
tools=[get_temperature_tool_schema],
|
||||
available_functions={
|
||||
"get_current_temperature": lambda city: f"The temperature in {city} is 72°F"
|
||||
},
|
||||
)
|
||||
|
||||
tool_call_events = get_tool_call_events(mock_emit)
|
||||
|
||||
assert len(tool_call_events) > 0, "Should receive tool call streaming events"
|
||||
|
||||
first_tool_call_event = tool_call_events[0]
|
||||
assert first_tool_call_event.call_type == LLMCallType.TOOL_CALL
|
||||
assert first_tool_call_event.tool_call is not None
|
||||
assert isinstance(first_tool_call_event.tool_call, ToolCall)
|
||||
assert first_tool_call_event.tool_call.function is not None
|
||||
assert first_tool_call_event.tool_call.function.name == "get_current_temperature"
|
||||
assert first_tool_call_event.tool_call.type == "function"
|
||||
Reference in New Issue
Block a user