letta-nightly 0.11.7.dev20251006104136__py3-none-any.whl → 0.11.7.dev20251008104128__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- letta/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/letta_llm_request_adapter.py +0 -1
- letta/adapters/letta_llm_stream_adapter.py +7 -2
- letta/adapters/simple_llm_request_adapter.py +88 -0
- letta/adapters/simple_llm_stream_adapter.py +192 -0
- letta/agents/agent_loop.py +6 -0
- letta/agents/ephemeral_summary_agent.py +2 -1
- letta/agents/helpers.py +142 -6
- letta/agents/letta_agent.py +13 -33
- letta/agents/letta_agent_batch.py +2 -4
- letta/agents/letta_agent_v2.py +87 -77
- letta/agents/letta_agent_v3.py +899 -0
- letta/agents/voice_agent.py +2 -6
- letta/constants.py +8 -4
- letta/errors.py +40 -0
- letta/functions/function_sets/base.py +84 -4
- letta/functions/function_sets/multi_agent.py +0 -3
- letta/functions/schema_generator.py +113 -71
- letta/groups/dynamic_multi_agent.py +3 -2
- letta/groups/helpers.py +1 -2
- letta/groups/round_robin_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent_v2.py +1 -1
- letta/groups/sleeptime_multi_agent_v3.py +17 -17
- letta/groups/supervisor_multi_agent.py +84 -80
- letta/helpers/converters.py +3 -0
- letta/helpers/message_helper.py +4 -0
- letta/helpers/tool_rule_solver.py +92 -5
- letta/interfaces/anthropic_streaming_interface.py +409 -0
- letta/interfaces/gemini_streaming_interface.py +296 -0
- letta/interfaces/openai_streaming_interface.py +752 -1
- letta/llm_api/anthropic_client.py +126 -16
- letta/llm_api/bedrock_client.py +4 -2
- letta/llm_api/deepseek_client.py +4 -1
- letta/llm_api/google_vertex_client.py +123 -42
- letta/llm_api/groq_client.py +4 -1
- letta/llm_api/llm_api_tools.py +11 -4
- letta/llm_api/llm_client_base.py +6 -2
- letta/llm_api/openai.py +32 -2
- letta/llm_api/openai_client.py +423 -18
- letta/llm_api/xai_client.py +4 -1
- letta/main.py +9 -5
- letta/memory.py +1 -0
- letta/orm/__init__.py +1 -1
- letta/orm/agent.py +10 -0
- letta/orm/block.py +7 -16
- letta/orm/blocks_agents.py +8 -2
- letta/orm/files_agents.py +2 -0
- letta/orm/job.py +7 -5
- letta/orm/mcp_oauth.py +1 -0
- letta/orm/message.py +21 -6
- letta/orm/organization.py +2 -0
- letta/orm/provider.py +6 -2
- letta/orm/run.py +71 -0
- letta/orm/sandbox_config.py +7 -1
- letta/orm/sqlalchemy_base.py +0 -306
- letta/orm/step.py +6 -5
- letta/orm/step_metrics.py +5 -5
- letta/otel/tracing.py +28 -3
- letta/plugins/defaults.py +4 -4
- letta/prompts/system_prompts/__init__.py +2 -0
- letta/prompts/system_prompts/letta_v1.py +25 -0
- letta/schemas/agent.py +3 -2
- letta/schemas/agent_file.py +9 -3
- letta/schemas/block.py +23 -10
- letta/schemas/enums.py +21 -2
- letta/schemas/job.py +17 -4
- letta/schemas/letta_message_content.py +71 -2
- letta/schemas/letta_stop_reason.py +5 -5
- letta/schemas/llm_config.py +53 -3
- letta/schemas/memory.py +1 -1
- letta/schemas/message.py +504 -117
- letta/schemas/openai/responses_request.py +64 -0
- letta/schemas/providers/__init__.py +2 -0
- letta/schemas/providers/anthropic.py +16 -0
- letta/schemas/providers/ollama.py +115 -33
- letta/schemas/providers/openrouter.py +52 -0
- letta/schemas/providers/vllm.py +2 -1
- letta/schemas/run.py +48 -42
- letta/schemas/step.py +2 -2
- letta/schemas/step_metrics.py +1 -1
- letta/schemas/tool.py +15 -107
- letta/schemas/tool_rule.py +88 -5
- letta/serialize_schemas/marshmallow_agent.py +1 -0
- letta/server/db.py +86 -408
- letta/server/rest_api/app.py +61 -10
- letta/server/rest_api/dependencies.py +14 -0
- letta/server/rest_api/redis_stream_manager.py +19 -8
- letta/server/rest_api/routers/v1/agents.py +364 -292
- letta/server/rest_api/routers/v1/blocks.py +14 -20
- letta/server/rest_api/routers/v1/identities.py +45 -110
- letta/server/rest_api/routers/v1/internal_templates.py +21 -0
- letta/server/rest_api/routers/v1/jobs.py +23 -6
- letta/server/rest_api/routers/v1/messages.py +1 -1
- letta/server/rest_api/routers/v1/runs.py +126 -85
- letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
- letta/server/rest_api/routers/v1/tools.py +281 -594
- letta/server/rest_api/routers/v1/voice.py +1 -1
- letta/server/rest_api/streaming_response.py +29 -29
- letta/server/rest_api/utils.py +122 -64
- letta/server/server.py +160 -887
- letta/services/agent_manager.py +236 -919
- letta/services/agent_serialization_manager.py +16 -0
- letta/services/archive_manager.py +0 -100
- letta/services/block_manager.py +211 -168
- letta/services/file_manager.py +1 -1
- letta/services/files_agents_manager.py +24 -33
- letta/services/group_manager.py +0 -142
- letta/services/helpers/agent_manager_helper.py +7 -2
- letta/services/helpers/run_manager_helper.py +85 -0
- letta/services/job_manager.py +96 -411
- letta/services/lettuce/__init__.py +6 -0
- letta/services/lettuce/lettuce_client_base.py +86 -0
- letta/services/mcp_manager.py +38 -6
- letta/services/message_manager.py +165 -362
- letta/services/organization_manager.py +0 -36
- letta/services/passage_manager.py +0 -345
- letta/services/provider_manager.py +0 -80
- letta/services/run_manager.py +301 -0
- letta/services/sandbox_config_manager.py +0 -234
- letta/services/step_manager.py +62 -39
- letta/services/summarizer/summarizer.py +9 -7
- letta/services/telemetry_manager.py +0 -16
- letta/services/tool_executor/builtin_tool_executor.py +35 -0
- letta/services/tool_executor/core_tool_executor.py +397 -2
- letta/services/tool_executor/files_tool_executor.py +3 -3
- letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
- letta/services/tool_executor/tool_execution_manager.py +6 -8
- letta/services/tool_executor/tool_executor_base.py +3 -3
- letta/services/tool_manager.py +85 -339
- letta/services/tool_sandbox/base.py +24 -13
- letta/services/tool_sandbox/e2b_sandbox.py +16 -1
- letta/services/tool_schema_generator.py +123 -0
- letta/services/user_manager.py +0 -99
- letta/settings.py +20 -4
- {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/METADATA +3 -5
- {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/RECORD +140 -132
- letta/agents/temporal/activities/__init__.py +0 -4
- letta/agents/temporal/activities/example_activity.py +0 -7
- letta/agents/temporal/activities/prepare_messages.py +0 -10
- letta/agents/temporal/temporal_agent_workflow.py +0 -56
- letta/agents/temporal/types.py +0 -25
- {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/licenses/LICENSE +0 -0
@@ -5,6 +5,28 @@ from typing import Optional
|
|
5
5
|
|
6
6
|
from openai import AsyncStream
|
7
7
|
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
|
8
|
+
from openai.types.responses import (
|
9
|
+
ResponseCompletedEvent,
|
10
|
+
ResponseContentPartAddedEvent,
|
11
|
+
ResponseContentPartDoneEvent,
|
12
|
+
ResponseCreatedEvent,
|
13
|
+
ResponseFunctionCallArgumentsDeltaEvent,
|
14
|
+
ResponseFunctionCallArgumentsDoneEvent,
|
15
|
+
ResponseFunctionToolCall,
|
16
|
+
ResponseInProgressEvent,
|
17
|
+
ResponseOutputItemAddedEvent,
|
18
|
+
ResponseOutputItemDoneEvent,
|
19
|
+
ResponseOutputMessage,
|
20
|
+
ResponseOutputText,
|
21
|
+
ResponseReasoningItem,
|
22
|
+
ResponseReasoningSummaryPartAddedEvent,
|
23
|
+
ResponseReasoningSummaryPartDoneEvent,
|
24
|
+
ResponseReasoningSummaryTextDeltaEvent,
|
25
|
+
ResponseReasoningSummaryTextDoneEvent,
|
26
|
+
ResponseTextDeltaEvent,
|
27
|
+
ResponseTextDoneEvent,
|
28
|
+
)
|
29
|
+
from openai.types.responses.response_stream_event import ResponseStreamEvent
|
8
30
|
|
9
31
|
from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
|
10
32
|
from letta.llm_api.openai_client import is_openai_reasoning_model
|
@@ -19,7 +41,12 @@ from letta.schemas.letta_message import (
|
|
19
41
|
ToolCallDelta,
|
20
42
|
ToolCallMessage,
|
21
43
|
)
|
22
|
-
from letta.schemas.letta_message_content import
|
44
|
+
from letta.schemas.letta_message_content import (
|
45
|
+
OmittedReasoningContent,
|
46
|
+
SummarizedReasoningContent,
|
47
|
+
SummarizedReasoningContentPart,
|
48
|
+
TextContent,
|
49
|
+
)
|
23
50
|
from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
|
24
51
|
from letta.schemas.message import Message
|
25
52
|
from letta.schemas.openai.chat_completion_response import FunctionCall, ToolCall
|
@@ -49,11 +76,15 @@ class OpenAIStreamingInterface:
|
|
49
76
|
tools: Optional[list] = None,
|
50
77
|
put_inner_thoughts_in_kwarg: bool = True,
|
51
78
|
requires_approval_tools: list = [],
|
79
|
+
run_id: str | None = None,
|
80
|
+
step_id: str | None = None,
|
52
81
|
):
|
53
82
|
self.use_assistant_message = use_assistant_message
|
54
83
|
self.assistant_message_tool_name = DEFAULT_MESSAGE_TOOL
|
55
84
|
self.assistant_message_tool_kwarg = DEFAULT_MESSAGE_TOOL_KWARG
|
56
85
|
self.put_inner_thoughts_in_kwarg = put_inner_thoughts_in_kwarg
|
86
|
+
self.run_id = run_id
|
87
|
+
self.step_id = step_id
|
57
88
|
|
58
89
|
self.optimistic_json_parser: OptimisticJSONParser = OptimisticJSONParser()
|
59
90
|
self.function_args_reader = JSONInnerThoughtsExtractor(wait_for_first_key=put_inner_thoughts_in_kwarg)
|
@@ -217,6 +248,8 @@ class OpenAIStreamingInterface:
|
|
217
248
|
state="omitted",
|
218
249
|
hidden_reasoning=None,
|
219
250
|
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
251
|
+
run_id=self.run_id,
|
252
|
+
step_id=self.step_id,
|
220
253
|
)
|
221
254
|
yield hidden_message
|
222
255
|
prev_message_type = hidden_message.message_type
|
@@ -256,6 +289,8 @@ class OpenAIStreamingInterface:
|
|
256
289
|
reasoning=updates_inner_thoughts,
|
257
290
|
# name=name,
|
258
291
|
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
292
|
+
run_id=self.run_id,
|
293
|
+
step_id=self.step_id,
|
259
294
|
)
|
260
295
|
prev_message_type = reasoning_message.message_type
|
261
296
|
yield reasoning_message
|
@@ -297,6 +332,8 @@ class OpenAIStreamingInterface:
|
|
297
332
|
tool_call_id=self.function_id_buffer,
|
298
333
|
),
|
299
334
|
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
335
|
+
run_id=self.run_id,
|
336
|
+
step_id=self.step_id,
|
300
337
|
)
|
301
338
|
else:
|
302
339
|
tool_call_msg = ToolCallMessage(
|
@@ -308,6 +345,8 @@ class OpenAIStreamingInterface:
|
|
308
345
|
tool_call_id=self.function_id_buffer,
|
309
346
|
),
|
310
347
|
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
348
|
+
run_id=self.run_id,
|
349
|
+
step_id=self.step_id,
|
311
350
|
)
|
312
351
|
prev_message_type = tool_call_msg.message_type
|
313
352
|
yield tool_call_msg
|
@@ -353,6 +392,8 @@ class OpenAIStreamingInterface:
|
|
353
392
|
date=datetime.now(timezone.utc),
|
354
393
|
content=extracted,
|
355
394
|
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
395
|
+
run_id=self.run_id,
|
396
|
+
step_id=self.step_id,
|
356
397
|
)
|
357
398
|
prev_message_type = assistant_message.message_type
|
358
399
|
yield assistant_message
|
@@ -378,6 +419,8 @@ class OpenAIStreamingInterface:
|
|
378
419
|
),
|
379
420
|
# name=name,
|
380
421
|
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
422
|
+
run_id=self.run_id,
|
423
|
+
step_id=self.step_id,
|
381
424
|
)
|
382
425
|
else:
|
383
426
|
tool_call_msg = ToolCallMessage(
|
@@ -390,6 +433,8 @@ class OpenAIStreamingInterface:
|
|
390
433
|
),
|
391
434
|
# name=name,
|
392
435
|
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
436
|
+
run_id=self.run_id,
|
437
|
+
step_id=self.step_id,
|
393
438
|
)
|
394
439
|
prev_message_type = tool_call_msg.message_type
|
395
440
|
yield tool_call_msg
|
@@ -411,6 +456,8 @@ class OpenAIStreamingInterface:
|
|
411
456
|
),
|
412
457
|
# name=name,
|
413
458
|
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
459
|
+
run_id=self.run_id,
|
460
|
+
step_id=self.step_id,
|
414
461
|
)
|
415
462
|
else:
|
416
463
|
tool_call_msg = ToolCallMessage(
|
@@ -423,7 +470,711 @@ class OpenAIStreamingInterface:
|
|
423
470
|
),
|
424
471
|
# name=name,
|
425
472
|
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
473
|
+
run_id=self.run_id,
|
474
|
+
step_id=self.step_id,
|
426
475
|
)
|
427
476
|
prev_message_type = tool_call_msg.message_type
|
428
477
|
yield tool_call_msg
|
429
478
|
self.function_id_buffer = None
|
479
|
+
|
480
|
+
|
481
|
+
class SimpleOpenAIStreamingInterface:
|
482
|
+
"""
|
483
|
+
Encapsulates the logic for streaming responses from OpenAI.
|
484
|
+
This class handles parsing of partial tokens, pre-execution messages,
|
485
|
+
and detection of tool call events.
|
486
|
+
"""
|
487
|
+
|
488
|
+
def __init__(
|
489
|
+
self,
|
490
|
+
is_openai_proxy: bool = False,
|
491
|
+
messages: Optional[list] = None,
|
492
|
+
tools: Optional[list] = None,
|
493
|
+
requires_approval_tools: list = [],
|
494
|
+
model: str = None,
|
495
|
+
run_id: str | None = None,
|
496
|
+
step_id: str | None = None,
|
497
|
+
):
|
498
|
+
self.run_id = run_id
|
499
|
+
self.step_id = step_id
|
500
|
+
# Premake IDs for database writes
|
501
|
+
self.letta_message_id = Message.generate_id()
|
502
|
+
|
503
|
+
self.message_id = None
|
504
|
+
self.model = model
|
505
|
+
|
506
|
+
# Token counters (from OpenAI usage)
|
507
|
+
self.input_tokens = 0
|
508
|
+
self.output_tokens = 0
|
509
|
+
|
510
|
+
# Fallback token counters (using tiktoken cl200k-base)
|
511
|
+
self.fallback_input_tokens = 0
|
512
|
+
self.fallback_output_tokens = 0
|
513
|
+
|
514
|
+
# Store messages and tools for fallback counting
|
515
|
+
self.is_openai_proxy = is_openai_proxy
|
516
|
+
self.messages = messages or []
|
517
|
+
self.tools = tools or []
|
518
|
+
|
519
|
+
# Buffers to hold accumulating tools
|
520
|
+
self.tool_call_name = ""
|
521
|
+
self.tool_call_args = ""
|
522
|
+
self.tool_call_id = ""
|
523
|
+
|
524
|
+
self.content_messages = []
|
525
|
+
self.emitted_hidden_reasoning = False # Track if we've emitted hidden reasoning message
|
526
|
+
|
527
|
+
self.requires_approval_tools = requires_approval_tools
|
528
|
+
|
529
|
+
def get_content(self) -> list[TextContent | OmittedReasoningContent]:
|
530
|
+
shown_omitted = False
|
531
|
+
concat_content = ""
|
532
|
+
merged_messages = []
|
533
|
+
for msg in self.content_messages:
|
534
|
+
if isinstance(msg, HiddenReasoningMessage) and not shown_omitted:
|
535
|
+
merged_messages.append(OmittedReasoningContent())
|
536
|
+
shown_omitted = True
|
537
|
+
elif isinstance(msg, AssistantMessage):
|
538
|
+
if isinstance(msg.content, list):
|
539
|
+
concat_content += "".join([c.text for c in msg.content])
|
540
|
+
else:
|
541
|
+
concat_content += msg.content
|
542
|
+
merged_messages.append(TextContent(text=concat_content))
|
543
|
+
return merged_messages
|
544
|
+
|
545
|
+
def get_tool_call_object(self) -> ToolCall:
|
546
|
+
"""Useful for agent loop"""
|
547
|
+
if not self.tool_call_name:
|
548
|
+
raise ValueError("No tool call name available")
|
549
|
+
if not self.tool_call_args:
|
550
|
+
raise ValueError("No tool call arguments available")
|
551
|
+
if not self.tool_call_id:
|
552
|
+
raise ValueError("No tool call ID available")
|
553
|
+
|
554
|
+
return ToolCall(
|
555
|
+
id=self.tool_call_id,
|
556
|
+
function=FunctionCall(arguments=self.tool_call_args, name=self.tool_call_name),
|
557
|
+
)
|
558
|
+
|
559
|
+
async def process(
|
560
|
+
self,
|
561
|
+
stream: AsyncStream[ChatCompletionChunk],
|
562
|
+
ttft_span: Optional["Span"] = None,
|
563
|
+
) -> AsyncGenerator[LettaMessage | LettaStopReason, None]:
|
564
|
+
"""
|
565
|
+
Iterates over the OpenAI stream, yielding SSE events.
|
566
|
+
It also collects tokens and detects if a tool call is triggered.
|
567
|
+
"""
|
568
|
+
# Fallback input token counting - this should only be required for non-OpenAI providers using the OpenAI client (e.g. LMStudio)
|
569
|
+
if self.is_openai_proxy:
|
570
|
+
if self.messages:
|
571
|
+
# Convert messages to dict format for token counting
|
572
|
+
message_dicts = [msg.to_openai_dict() if hasattr(msg, "to_openai_dict") else msg for msg in self.messages]
|
573
|
+
message_dicts = [m for m in message_dicts if m is not None]
|
574
|
+
self.fallback_input_tokens = num_tokens_from_messages(message_dicts) # fallback to gpt-4 cl100k-base
|
575
|
+
|
576
|
+
if self.tools:
|
577
|
+
# Convert tools to dict format for token counting
|
578
|
+
tool_dicts = [tool["function"] if isinstance(tool, dict) and "function" in tool else tool for tool in self.tools]
|
579
|
+
self.fallback_input_tokens += num_tokens_from_functions(tool_dicts)
|
580
|
+
|
581
|
+
prev_message_type = None
|
582
|
+
message_index = 0
|
583
|
+
try:
|
584
|
+
async with stream:
|
585
|
+
# For reasoning models, emit a hidden reasoning message before the first chunk
|
586
|
+
if not self.emitted_hidden_reasoning and is_openai_reasoning_model(self.model):
|
587
|
+
self.emitted_hidden_reasoning = True
|
588
|
+
hidden_message = HiddenReasoningMessage(
|
589
|
+
id=self.letta_message_id,
|
590
|
+
date=datetime.now(timezone.utc),
|
591
|
+
state="omitted",
|
592
|
+
hidden_reasoning=None,
|
593
|
+
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
594
|
+
run_id=self.run_id,
|
595
|
+
step_id=self.step_id,
|
596
|
+
)
|
597
|
+
self.content_messages.append(hidden_message)
|
598
|
+
prev_message_type = hidden_message.message_type
|
599
|
+
message_index += 1 # Increment for the next message
|
600
|
+
yield hidden_message
|
601
|
+
|
602
|
+
async for chunk in stream:
|
603
|
+
try:
|
604
|
+
async for message in self._process_chunk(chunk, ttft_span, prev_message_type, message_index):
|
605
|
+
new_message_type = message.message_type
|
606
|
+
if new_message_type != prev_message_type:
|
607
|
+
if prev_message_type != None:
|
608
|
+
message_index += 1
|
609
|
+
prev_message_type = new_message_type
|
610
|
+
yield message
|
611
|
+
except asyncio.CancelledError as e:
|
612
|
+
import traceback
|
613
|
+
|
614
|
+
logger.info("Cancelled stream attempt but overriding %s: %s", e, traceback.format_exc())
|
615
|
+
async for message in self._process_chunk(chunk, ttft_span, prev_message_type, message_index):
|
616
|
+
new_message_type = message.message_type
|
617
|
+
if new_message_type != prev_message_type:
|
618
|
+
if prev_message_type != None:
|
619
|
+
message_index += 1
|
620
|
+
prev_message_type = new_message_type
|
621
|
+
yield message
|
622
|
+
|
623
|
+
# Don't raise the exception here
|
624
|
+
continue
|
625
|
+
|
626
|
+
except Exception as e:
|
627
|
+
import traceback
|
628
|
+
|
629
|
+
logger.error("Error processing stream: %s\n%s", e, traceback.format_exc())
|
630
|
+
if ttft_span:
|
631
|
+
ttft_span.add_event(
|
632
|
+
name="stop_reason",
|
633
|
+
attributes={"stop_reason": StopReasonType.error.value, "error": str(e), "stacktrace": traceback.format_exc()},
|
634
|
+
)
|
635
|
+
yield LettaStopReason(stop_reason=StopReasonType.error)
|
636
|
+
raise e
|
637
|
+
finally:
|
638
|
+
logger.info("OpenAIStreamingInterface: Stream processing complete.")
|
639
|
+
|
640
|
+
async def _process_chunk(
|
641
|
+
self,
|
642
|
+
chunk: ChatCompletionChunk,
|
643
|
+
ttft_span: Optional["Span"] = None,
|
644
|
+
prev_message_type: Optional[str] = None,
|
645
|
+
message_index: int = 0,
|
646
|
+
) -> AsyncGenerator[LettaMessage | LettaStopReason, None]:
|
647
|
+
if not self.model or not self.message_id:
|
648
|
+
self.model = chunk.model
|
649
|
+
self.message_id = chunk.id
|
650
|
+
|
651
|
+
# track usage
|
652
|
+
if chunk.usage:
|
653
|
+
self.input_tokens += chunk.usage.prompt_tokens
|
654
|
+
self.output_tokens += chunk.usage.completion_tokens
|
655
|
+
|
656
|
+
if chunk.choices:
|
657
|
+
choice = chunk.choices[0]
|
658
|
+
message_delta = choice.delta
|
659
|
+
|
660
|
+
if message_delta.content is not None and message_delta.content != "":
|
661
|
+
assistant_msg = AssistantMessage(
|
662
|
+
id=self.letta_message_id,
|
663
|
+
content=[TextContent(text=message_delta.content)],
|
664
|
+
date=datetime.now(timezone.utc).isoformat(),
|
665
|
+
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
666
|
+
run_id=self.run_id,
|
667
|
+
step_id=self.step_id,
|
668
|
+
)
|
669
|
+
self.content_messages.append(assistant_msg)
|
670
|
+
prev_message_type = assistant_msg.message_type
|
671
|
+
message_index += 1 # Increment for the next message
|
672
|
+
yield assistant_msg
|
673
|
+
|
674
|
+
if message_delta.tool_calls is not None and len(message_delta.tool_calls) > 0:
|
675
|
+
tool_call = message_delta.tool_calls[0]
|
676
|
+
|
677
|
+
# For OpenAI reasoning models, emit a hidden reasoning message before the first tool call
|
678
|
+
# if not self.emitted_hidden_reasoning and is_openai_reasoning_model(self.model):
|
679
|
+
# self.emitted_hidden_reasoning = True
|
680
|
+
# if prev_message_type and prev_message_type != "hidden_reasoning_message":
|
681
|
+
# message_index += 1
|
682
|
+
# hidden_message = HiddenReasoningMessage(
|
683
|
+
# id=self.letta_message_id,
|
684
|
+
# date=datetime.now(timezone.utc),
|
685
|
+
# state="omitted",
|
686
|
+
# hidden_reasoning=None,
|
687
|
+
# otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
688
|
+
# )
|
689
|
+
# self.content_messages.append(hidden_message)
|
690
|
+
# prev_message_type = hidden_message.message_type
|
691
|
+
# message_index += 1 # Increment for the next message
|
692
|
+
# yield hidden_message
|
693
|
+
|
694
|
+
if not tool_call.function.name and not tool_call.function.arguments and not tool_call.id:
|
695
|
+
# No chunks to process, exit
|
696
|
+
return
|
697
|
+
|
698
|
+
if tool_call.function.name:
|
699
|
+
self.tool_call_name += tool_call.function.name
|
700
|
+
if tool_call.function.arguments:
|
701
|
+
self.tool_call_args += tool_call.function.arguments
|
702
|
+
if tool_call.id:
|
703
|
+
self.tool_call_id += tool_call.id
|
704
|
+
|
705
|
+
if self.requires_approval_tools:
|
706
|
+
tool_call_msg = ApprovalRequestMessage(
|
707
|
+
id=self.letta_message_id,
|
708
|
+
date=datetime.now(timezone.utc),
|
709
|
+
tool_call=ToolCallDelta(
|
710
|
+
name=tool_call.function.name,
|
711
|
+
arguments=tool_call.function.arguments,
|
712
|
+
tool_call_id=tool_call.id,
|
713
|
+
),
|
714
|
+
# name=name,
|
715
|
+
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
716
|
+
run_id=self.run_id,
|
717
|
+
step_id=self.step_id,
|
718
|
+
)
|
719
|
+
else:
|
720
|
+
tool_call_msg = ToolCallMessage(
|
721
|
+
id=self.letta_message_id,
|
722
|
+
date=datetime.now(timezone.utc),
|
723
|
+
tool_call=ToolCallDelta(
|
724
|
+
name=tool_call.function.name,
|
725
|
+
arguments=tool_call.function.arguments,
|
726
|
+
tool_call_id=tool_call.id,
|
727
|
+
),
|
728
|
+
# name=name,
|
729
|
+
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
730
|
+
run_id=self.run_id,
|
731
|
+
step_id=self.step_id,
|
732
|
+
)
|
733
|
+
prev_message_type = tool_call_msg.message_type
|
734
|
+
message_index += 1 # Increment for the next message
|
735
|
+
yield tool_call_msg
|
736
|
+
|
737
|
+
|
738
|
+
class SimpleOpenAIResponsesStreamingInterface:
|
739
|
+
"""
|
740
|
+
Encapsulates the logic for streaming responses from OpenAI Responses API.
|
741
|
+
"""
|
742
|
+
|
743
|
+
def __init__(
|
744
|
+
self,
|
745
|
+
is_openai_proxy: bool = False,
|
746
|
+
messages: Optional[list] = None,
|
747
|
+
tools: Optional[list] = None,
|
748
|
+
requires_approval_tools: list = [],
|
749
|
+
model: str = None,
|
750
|
+
run_id: str | None = None,
|
751
|
+
step_id: str | None = None,
|
752
|
+
):
|
753
|
+
self.is_openai_proxy = is_openai_proxy
|
754
|
+
self.messages = messages
|
755
|
+
self.tools = tools
|
756
|
+
self.requires_approval_tools = requires_approval_tools
|
757
|
+
# We need to store the name for approvals
|
758
|
+
self.tool_call_name = None
|
759
|
+
# ID responses used
|
760
|
+
self.message_id = None
|
761
|
+
self.run_id = run_id
|
762
|
+
self.step_id = step_id
|
763
|
+
|
764
|
+
# Premake IDs for database writes
|
765
|
+
self.letta_message_id = Message.generate_id()
|
766
|
+
self.model = model
|
767
|
+
self.final_response = None
|
768
|
+
|
769
|
+
def get_content(self) -> list[TextContent | SummarizedReasoningContent]:
|
770
|
+
"""This includes both SummarizedReasoningContent and TextContent"""
|
771
|
+
if self.final_response is None:
|
772
|
+
raise ValueError("No final response available")
|
773
|
+
|
774
|
+
content = []
|
775
|
+
for response in self.final_response.output:
|
776
|
+
if isinstance(response, ResponseReasoningItem):
|
777
|
+
# TODO consider cleaning up our representation to not require indexing
|
778
|
+
letta_summary = [SummarizedReasoningContentPart(index=i, text=part.text) for i, part in enumerate(response.summary)]
|
779
|
+
content.append(
|
780
|
+
SummarizedReasoningContent(
|
781
|
+
id=response.id,
|
782
|
+
summary=letta_summary,
|
783
|
+
encrypted_content=response.encrypted_content,
|
784
|
+
)
|
785
|
+
)
|
786
|
+
elif isinstance(response, ResponseOutputMessage):
|
787
|
+
if len(response.content) == 1:
|
788
|
+
content.append(
|
789
|
+
TextContent(
|
790
|
+
text=response.content[0].text,
|
791
|
+
)
|
792
|
+
)
|
793
|
+
else:
|
794
|
+
raise ValueError(f"Got {len(response.content)} content parts, expected 1")
|
795
|
+
|
796
|
+
return content
|
797
|
+
|
798
|
+
def get_tool_call_object(self) -> ToolCall:
|
799
|
+
"""Useful for agent loop"""
|
800
|
+
if self.final_response is None:
|
801
|
+
raise ValueError("No final response available")
|
802
|
+
|
803
|
+
tool_calls = []
|
804
|
+
for response in self.final_response.output:
|
805
|
+
# TODO make sure this shouldn't be ResponseCustomToolCall?
|
806
|
+
if isinstance(response, ResponseFunctionToolCall):
|
807
|
+
tool_calls.append(
|
808
|
+
ToolCall(
|
809
|
+
id=response.call_id,
|
810
|
+
function=FunctionCall(
|
811
|
+
name=response.name,
|
812
|
+
arguments=response.arguments,
|
813
|
+
),
|
814
|
+
)
|
815
|
+
)
|
816
|
+
|
817
|
+
if len(tool_calls) == 0:
|
818
|
+
raise ValueError("No tool calls available")
|
819
|
+
if len(tool_calls) > 1:
|
820
|
+
raise ValueError(f"Got {len(tool_calls)} tool calls, expected 1")
|
821
|
+
|
822
|
+
return tool_calls[0]
|
823
|
+
|
824
|
+
async def process(
|
825
|
+
self,
|
826
|
+
stream: AsyncStream[ResponseStreamEvent],
|
827
|
+
ttft_span: Optional["Span"] = None,
|
828
|
+
) -> AsyncGenerator[LettaMessage | LettaStopReason, None]:
|
829
|
+
"""
|
830
|
+
Iterates over the OpenAI stream, yielding SSE events.
|
831
|
+
It also collects tokens and detects if a tool call is triggered.
|
832
|
+
"""
|
833
|
+
# Fallback input token counting - this should only be required for non-OpenAI providers using the OpenAI client (e.g. LMStudio)
|
834
|
+
if self.is_openai_proxy:
|
835
|
+
raise NotImplementedError("OpenAI proxy is not supported for OpenAI Responses API")
|
836
|
+
|
837
|
+
prev_message_type = None
|
838
|
+
message_index = 0
|
839
|
+
try:
|
840
|
+
async with stream:
|
841
|
+
async for event in stream:
|
842
|
+
try:
|
843
|
+
async for message in self._process_event(event, ttft_span, prev_message_type, message_index):
|
844
|
+
new_message_type = message.message_type
|
845
|
+
if new_message_type != prev_message_type:
|
846
|
+
if prev_message_type != None:
|
847
|
+
message_index += 1
|
848
|
+
prev_message_type = new_message_type
|
849
|
+
yield message
|
850
|
+
except asyncio.CancelledError as e:
|
851
|
+
import traceback
|
852
|
+
|
853
|
+
logger.info("Cancelled stream attempt but overriding %s: %s", e, traceback.format_exc())
|
854
|
+
async for message in self._process_event(event, ttft_span, prev_message_type, message_index):
|
855
|
+
new_message_type = message.message_type
|
856
|
+
if new_message_type != prev_message_type:
|
857
|
+
if prev_message_type != None:
|
858
|
+
message_index += 1
|
859
|
+
prev_message_type = new_message_type
|
860
|
+
yield message
|
861
|
+
|
862
|
+
# Don't raise the exception here
|
863
|
+
continue
|
864
|
+
|
865
|
+
except Exception as e:
|
866
|
+
import traceback
|
867
|
+
|
868
|
+
logger.error("Error processing stream: %s\n%s", e, traceback.format_exc())
|
869
|
+
if ttft_span:
|
870
|
+
ttft_span.add_event(
|
871
|
+
name="stop_reason",
|
872
|
+
attributes={"stop_reason": StopReasonType.error.value, "error": str(e), "stacktrace": traceback.format_exc()},
|
873
|
+
)
|
874
|
+
yield LettaStopReason(stop_reason=StopReasonType.error)
|
875
|
+
raise e
|
876
|
+
finally:
|
877
|
+
logger.info("OpenAIStreamingInterface: Stream processing complete.")
|
878
|
+
|
879
|
+
async def _process_event(
|
880
|
+
self,
|
881
|
+
event: ResponseStreamEvent,
|
882
|
+
ttft_span: Optional["Span"] = None,
|
883
|
+
prev_message_type: Optional[str] = None,
|
884
|
+
message_index: int = 0,
|
885
|
+
) -> AsyncGenerator[LettaMessage | LettaStopReason, None]:
|
886
|
+
if isinstance(event, ResponseCreatedEvent):
|
887
|
+
# No-op, just had the input events
|
888
|
+
return
|
889
|
+
# or yield None?
|
890
|
+
|
891
|
+
elif isinstance(event, ResponseInProgressEvent):
|
892
|
+
# No-op, just an indicator that we've started
|
893
|
+
return
|
894
|
+
|
895
|
+
elif isinstance(event, ResponseOutputItemAddedEvent):
|
896
|
+
new_event_item = event.item
|
897
|
+
|
898
|
+
# New "item" was added, can be reasoning, tool call, or content
|
899
|
+
if isinstance(new_event_item, ResponseReasoningItem):
|
900
|
+
# Look for summary delta, or encrypted_content
|
901
|
+
summary = new_event_item.summary
|
902
|
+
content = new_event_item.content # NOTE: always none
|
903
|
+
encrypted_content = new_event_item.encrypted_content
|
904
|
+
# TODO change to summarize reasoning message, but we need to figure out the streaming indices of summary problem
|
905
|
+
concat_summary = "".join([s.text for s in summary])
|
906
|
+
if concat_summary != "":
|
907
|
+
if prev_message_type and prev_message_type != "reasoning_message":
|
908
|
+
message_index += 1
|
909
|
+
yield ReasoningMessage(
|
910
|
+
id=self.letta_message_id,
|
911
|
+
date=datetime.now(timezone.utc).isoformat(),
|
912
|
+
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
913
|
+
source="reasoner_model",
|
914
|
+
reasoning=concat_summary,
|
915
|
+
run_id=self.run_id,
|
916
|
+
step_id=self.step_id,
|
917
|
+
)
|
918
|
+
prev_message_type = "reasoning_message"
|
919
|
+
else:
|
920
|
+
return
|
921
|
+
|
922
|
+
elif isinstance(new_event_item, ResponseFunctionToolCall):
|
923
|
+
# Look for call_id, name, and possibly arguments (though likely always empty string)
|
924
|
+
call_id = new_event_item.call_id
|
925
|
+
name = new_event_item.name
|
926
|
+
arguments = new_event_item.arguments
|
927
|
+
# cache for approval if/elses
|
928
|
+
self.tool_call_name = name
|
929
|
+
if self.tool_call_name and self.tool_call_name in self.requires_approval_tools:
|
930
|
+
if prev_message_type and prev_message_type != "approval_request_message":
|
931
|
+
message_index += 1
|
932
|
+
yield ApprovalRequestMessage(
|
933
|
+
id=self.letta_message_id,
|
934
|
+
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
935
|
+
date=datetime.now(timezone.utc),
|
936
|
+
tool_call=ToolCallDelta(
|
937
|
+
name=name,
|
938
|
+
arguments=arguments if arguments != "" else None,
|
939
|
+
tool_call_id=call_id,
|
940
|
+
),
|
941
|
+
run_id=self.run_id,
|
942
|
+
step_id=self.step_id,
|
943
|
+
)
|
944
|
+
prev_message_type = "tool_call_message"
|
945
|
+
else:
|
946
|
+
if prev_message_type and prev_message_type != "tool_call_message":
|
947
|
+
message_index += 1
|
948
|
+
yield ToolCallMessage(
|
949
|
+
id=self.letta_message_id,
|
950
|
+
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
951
|
+
date=datetime.now(timezone.utc),
|
952
|
+
tool_call=ToolCallDelta(
|
953
|
+
name=name,
|
954
|
+
arguments=arguments if arguments != "" else None,
|
955
|
+
tool_call_id=call_id,
|
956
|
+
),
|
957
|
+
run_id=self.run_id,
|
958
|
+
step_id=self.step_id,
|
959
|
+
)
|
960
|
+
prev_message_type = "tool_call_message"
|
961
|
+
|
962
|
+
elif isinstance(new_event_item, ResponseOutputMessage):
|
963
|
+
# Look for content (may be empty list []), or contain ResponseOutputText
|
964
|
+
if len(new_event_item.content) > 0:
|
965
|
+
for content_item in new_event_item.content:
|
966
|
+
if isinstance(content_item, ResponseOutputText):
|
967
|
+
# Add this as a AssistantMessage part
|
968
|
+
if prev_message_type and prev_message_type != "assistant_message":
|
969
|
+
message_index += 1
|
970
|
+
yield AssistantMessage(
|
971
|
+
id=self.letta_message_id,
|
972
|
+
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
973
|
+
date=datetime.now(timezone.utc),
|
974
|
+
content=content_item.text,
|
975
|
+
run_id=self.run_id,
|
976
|
+
step_id=self.step_id,
|
977
|
+
)
|
978
|
+
prev_message_type = "assistant_message"
|
979
|
+
else:
|
980
|
+
return
|
981
|
+
|
982
|
+
else:
|
983
|
+
# Other types we don't handle, ignore
|
984
|
+
return
|
985
|
+
|
986
|
+
# Reasoning summary is streaming in
|
987
|
+
# TODO / FIXME return a SummaryReasoning type
|
988
|
+
elif isinstance(event, ResponseReasoningSummaryPartAddedEvent):
|
989
|
+
# This means the part got added, but likely no content yet (likely empty string)
|
990
|
+
summary_index = event.summary_index
|
991
|
+
part = event.part
|
992
|
+
|
993
|
+
# If this is a follow-up summary part, we need to add leading newlines
|
994
|
+
if summary_index > 1:
|
995
|
+
summary_text = "\n\n" + part.text
|
996
|
+
else:
|
997
|
+
summary_text = part.text
|
998
|
+
|
999
|
+
if prev_message_type and prev_message_type != "reasoning_message":
|
1000
|
+
message_index += 1
|
1001
|
+
yield ReasoningMessage(
|
1002
|
+
id=self.letta_message_id,
|
1003
|
+
date=datetime.now(timezone.utc).isoformat(),
|
1004
|
+
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
1005
|
+
source="reasoner_model",
|
1006
|
+
reasoning=summary_text,
|
1007
|
+
run_id=self.run_id,
|
1008
|
+
)
|
1009
|
+
prev_message_type = "reasoning_message"
|
1010
|
+
|
1011
|
+
# Reasoning summary streaming
|
1012
|
+
elif isinstance(event, ResponseReasoningSummaryTextDeltaEvent):
|
1013
|
+
# NOTE: the summary is a list with indices
|
1014
|
+
summary_index = event.summary_index
|
1015
|
+
delta = event.delta
|
1016
|
+
if delta != "":
|
1017
|
+
summary_index = event.summary_index
|
1018
|
+
# Check if we need to instantiate a fresh new part
|
1019
|
+
# NOTE: we can probably use the part added and part done events, but this is safer
|
1020
|
+
# TODO / FIXME return a SummaryReasoning type
|
1021
|
+
if prev_message_type and prev_message_type != "reasoning_message":
|
1022
|
+
message_index += 1
|
1023
|
+
yield ReasoningMessage(
|
1024
|
+
id=self.letta_message_id,
|
1025
|
+
date=datetime.now(timezone.utc).isoformat(),
|
1026
|
+
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
1027
|
+
source="reasoner_model",
|
1028
|
+
reasoning=delta,
|
1029
|
+
run_id=self.run_id,
|
1030
|
+
step_id=self.step_id,
|
1031
|
+
)
|
1032
|
+
prev_message_type = "reasoning_message"
|
1033
|
+
else:
|
1034
|
+
return
|
1035
|
+
|
1036
|
+
# Reasoning summary streaming
|
1037
|
+
elif isinstance(event, ResponseReasoningSummaryTextDoneEvent):
|
1038
|
+
# NOTE: is this inclusive of the deltas?
|
1039
|
+
# If not, we should add it to the rolling
|
1040
|
+
summary_index = event.summary_index
|
1041
|
+
text = event.text
|
1042
|
+
return
|
1043
|
+
|
1044
|
+
# Reasoning summary streaming
|
1045
|
+
elif isinstance(event, ResponseReasoningSummaryPartDoneEvent):
|
1046
|
+
# NOTE: this one is definitely inclusive, so can skip
|
1047
|
+
summary_index = event.summary_index
|
1048
|
+
# text = event
|
1049
|
+
return
|
1050
|
+
|
1051
|
+
# Assistant message streaming
|
1052
|
+
elif isinstance(event, ResponseContentPartAddedEvent):
|
1053
|
+
part = event.part
|
1054
|
+
if isinstance(part, ResponseOutputText):
|
1055
|
+
# Append to running
|
1056
|
+
return # TODO
|
1057
|
+
else:
|
1058
|
+
# TODO handle
|
1059
|
+
return
|
1060
|
+
|
1061
|
+
# Assistant message streaming
|
1062
|
+
elif isinstance(event, ResponseTextDeltaEvent):
|
1063
|
+
delta = event.delta
|
1064
|
+
if delta != "":
|
1065
|
+
# Append to running
|
1066
|
+
if prev_message_type and prev_message_type != "assistant_message":
|
1067
|
+
message_index += 1
|
1068
|
+
yield AssistantMessage(
|
1069
|
+
id=self.letta_message_id,
|
1070
|
+
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
1071
|
+
date=datetime.now(timezone.utc),
|
1072
|
+
content=delta,
|
1073
|
+
run_id=self.run_id,
|
1074
|
+
step_id=self.step_id,
|
1075
|
+
)
|
1076
|
+
prev_message_type = "assistant_message"
|
1077
|
+
else:
|
1078
|
+
return
|
1079
|
+
|
1080
|
+
# Assistant message streaming
|
1081
|
+
elif isinstance(event, ResponseTextDoneEvent):
|
1082
|
+
# NOTE: inclusive, can skip
|
1083
|
+
text = event.text
|
1084
|
+
return
|
1085
|
+
|
1086
|
+
# Assistant message done
|
1087
|
+
elif isinstance(event, ResponseContentPartDoneEvent):
|
1088
|
+
# NOTE: inclusive, can skip
|
1089
|
+
part = event.part
|
1090
|
+
return
|
1091
|
+
|
1092
|
+
# Function calls
|
1093
|
+
elif isinstance(event, ResponseFunctionCallArgumentsDeltaEvent):
|
1094
|
+
# only includes delta on args
|
1095
|
+
delta = event.delta
|
1096
|
+
|
1097
|
+
if self.tool_call_name and self.tool_call_name in self.requires_approval_tools:
|
1098
|
+
if prev_message_type and prev_message_type != "approval_request_message":
|
1099
|
+
message_index += 1
|
1100
|
+
yield ApprovalRequestMessage(
|
1101
|
+
id=self.letta_message_id,
|
1102
|
+
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
1103
|
+
date=datetime.now(timezone.utc),
|
1104
|
+
tool_call=ToolCallDelta(
|
1105
|
+
name=None,
|
1106
|
+
arguments=delta,
|
1107
|
+
tool_call_id=None,
|
1108
|
+
),
|
1109
|
+
run_id=self.run_id,
|
1110
|
+
step_id=self.step_id,
|
1111
|
+
)
|
1112
|
+
prev_message_type = "approval_request_message"
|
1113
|
+
else:
|
1114
|
+
if prev_message_type and prev_message_type != "tool_call_message":
|
1115
|
+
message_index += 1
|
1116
|
+
yield ToolCallMessage(
|
1117
|
+
id=self.letta_message_id,
|
1118
|
+
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
1119
|
+
date=datetime.now(timezone.utc),
|
1120
|
+
tool_call=ToolCallDelta(
|
1121
|
+
name=None,
|
1122
|
+
arguments=delta,
|
1123
|
+
tool_call_id=None,
|
1124
|
+
),
|
1125
|
+
run_id=self.run_id,
|
1126
|
+
step_id=self.step_id,
|
1127
|
+
)
|
1128
|
+
prev_message_type = "tool_call_message"
|
1129
|
+
|
1130
|
+
# Function calls
|
1131
|
+
elif isinstance(event, ResponseFunctionCallArgumentsDoneEvent):
|
1132
|
+
# NOTE: inclusive
|
1133
|
+
full_args = event.arguments
|
1134
|
+
return
|
1135
|
+
|
1136
|
+
# Generic
|
1137
|
+
elif isinstance(event, ResponseOutputItemDoneEvent):
|
1138
|
+
# Inclusive, so skip
|
1139
|
+
return
|
1140
|
+
|
1141
|
+
# Generic finish
|
1142
|
+
elif isinstance(event, ResponseCompletedEvent):
|
1143
|
+
# NOTE we can "rebuild" the final state of the stream using the values in here, instead of relying on the accumulators
|
1144
|
+
self.final_response = event.response
|
1145
|
+
self.model = event.response.model
|
1146
|
+
self.input_tokens = event.response.usage.input_tokens
|
1147
|
+
self.output_tokens = event.response.usage.output_tokens
|
1148
|
+
self.message_id = event.response.id
|
1149
|
+
return
|
1150
|
+
|
1151
|
+
else:
|
1152
|
+
logger.debug(f"Unhandled event: {event}")
|
1153
|
+
return
|
1154
|
+
|
1155
|
+
|
1156
|
+
"""
|
1157
|
+
ResponseCreatedEvent(response=Response(id='resp_0ad9f0876b2555790068c7b783d17c8192a1a12ecc0b83d381', created_at=1757919107.0, error=None, incomplete_details=None, instructions=None, metadata={}, model='gpt-5-2025-08-07', object='response', output=[], parallel_tool_calls=True, temperature=1.0, tool_choice='auto', tools=[], top_p=1.0, background=False, max_output_tokens=None, max_tool_calls=None, previous_response_id=None, prompt=None, prompt_cache_key=None, reasoning=Reasoning(effort='high', generate_summary=None, summary='detailed'), safety_identifier=None, service_tier='auto', status='in_progress', text=ResponseTextConfig(format=ResponseFormatText(type='text'), verbosity='medium'), top_logprobs=0, truncation='disabled', usage=None, user=None, store=True), sequence_number=0, type='response.created')
|
1158
|
+
ResponseInProgressEvent(response=Response(id='resp_0ad9f0876b2555790068c7b783d17c8192a1a12ecc0b83d381', created_at=1757919107.0, error=None, incomplete_details=None, instructions=None, metadata={}, model='gpt-5-2025-08-07', object='response', output=[], parallel_tool_calls=True, temperature=1.0, tool_choice='auto', tools=[], top_p=1.0, background=False, max_output_tokens=None, max_tool_calls=None, previous_response_id=None, prompt=None, prompt_cache_key=None, reasoning=Reasoning(effort='high', generate_summary=None, summary='detailed'), safety_identifier=None, service_tier='auto', status='in_progress', text=ResponseTextConfig(format=ResponseFormatText(type='text'), verbosity='medium'), top_logprobs=0, truncation='disabled', usage=None, user=None, store=True), sequence_number=1, type='response.in_progress')
|
1159
|
+
ResponseOutputItemAddedEvent(item=ResponseReasoningItem(id='rs_0ad9f0876b2555790068c7b78439888192a40c50a09625bb26', summary=[], type='reasoning', content=None, encrypted_content='gAAAAABox7eEiOVncSJVTjHrczwPKD0bueuhRmgzj6sBTQPnyB5TTE4T3CCoxXALshB1mkOnz48dkd8OkkqFSjZ90OmFi1uVZ9LdJQxoibXj2qUetqhwO_Lm8tcy5Yi4DHrqqhMPbGnDOuJr38PyI_Jx5BDPzJlPbDeU6a99Eg531W7nfSVCzwihekQxlcV9X0xYAvSaigCgbu75sSkx4mopcYDeBTxTjYtpJIAH4C-ygv_MyEeqTJqGdGoQ1NjmF6QJECIXir6llkHlvUHhGeAH6bUabUw7SDBk7gJnMAwDUOZVfp0GyWHRVbDfLCrP7G5nkz98iaEl9LFOcTolsrqxYI_e7k2rIejhfvvSEwgvhCOidNjjKNr3Jujt2ALJ6kGgG3fyWu81cLMobRTL6H0iQ2uT8u9XqZ2eiwHwImexRytC1sSDPK9LaBih46J66HVBKQTeRqMA7m379U8o-qLESN6AiS0PoiJvBpT3F89qJSl3rG19NwzJpPC99Ni1Dzgbr6VPqVmYBqJ5pRt98P-zcW4G72xNr1BLWgCGlCiuuNOxvn2fxPmdHt6S4422oNYb8mNkKeL7p0-6QB9C6L4WPrXUmCOr2_9-dcd1YIplHNQd7BGcbrotZIOj_kTgOvkbQa72ihDV6lNFg8w0_WO2JqubjxP4Ss22-hhtODP6dtuhWjAX5vhIS1j0lFlCRjnQsdC6j7nWhq8ymoPVrmoTE9Ej-evsvTnKO1QVXDKPrKd0y-fMmuvMghHCmhqJ5IiYT1xPX6X83HEXwZs2YY5aHHZkKcbgScAhcv0d1Rv4dp18XHzHUkM=', status=None), output_index=0, sequence_number=2, type='response.output_item.added')
|
1160
|
+
ResponseReasoningSummaryPartAddedEvent(item_id='rs_0ad9f0876b2555790068c7b78439888192a40c50a09625bb26', output_index=0, part=Part(text='', type='summary_text'), sequence_number=3, summary_index=0, type='response.reasoning_summary_part.added')
|
1161
|
+
ResponseReasoningSummaryTextDeltaEvent(delta='**Analy', item_id='rs_0ad9f0876b2555790068c7b78439888192a40c50a09625bb26', output_index=0, sequence_number=4, summary_index=0, type='response.reasoning_summary_text.delta', obfuscation='JdVJEL6G1')
|
1162
|
+
ResponseReasoningSummaryTextDeltaEvent(delta='zing', item_id='rs_0ad9f0876b2555790068c7b78439888192a40c50a09625bb26', output_index=0, sequence_number=5, summary_index=0, type='response.reasoning_summary_text.delta', obfuscation='3g4DefV5mIyG')
|
1163
|
+
ResponseReasoningSummaryTextDeltaEvent(delta=' r', item_id='rs_0ad9f0876b2555790068c7b78439888192a40c50a09625bb26', output_index=0, sequence_number=6, summary_index=0, type='response.reasoning_summary_text.delta', obfuscation='dCErh1m4eFG18w')
|
1164
|
+
ResponseReasoningSummaryTextDeltaEvent(delta=' things', item_id='rs_0ad9f0876b2555790068c7b78439888192a40c50a09625bb26', output_index=0, sequence_number=214, summary_index=1, type='response.reasoning_summary_text.delta', obfuscation='hPD6t2pv9')
|
1165
|
+
ResponseReasoningSummaryTextDeltaEvent(delta='!', item_id='rs_0ad9f0876b2555790068c7b78439888192a40c50a09625bb26', output_index=0, sequence_number=215, summary_index=1, type='response.reasoning_summary_text.delta', obfuscation='g1Sjo96fgHE4LQa')
|
1166
|
+
ResponseReasoningSummaryTextDoneEvent(item_id='rs_0ad9f0876b2555790068c7b78439888192a40c50a09625bb26', output_index=0, sequence_number=216, summary_index=1, text='**Clarifying letter counts**\n\nI realize this task is straightforward: I can provide both answers. If the user is counting uppercase R\'s, the answer would be 0. For a case-insensitive count, it\'s 3. It\'s good to give both for clarity. I should keep it brief; a concise response would be: "If you\'re asking about uppercase \'R\', there are 0. If counting \'r\' regardless of case, there are 3." This way, I cover all bases without overcomplicating things!', type='response.reasoning_summary_text.done')
|
1167
|
+
ResponseReasoningSummaryPartDoneEvent(item_id='rs_0ad9f0876b2555790068c7b78439888192a40c50a09625bb26', output_index=0, part=Part(text='**Clarifying letter counts**\n\nI realize this task is straightforward: I can provide both answers. If the user is counting uppercase R\'s, the answer would be 0. For a case-insensitive count, it\'s 3. It\'s good to give both for clarity. I should keep it brief; a concise response would be: "If you\'re asking about uppercase \'R\', there are 0. If counting \'r\' regardless of case, there are 3." This way, I cover all bases without overcomplicating things!', type='summary_text'), sequence_number=217, summary_index=1, type='response.reasoning_summary_part.done')
|
1168
|
+
ResponseOutputItemDoneEvent(item=ResponseReasoningItem(id='rs_0ad9f0876b2555790068c7b78439888192a40c50a09625bb26', summary=[Summary(text='**Analyzing riddle ambiguity**\n\nI’m thinking about a puzzle that mixes cases to mislead, but the answer should be \'3\'. There are gotcha riddles about counting letters, like asking how many R\'s are in "Strawberry." If it\'s capitalized, the answer differs. The typical trick is that there are no capital R\'s in "strawberry." Since the user asked about uppercase R\'s but quoted the lowercase version, it\'s confusing. I should clarify if they mean uppercase \'R\' or any \'r\'.', type='summary_text'), Summary(text='**Clarifying letter counts**\n\nI realize this task is straightforward: I can provide both answers. If the user is counting uppercase R\'s, the answer would be 0. For a case-insensitive count, it\'s 3. It\'s good to give both for clarity. I should keep it brief; a concise response would be: "If you\'re asking about uppercase \'R\', there are 0. If counting \'r\' regardless of case, there are 3." This way, I cover all bases without overcomplicating things!', type='summary_text')], type='reasoning', content=None, encrypted_content='gAAAAABox7eQs7K1F8qaKB_jhBgufrTLqEXk96f-M9YyeTQ7tvQO730WOtTZtmuQ7XLiAekqxt4yrNQEmgYZhS7qQx-5oq30NlfHezcgkYqmvBqFhGtJkg_Ea6eO9WVMYaXK6nbxXvyK-HS73GvF8AN6NE72rITUE0fdlT6_VeU_OLBSDtVJXUMqbr6V4MOllzXRklbIOJCemZWRax0tenrxaVBrR4IbGoXoFbz5q2Lt8-Xc4NtuUShrzv8AU8Lm46KGvZeX2bWtS0d7-x3in6HJNk4gAFmepYh-cNbk_Qd8UVMvARb2nBjK7jHTB6IP1fDbVYYMUvX6ox8q2jPdHA7ZFRF-YFDXUyX6lwvhLGhVodqyQ4IdmZv1sJ78mvLUpuEdJrHSapA83SN6oaqpoD5cO174UKxyZnrhwQCyxPQ__lS5ZaUgnsIfgtuF5_cKATDxJFBrVo-0SwPHJZdtiCD1CYaVgUKr6uBDtUk32WCDOSJbFK5ClYM1W41x7mBLUWwBJVJ4PZVz3Cc6lR6EMa4a3SAMtIRzMY3869ox6WwDUV8TAYpSMdsb_VW3aezj0hXhnGYUrfmrmtYJEmxy36kV9GsHoBSLwXwNYbjTnP-Pni_AqlCQgZWKTI9KzJ8Zi95l617XwDJ6PzaHt2D6OSX2pmiVPwMGjZDIR6o21fBw3ZwI9TGkJitwL5O9Xlc6PQfYnk-oAVt17OZet6tXQe8LA3wq-9BQXY-88OQRrIGsnFjuGKOmaEXXDmaT1u9lGwOfSdKtU-4X67iDmy5e--lKYZrbWEVy2aoMcwMh2gTsPl-nS_fLzPdOlIgXv4DKCFf_E93LjjdyVoSFctm928rqY_qayqvP5kGx4UjPGiIxFD7tI3lEGMMFA8P0h6nE6NnZgb7pgMtgsqF17SdBKAXFLF8JtuaZulzoBdJJ_2Skq0FO7X8xynq_hhIDdwK2QU9PEfaX7h0j-kGYwVuWs8C_zispG--pHveDqPE1j9GUVrRN9W72-qNHnXRMPEDan1jq4WFN4VknDVwbnK9HR_suKJOTKGZF0MJACtaL4_FyvGfqANLky3cfWeMLpYmXec2Buo-4x8XwlRASCyvK6KXnz7K-M0SuvtoEqTBw0Pa4PBO683OtssZ-ujqMgnzFy3tTKpAabGq-Tz3Dn5fxbYgZONpE6jdTEQxBhkkvplReda3GATlskQHrQtn5Q_tvYwOIQu3iFiP9uoTtfCVQ_Tm4CIGxcEDqWnVaP1fOe8LKHwCvPf7bm046YI3oL-2do70oBJEch0JRKiI3ijrqHzXpI6e-bam9inNnzKxq0HMornRJh37HMDtME0nbXvrNSTu7k7pldDJtQ7SVIoey_PnLirAL9WdfM0HTdsAVmHgXp8u6Ta3_aob-vdrYs19TnGAh6Hp5DqC47wrDeg4RqqSWTM5PLdj4kfdmzkBB90zLMTdR_7Xq7ox64NXfaOXkyLSdFNgz3vmMGyyI3RDeDfVN8tLWfmAWKnooXp866vmdkdWp2IGiq8VWFOe20oaugm8CtT54XLlL6Hh_nipMZy_4pLTVZSsSNd1lvUn-xPMu7WD3NMEdk5b61juYsa77CLHj71vzbPVfHhmOtxqQ_Iqeh4sgPhY0FKRhblvs6yIXy__Ab9MKMYz1Cba1qAr-m9_JGNR1PzUPb7CfS-gbwBwqGoNy5ig1ir-GccsA9hB0UPORaobOGkklDI6B-aEjf8DkzEGdzXQLpJkWwv4cjJAeU1oA9R0hNAwR_STZDvmjkos7j0opRUl-qOez4pBeoRcR8T6V3uqO6OD4j0WNMkLekAGi_BE9tt25v2ClWVSeBE7M9TjlG4uOJwp_IHJZRM3VwunJt9L6ZXALck5sEdIG1EYiAgSophCMqfqUUS2cG7QkDOH-N_jGQisoKRqWJKouERgIHT9TK5ZDeL3WVQL3a-6-HH3y-Lv8UJC1-F_V15FZTgAK3SxUqeHHts6EvDKEqde9QxxTPWwhMOk6dBxNQ0jxfKn9pzNNXhasVIHnk5zn1wWJkm8P3B5sG6Oxwpsxu6ywbY4AOFjBRwHGnnO06CykNaB6uR3KxIlDo2pdidOChI1uZrqYAEDKhjGHcKUQOlgq83wz4dLciiioDYPHfexfSl91QQaQZWrAIN77AbT6e9wxXaZZNQ4Jwo9JpQNjRkoBu2_4tW317nzLj31ayK-5w07imhOBh3ziD8yx3MC7AxuIbsAWo_scZgq8h7OxwRBih9NyiYMePLTLPOPahjDQvl-4XFj4NVNNnXKsiLrxPwtxmMREZraJxcmrSzDFiYDnqkibHXQ3eYyykcjCY3kWRCszoAEYhI3a2qsfbyePgPlfynf3_8rCsb2qaiXmu93lLqrRRg0ktRXtBb3lJVlpVGezUD6Itc_BDZQJAfC0PJbf_AoLfxIVw9-Pj5p5ssxuyybJn0thiqR5CnzcK_TO4jA2PJkjdfK5zLZbyNSYp4NKUpaL1u0jxiuD_vJ30qt3hJugsTv8EvCLdtoNwuvBwjhqplPPZ8_TWCVsowYm3n9LEYWCK-EEk6D2H4_Z8gQYNWz0O735CSiAVpSZpChBRwkfOhlerp8o6k8NJmf7VEqVCE5_iwrKqllB0o8hNLPDSlzQ97EacKz6wsLBorlqTRvGvRrJqwQHwybQLkJlCinqZV9XF52kc0c9GqdKdF-aPxv5VNoPenEBDo6EpAnDyM-TRxzsWtQ71kzRQgLIi-tvO9fTA2MExrF4tv_m1CULjF2jIoeG8RZPC4zhHVd9lvyflhCVSLflF6GR2qzSQua2zqqMsfM4qYGdW83in2U5KDWc7yD7FVi_IM5F1_AKeUaPQ_9MbwCkUO8zdDSQ-eVxY051PGiKHNKTP982Legft29skJqqDZv57Oju9wtI9PmmoeozaBPv4-spuuczsMsVbl6aRLs8xQsPQoke-MUMuelF1kGIqJnMktKiN8AGB8CoU_XzBjGSV-8yJj7tCBYquF66tj5wyn5tsVWwHsi8sl-IRMrVsza1LY0mVx-6ljo97j3WME1LuCTTNF5GOZMHUfRUXgHW5aENuENS9LhsqymVK8sAeQVMVVijC1Gnq2I0ddKLwodsrzCReaqLKx4y3Q4NB0Rom76UzyODd3vzDxjUS9k-IvRbzyXYC0YO-WsngpJr8sKZ8eQqJuBSE3rjT6CEx6-Ldxf8ad-iT6rh-nJRMn27jtHaUgQdZoexMDS1yons8r-MfUYayTaAGeIiimpuCj1A-f3zpQgqqehRkxoEJmjcGLe0oRI5H-kXEk8_LZt45nCiD86HnSCBqRasFNV0lAhWy2UF2cuu0AQuixUDRRgJU5ilWuDTcnJAo-Y4T7wh06xUGuCa50mLAszVnldO-JFrYGYE5UsWTe7qSNOSNsLIJqVoR4WLJJp-FaDFpiir14v1llvh1OumR03aDCA4gOQzeFNzfkUIQNRq0sU1ReZcxLUnlNjHWFqSBfB53rerSV8mdauA91EweO3cOJ1iTUFnAST_QPB2da03hINiRWd8jSkkiUdha-t6iajgOA2w_YlP2cyZ2b-L-cVhBFx0r1VSHocASSSTK1vU1vrPwtXJdMHq6c_EcMSirybtLzpIM3WR-z1wbr2gYvPF2KR_DvsybXE3DsX4qKInsykvBLmg-0RYWcFivmBgAGcIgYLjuCaWbpi5wYi_hNbPBJw07WpxN4QOS9_CaOn0AQh0NnqgPg9DH_am9mpOutvWMKWOqMcNKaRACDCpQkGDhX8yfF6W4EihLKam0vmiYYYtnFQ19Xl59cXf8gVcbNOnElOuA3gK_4PMCYHL66tPUdhKreBlboULKLm0xgYMf3lRrPh803TG0x5L0oYAGzXcGUZIs0AtX2wmkfYSSivFsqSThLY-q2VHtiNBigEZRIWr1lfNzLFYzNipiajvFAfB1EpDpfRkjnnoV5n656y11uFcyySyiskKxZqZryqfb3HPfn8VlK3baKLMk5a0i1CZp5LswGErlk2qgwaSYSWOcHmt6z1GfJOKzrGkHFTWMzzg', status=None), output_index=0, sequence_number=218, type='response.output_item.done')
|
1169
|
+
ResponseOutputItemAddedEvent(item=ResponseOutputMessage(id='msg_0ad9f0876b2555790068c7b790e5388192aa7d4d442882790a', content=[], role='assistant', status='in_progress', type='message'), output_index=1, sequence_number=219, type='response.output_item.added')
|
1170
|
+
ResponseContentPartAddedEvent(content_index=0, item_id='msg_0ad9f0876b2555790068c7b790e5388192aa7d4d442882790a', output_index=1, part=ResponseOutputText(annotations=[], text='', type='output_text', logprobs=[]), sequence_number=220, type='response.content_part.added')
|
1171
|
+
ResponseTextDeltaEvent(content_index=0, delta='Upper', item_id='msg_0ad9f0876b2555790068c7b790e5388192aa7d4d442882790a', logprobs=[], output_index=1, sequence_number=221, type='response.output_text.delta', obfuscation='a8XGRatycGS')
|
1172
|
+
esponseTextDeltaEvent(content_index=0, delta=' ', item_id='msg_0ad9f0876b2555790068c7b790e5388192aa7d4d442882790a', logprobs=[], output_index=1, sequence_number=234, type='response.output_text.delta', obfuscation='Ljhu9qR46fiOkfr')
|
1173
|
+
...
|
1174
|
+
ResponseTextDeltaEvent(content_index=0, delta='3', item_id='msg_0ad9f0876b2555790068c7b790e5388192aa7d4d442882790a', logprobs=[], output_index=1, sequence_number=235, type='response.output_text.delta', obfuscation='5auIEi4JmSFDF72')
|
1175
|
+
ResponseTextDeltaEvent(content_index=0, delta='.', item_id='msg_0ad9f0876b2555790068c7b790e5388192aa7d4d442882790a', logprobs=[], output_index=1, sequence_number=236, type='response.output_text.delta', obfuscation='I78DIGKqtD2P6H2')
|
1176
|
+
ResponseTextDoneEvent(content_index=0, item_id='msg_0ad9f0876b2555790068c7b790e5388192aa7d4d442882790a', logprobs=[], output_index=1, sequence_number=237, text='Uppercase R: 0. Counting r regardless of case: 3.', type='response.output_text.done')
|
1177
|
+
ResponseContentPartDoneEvent(content_index=0, item_id='msg_0ad9f0876b2555790068c7b790e5388192aa7d4d442882790a', output_index=1, part=ResponseOutputText(annotations=[], text='Uppercase R: 0. Counting r regardless of case: 3.', type='output_text', logprobs=[]), sequence_number=238, type='response.content_part.done')
|
1178
|
+
ResponseOutputItemDoneEvent(item=ResponseOutputMessage(id='msg_0ad9f0876b2555790068c7b790e5388192aa7d4d442882790a', content=[ResponseOutputText(annotations=[], text='Uppercase R: 0. Counting r regardless of case: 3.', type='output_text', logprobs=[])], role='assistant', status='completed', type='message'), output_index=1, sequence_number=239, type='response.output_item.done')
|
1179
|
+
ResponseCompletedEvent(response=Response(id='resp_0ad9f0876b2555790068c7b783d17c8192a1a12ecc0b83d381', created_at=1757919107.0, error=None, incomplete_details=None, instructions=None, metadata={}, model='gpt-5-2025-08-07', object='response', output=[ResponseReasoningItem(id='rs_0ad9f0876b2555790068c7b78439888192a40c50a09625bb26', summary=[Summary(text='**Analyzing riddle ambiguity**\n\nI’m thinking about a puzzle that mixes cases to mislead, but the answer should be \'3\'. There are gotcha riddles about counting letters, like asking how many R\'s are in "Strawberry." If it\'s capitalized, the answer differs. The typical trick is that there are no capital R\'s in "strawberry." Since the user asked about uppercase R\'s but quoted the lowercase version, it\'s confusing. I should clarify if they mean uppercase \'R\' or any \'r\'.', type='summary_text'), Summary(text='**Clarifying letter counts**\n\nI realize this task is straightforward: I can provide both answers. If the user is counting uppercase R\'s, the answer would be 0. For a case-insensitive count, it\'s 3. It\'s good to give both for clarity. I should keep it brief; a concise response would be: "If you\'re asking about uppercase \'R\', there are 0. If counting \'r\' regardless of case, there are 3." This way, I cover all bases without overcomplicating things!', type='summary_text')], type='reasoning', content=None, encrypted_content='gAAAAABox7eRIRNnSmrunATD6UBi-Hm77E5JggsaXTKrNH-6ZkwIcosPQPf4vVdjR3ywdcYr4pr2Od3C0ADYSUpyR35tyusZq8A8yR-EmpgA-7otyIGLk5zzZy3AqKv2zZElkvgcr8PEKpYpC8VS6AO4Qg3g_gvBD8eV8j2O_FtGTIQ5MKS_Q0_gf9BCJtkh-PgYjL-0bEXsmCfgPa37BogC4nYh42b5hc7vge3ZH_RmR3irxWontsGaUIkOxR8_oK3RGKvkLfR24QYd4U8BIiZk3G58cR1UDRmtvfHwM4E7W6mpog-dFe9D-V96q1OWBGsNObyHxJcoSNGLxHkxWRvGnq3aWts_Lh-srgJ50rIa19pnOzfXePfdNxdXy7dYXD0D1uiBibpX5nneKUr1C0QmQdwS_nW16pr1oNKZ2fVkZJDTn31rOR3WfvtY9gL7tKo_CMnJ8jT3YKhZxFHG9PhHEoA5OsE_QC-3To54meckPExJqrVJ-h3u_5S4lHK9xu8buzIv4WM92X91zeX98A3g_YkqqvoTUmkFyMoIr8PVxM6Cmg4JtooT9bL2FAVUo6MV2_tlX07hNNH-hWSgqZHMVdx3_cTDAfKW3cAbwaG16ApgK_VUUc7rIfygHxgxtW-YeZpbETlvdNrDIDhzhuPQqPB86DQFh9O262o3cvBHok7V0WVqq-KXH5mH-eio7MhZJ46Ri4qklU9Xn77Tw4zl1cw029FuDKwF0_KsFZ8Omayi5iWJoZFjzqhATR_qt2J3nr368skIHDQ1tSa1vUAJt4UM7A4Un9KG2syCydoAmVQYoRgc5niiWU9FFouzulKW_cfyLrJDlVN1EfaUx2xVzaJO-LhdimhDiP4CKk5DsvEuuhTDn9RkO19cz7eJdrt_wGthYRlcJ-5bSFsSG1UV4VlovcLjuqApc5Fsis9kRo0jkar53HM7rmI7t9uN3TcTCQWGpbDvi-OQblbdvNFZh8wy-BaC0SFtOwcVkhwR2CDCf-7FuB5HOJnzmSOtKDZoFrA9gspNZjXoV6LCKmKIGj_tRLaI9jsn9iZZ7Bdtv2SLw7blE53f4OesXbsC0evl9GzlJfIsiaO1I5pEGCT2sWitWyHrbQLJTWeUBi6SoeULpujVp_w25xJonbCD9HAV51bD6rmAI9LEj0bYOBJ1RmtESAqZpV2wj68i-tv5ejdQ-YXOXSuy4DwInYsALmGMRhFFf0tKhNLHMVdCOij0zo4fU24EhmfxMRZifapm4fDBe2bswE10_LJI2DhzLv_NwQfHMQ0qEDOZQss74qaggBnsr4N-OK6egO3RJYCFddDFUa9vwxYIBHjlqb2p7tX4YpugHQ0ZmDYpUAwRzUmcwYaLjs9lzskQzzpOCeKXmwksWWOax-aWkkw9ic17PTAqne84_LMSNnY4mPOYU4sQ0DxdfNX_2iGVrWSkP3XcLUut1OH6Mah-yWaioJbLoXIpxbngW-IAm3Uxafha94fOHSaMymRYG8ZIbKHvg6n3tud08gBfiiJON5CLCovoKkAeGC3-NQQC58341osMVKSRF6SEpsHMGd97lMdWTlkB3v29m-xf8nuCOqgk4Ig5gIodter_BWs2BEXLiw5ISDBvdl26FVUUoBOpexXFwf99wTroDPK85UYlH5W9m51FlSwfgm0Vg5N9nzivMGClDy_jvNDyI5UHnQjVuqnTAcK0nF6RJn-lnO6hT60qq9hRsqa84iUMqOmQZxXv1KbS0exoqfrqps6ILqifM93r87HVzrrCShWFB1A7hfJHoVqRq-PLsO2iv-V6v9S5_nFwYGG8srrNUuNgzLvLB9J7hlN6fPL4f6vWIJ9sBVpPukR-Pr_I8q8hZicr3YVshIuB544w-srUH4OvRx5v5pz9Jfcm2hHZZjO2yaVDAWKQ_PQk0xj43b-pyrjpAznYdG1QMmFcTfjqVDal97EQeMjIAIjlah0BOqhzHtT0dBjYLyBXZwzO7ii7z-6-jQk5FIDX-RqrdHm8D41dTHx-W7LMvNwpW6ueir7HVoYdIZAP0qaSU-Nf3oJTK8wGhRsh4G3PBsrGbamsfK7c2-AYi_f6kcvXWE4G7ch6c5H6cVqrriil8AcjUZ422dIkFIJHfhbPeAIFy6zuDm9ZnZvgjyqI_mnnK0hlLzfSgJFV6QRAYdkmiviit4qIwEOobM6zYeYPdb09Y15MDLcCOM1KpCecaSJDZm4PrnvP3F0nUpYHvVygA1C-CPenmCjeC_AqWMJ_BVXQyIcVx31fxZCvBIkEskI9Wm6qfJkN8IYw00_X4PnpV-u9d6poChA2smfOsFaeHqoNE_RmPO_QTqHE4m6xBH2RueVnIt4QZ2NVOFyZUI4vBEOsNOXYQpw8tkzONR3FcRRsp2qWNXfmTVdrkVR_-oQpUSlkhQKKo9thNq6SpDezaMUpWjMpi1lgaIZUbSU0WUq3A2EtpWW1yJQjuA2rosQYh2zgILAEtyYgu0Qh5qqsKQB_7oyv3LOB5JHVYa94H1xqHwk9XVfOM6Eeszb1-FYZ3ibagpOiIzPPrGhZA1FIfdVDLk3ulDR7l3-NZZD48SkbkxlnJqbjksgtoM-0AAPVV7q4OSH9MBHK29yVJRahzoFei9toYhD2qN3Mo-HVbWPOo89wJ8LKnwTTF02RUcA4xstjuD5B4IEGF2fMprohnlYVpULejRkkga3Mt6wdjLHzJY4WHkSaGfrDChgMfRpAhtPYQ4sSf4FVFaeT6up-1pU3o-n56zibIwHDfmB_rXEXHLIpaUBEDo7-X8TXZ8SvS-isKwmExJxDtjUI_pglFcThIfigVOJvyemEQ11iLmcoIw6vj5Zge3xzxR7pJgiHbGXhbUpYIJyvrol7NIBZwW_AhgE0WJEjrq9ffdoE9OB311ZZbES2q-ghlfGKgyFrrZNgpY_mYCjd8yx5APWvBYoj-w1WxL42Q0bE3DSyBM9JOwb8t1SPNNduz01MVsbj6_zbya7KDW4pHGiU-4Dh-YU8q9ndeuIezb7km6vQn6zjOfLLPXSkIH99RgAn-eNMPdk5CZWXm16nqgpL1ZtxivXhPItlq2p5akhj64_nreXLe2bKscR7syMZ_9xRC1u9EdomxyuJx6HAB-Jo7_AatJcYeI0BNLiGjnflLnbqwP0jH9_6Q2ucC9oNoNNtiyzq-Wy7zW9Q9eDCL8zVfKVAwNkyvzKSra8EJ6u-ukskCAXmN09_WUXQC00H7foIKOhhn4LXT9LoVgFblMsVjm_bBzXQuEA11Bc3RAJHUyLlpH9K-vz1Zebn-1AUDSlEQENIkzW6TpnoumA1m728tvaF8byNOqKgfRdftIixRHmKYUPrgKXrJErEz6P_n2MJvOvvCVH03o_Dpoh19PY6Rcvv1t56SaUCzdEyTcsVP9JRNh26HckesWjb2IfJsDuGrjlX5V5FabPImAKVRGzwNW5lJLwB59OBGkS4xXxI_vwzFiwrP6Pb2DPVgw3-Epe017D0atbZVs6Oik-14Q9uLrxBz4X2EV0HK_nnkg3mndj2LDBEXtCFky6sIrWer4W3i4Ksrfe5oxGiV02tjNNzFSqHg_z9QX43kTbcBePuYDlMRJ2DwmBykJUXdLcT4j9FlQ9BwOSAKHNaE35j-YZkASDYKqRn5SL9zC71C2qyJVDQ-5cw9GRaFZfLDKO6ySv7yZb367UpQ1uUUzqsyivAYA8jqez7LV0Yxz_hq5mBKE-NdHf-EU9uHHg1zkB73pk1wFOqE5siD0fjr7IkU3R3OcTsNSXEMa63jfeiODcSEoKwcOB8gxG-3Xwh1ueQO6sGvP7Z6sWBfPeWlmA662QytXV7njzFerjuXVRLbCfUg1v26xoPdh2jCKN_GZXroctfpV5LuOGfXd6xjgEpDq4CxNLFmNfVAZBKMQ-Fxk_szAtGpOB3lPcJTdy73VelN_L-adhUGmJmETqqK77CFTYze80l1c_lzWn6zNvS6T5HmLaNFdf5m-Rl_DSEijvJiqZrkY-Ff_R3FthqM4NZDrxwkkX99uXbkEqXjReJ', status=None), ResponseOutputMessage(id='msg_0ad9f0876b2555790068c7b790e5388192aa7d4d442882790a', content=[ResponseOutputText(annotations=[], text='Uppercase R: 0. Counting r regardless of case: 3.', type='output_text', logprobs=[])], role='assistant', status='completed', type='message')], parallel_tool_calls=True, temperature=1.0, tool_choice='auto', tools=[], top_p=1.0, background=False, max_output_tokens=None, max_tool_calls=None, previous_response_id=None, prompt=None, prompt_cache_key=None, reasoning=Reasoning(effort='high', generate_summary=None, summary='detailed'), safety_identifier=None, service_tier='default', status='completed', text=ResponseTextConfig(format=ResponseFormatText(type='text'), verbosity='medium'), top_logprobs=0, truncation='disabled', usage=ResponseUsage(input_tokens=19, input_tokens_details=InputTokensDetails(cached_tokens=0), output_tokens=598, output_tokens_details=OutputTokensDetails(reasoning_tokens=576), total_tokens=617), user=None, store=True), sequence_number=240, type='response.completed')
|
1180
|
+
"""
|