letta-nightly 0.12.0.dev20251009203644__py3-none-any.whl → 0.12.1.dev20251010012355__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/__init__.py CHANGED
@@ -5,7 +5,7 @@ try:
5
5
  __version__ = version("letta")
6
6
  except PackageNotFoundError:
7
7
  # Fallback for development installations
8
- __version__ = "0.12.0"
8
+ __version__ = "0.12.1"
9
9
 
10
10
  if os.environ.get("LETTA_VERSION"):
11
11
  __version__ = os.environ["LETTA_VERSION"]
@@ -2,7 +2,7 @@ from typing import AsyncGenerator, List
2
2
 
3
3
  from letta.adapters.letta_llm_stream_adapter import LettaLLMStreamAdapter
4
4
  from letta.helpers.datetime_helpers import get_utc_timestamp_ns
5
- from letta.interfaces.anthropic_streaming_interface import SimpleAnthropicStreamingInterface
5
+ from letta.interfaces.anthropic_parallel_tool_call_streaming_interface import SimpleAnthropicStreamingInterface
6
6
  from letta.interfaces.gemini_streaming_interface import SimpleGeminiStreamingInterface
7
7
  from letta.interfaces.openai_streaming_interface import SimpleOpenAIResponsesStreamingInterface, SimpleOpenAIStreamingInterface
8
8
  from letta.schemas.enums import ProviderType
@@ -99,16 +99,16 @@ class LettaAgentV2(BaseAgentV2):
99
99
  self.step_manager = StepManager()
100
100
  self.telemetry_manager = TelemetryManager()
101
101
 
102
- # TODO: Expand to more
103
- if summarizer_settings.enable_summarization and model_settings.openai_api_key:
104
- self.summarization_agent = EphemeralSummaryAgent(
105
- target_block_label="conversation_summary",
106
- agent_id=self.agent_state.id,
107
- block_manager=self.block_manager,
108
- message_manager=self.message_manager,
109
- agent_manager=self.agent_manager,
110
- actor=self.actor,
111
- )
102
+ ## TODO: Expand to more
103
+ # if summarizer_settings.enable_summarization and model_settings.openai_api_key:
104
+ # self.summarization_agent = EphemeralSummaryAgent(
105
+ # target_block_label="conversation_summary",
106
+ # agent_id=self.agent_state.id,
107
+ # block_manager=self.block_manager,
108
+ # message_manager=self.message_manager,
109
+ # agent_manager=self.agent_manager,
110
+ # actor=self.actor,
111
+ # )
112
112
 
113
113
  # Initialize summarizer for context window management
114
114
  self.summarizer = Summarizer(
@@ -117,7 +117,7 @@ class LettaAgentV2(BaseAgentV2):
117
117
  if self.agent_state.agent_type == AgentType.voice_convo_agent
118
118
  else summarizer_settings.mode
119
119
  ),
120
- summarizer_agent=self.summarization_agent,
120
+ summarizer_agent=None, # self.summarization_agent,
121
121
  message_buffer_limit=summarizer_settings.message_buffer_limit,
122
122
  message_buffer_min=summarizer_settings.message_buffer_min,
123
123
  partial_evict_summarizer_percentage=summarizer_settings.partial_evict_summarizer_percentage,
@@ -0,0 +1,487 @@
1
+ import asyncio
2
+ import json
3
+ from collections.abc import AsyncGenerator
4
+ from datetime import datetime, timezone
5
+ from enum import Enum
6
+ from typing import Optional
7
+
8
+ from anthropic import AsyncStream
9
+ from anthropic.types.beta import (
10
+ BetaInputJSONDelta,
11
+ BetaRawContentBlockDeltaEvent,
12
+ BetaRawContentBlockStartEvent,
13
+ BetaRawContentBlockStopEvent,
14
+ BetaRawMessageDeltaEvent,
15
+ BetaRawMessageStartEvent,
16
+ BetaRawMessageStopEvent,
17
+ BetaRawMessageStreamEvent,
18
+ BetaRedactedThinkingBlock,
19
+ BetaSignatureDelta,
20
+ BetaTextBlock,
21
+ BetaTextDelta,
22
+ BetaThinkingBlock,
23
+ BetaThinkingDelta,
24
+ BetaToolUseBlock,
25
+ )
26
+
27
+ from letta.log import get_logger
28
+ from letta.schemas.letta_message import (
29
+ ApprovalRequestMessage,
30
+ AssistantMessage,
31
+ HiddenReasoningMessage,
32
+ LettaMessage,
33
+ ReasoningMessage,
34
+ ToolCallDelta,
35
+ ToolCallMessage,
36
+ )
37
+ from letta.schemas.letta_message_content import ReasoningContent, RedactedReasoningContent, TextContent
38
+ from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
39
+ from letta.schemas.message import Message
40
+ from letta.schemas.openai.chat_completion_response import FunctionCall, ToolCall
41
+ from letta.server.rest_api.json_parser import JSONParser, PydanticJSONParser
42
+
43
+ logger = get_logger(__name__)
44
+
45
+
46
+ # TODO: These modes aren't used right now - but can be useful we do multiple sequential tool calling within one Claude message
47
+ class EventMode(Enum):
48
+ TEXT = "TEXT"
49
+ TOOL_USE = "TOOL_USE"
50
+ THINKING = "THINKING"
51
+ REDACTED_THINKING = "REDACTED_THINKING"
52
+
53
+
54
+ # TODO: There's a duplicate version of this in anthropic_streaming_interface
55
+ class SimpleAnthropicStreamingInterface:
56
+ """
57
+ A simpler version of AnthropicStreamingInterface focused on streaming assistant text and
58
+ tool call deltas. Updated to support parallel tool calling by collecting completed
59
+ ToolUse blocks (from content_block stop events) and exposing all finalized tool calls
60
+ via get_tool_call_objects().
61
+
62
+ Notes:
63
+ - We keep emitting the stream (text and tool-call deltas) as before for latency.
64
+ - We no longer rely on accumulating partial JSON to build the final tool call; instead
65
+ we read the finalized ToolUse input from the stop event and store it.
66
+ - Multiple tool calls within a single message (parallel tool use) are collected and
67
+ can be returned to the agent as a list.
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ requires_approval_tools: list = [],
73
+ run_id: str | None = None,
74
+ step_id: str | None = None,
75
+ ):
76
+ self.json_parser: JSONParser = PydanticJSONParser()
77
+ self.run_id = run_id
78
+ self.step_id = step_id
79
+
80
+ # Premake IDs for database writes
81
+ self.letta_message_id = Message.generate_id()
82
+
83
+ self.anthropic_mode = None
84
+ self.message_id = None
85
+ self.accumulated_inner_thoughts = []
86
+ self.tool_call_id = None
87
+ self.tool_call_name = None
88
+ self.accumulated_tool_call_args = ""
89
+ self.previous_parse = {}
90
+
91
+ # usage trackers
92
+ self.input_tokens = 0
93
+ self.output_tokens = 0
94
+ self.model = None
95
+
96
+ # reasoning object trackers
97
+ self.reasoning_messages = []
98
+
99
+ # assistant object trackers
100
+ self.assistant_messages: list[AssistantMessage] = []
101
+
102
+ # Buffer to hold tool call messages until inner thoughts are complete
103
+ self.tool_call_buffer = []
104
+ self.inner_thoughts_complete = False
105
+
106
+ # Buffer to handle partial XML tags across chunks
107
+ self.partial_tag_buffer = ""
108
+
109
+ self.requires_approval_tools = requires_approval_tools
110
+ # Collected finalized tool calls (supports parallel tool use)
111
+ self.collected_tool_calls: list[ToolCall] = []
112
+ # Track active tool_use blocks by stream index for parallel tool calling
113
+ # { index: {"id": str, "name": str, "args": str} }
114
+ self.active_tool_uses: dict[int, dict[str, str]] = {}
115
+ # Maintain start order and indexed collection for stable ordering
116
+ self._tool_use_start_order: list[int] = []
117
+ self._collected_indexed: list[tuple[int, ToolCall]] = []
118
+
119
+ def get_tool_call_objects(self) -> list[ToolCall]:
120
+ """Return all finalized tool calls collected during this message (parallel supported)."""
121
+ # Prefer indexed ordering if available
122
+ if self._collected_indexed:
123
+ return [
124
+ call
125
+ for _, call in sorted(
126
+ self._collected_indexed,
127
+ key=lambda x: self._tool_use_start_order.index(x[0]) if x[0] in self._tool_use_start_order else x[0],
128
+ )
129
+ ]
130
+ return self.collected_tool_calls
131
+
132
+ # This exists for legacy compatibility
133
+ def get_tool_call_object(self) -> Optional[ToolCall]:
134
+ tool_calls = self.get_tool_call_objects()
135
+ if tool_calls:
136
+ return tool_calls[0]
137
+ return None
138
+
139
+ def get_reasoning_content(self) -> list[TextContent | ReasoningContent | RedactedReasoningContent]:
140
+ def _process_group(
141
+ group: list[ReasoningMessage | HiddenReasoningMessage | AssistantMessage],
142
+ group_type: str,
143
+ ) -> TextContent | ReasoningContent | RedactedReasoningContent:
144
+ if group_type == "reasoning":
145
+ reasoning_text = "".join(chunk.reasoning for chunk in group).strip()
146
+ is_native = any(chunk.source == "reasoner_model" for chunk in group)
147
+ signature = next((chunk.signature for chunk in group if chunk.signature is not None), None)
148
+ if is_native:
149
+ return ReasoningContent(is_native=is_native, reasoning=reasoning_text, signature=signature)
150
+ else:
151
+ return TextContent(text=reasoning_text)
152
+ elif group_type == "redacted":
153
+ redacted_text = "".join(chunk.hidden_reasoning for chunk in group if chunk.hidden_reasoning is not None)
154
+ return RedactedReasoningContent(data=redacted_text)
155
+ elif group_type == "text":
156
+ concat = ""
157
+ for chunk in group:
158
+ if isinstance(chunk.content, list):
159
+ concat += "".join([c.text for c in chunk.content])
160
+ else:
161
+ concat += chunk.content
162
+ return TextContent(text=concat)
163
+ else:
164
+ raise ValueError("Unexpected group type")
165
+
166
+ merged = []
167
+ current_group = []
168
+ current_group_type = None # "reasoning" or "redacted"
169
+
170
+ for msg in self.reasoning_messages:
171
+ # Determine the type of the current message
172
+ if isinstance(msg, HiddenReasoningMessage):
173
+ msg_type = "redacted"
174
+ elif isinstance(msg, ReasoningMessage):
175
+ msg_type = "reasoning"
176
+ elif isinstance(msg, AssistantMessage):
177
+ msg_type = "text"
178
+ else:
179
+ raise ValueError("Unexpected message type")
180
+
181
+ # Initialize group type if not set
182
+ if current_group_type is None:
183
+ current_group_type = msg_type
184
+
185
+ # If the type changes, process the current group
186
+ if msg_type != current_group_type:
187
+ merged.append(_process_group(current_group, current_group_type))
188
+ current_group = []
189
+ current_group_type = msg_type
190
+
191
+ current_group.append(msg)
192
+
193
+ # Process the final group, if any.
194
+ if current_group:
195
+ merged.append(_process_group(current_group, current_group_type))
196
+
197
+ return merged
198
+
199
+ def get_content(self) -> list[TextContent | ReasoningContent | RedactedReasoningContent]:
200
+ return self.get_reasoning_content()
201
+
202
+ async def process(
203
+ self,
204
+ stream: AsyncStream[BetaRawMessageStreamEvent],
205
+ ttft_span: Optional["Span"] = None,
206
+ ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]:
207
+ prev_message_type = None
208
+ message_index = 0
209
+ event = None
210
+ try:
211
+ async with stream:
212
+ async for event in stream:
213
+ try:
214
+ async for message in self._process_event(event, ttft_span, prev_message_type, message_index):
215
+ new_message_type = message.message_type
216
+ if new_message_type != prev_message_type:
217
+ if prev_message_type != None:
218
+ message_index += 1
219
+ prev_message_type = new_message_type
220
+ # print(f"Yielding message: {message}")
221
+ yield message
222
+ except asyncio.CancelledError as e:
223
+ import traceback
224
+
225
+ logger.info("Cancelled stream attempt but overriding %s: %s", e, traceback.format_exc())
226
+ async for message in self._process_event(event, ttft_span, prev_message_type, message_index):
227
+ new_message_type = message.message_type
228
+ if new_message_type != prev_message_type:
229
+ if prev_message_type != None:
230
+ message_index += 1
231
+ prev_message_type = new_message_type
232
+ yield message
233
+
234
+ # Don't raise the exception here
235
+ continue
236
+
237
+ except Exception as e:
238
+ import traceback
239
+
240
+ logger.error("Error processing stream: %s\n%s", e, traceback.format_exc())
241
+ if ttft_span:
242
+ ttft_span.add_event(
243
+ name="stop_reason",
244
+ attributes={"stop_reason": StopReasonType.error.value, "error": str(e), "stacktrace": traceback.format_exc()},
245
+ )
246
+ yield LettaStopReason(stop_reason=StopReasonType.error)
247
+ raise e
248
+ finally:
249
+ logger.info("AnthropicStreamingInterface: Stream processing complete.")
250
+
251
+ async def _process_event(
252
+ self,
253
+ event: BetaRawMessageStreamEvent,
254
+ ttft_span: Optional["Span"] = None,
255
+ prev_message_type: Optional[str] = None,
256
+ message_index: int = 0,
257
+ ) -> AsyncGenerator[LettaMessage | LettaStopReason, None]:
258
+ """Process a single event from the Anthropic stream and yield any resulting messages.
259
+
260
+ Args:
261
+ event: The event to process
262
+
263
+ Yields:
264
+ Messages generated from processing this event
265
+ """
266
+ if isinstance(event, BetaRawContentBlockStartEvent):
267
+ content = event.content_block
268
+
269
+ if isinstance(content, BetaTextBlock):
270
+ self.anthropic_mode = EventMode.TEXT
271
+ # TODO: Can capture citations, etc.
272
+
273
+ elif isinstance(content, BetaToolUseBlock):
274
+ # New tool_use block started at this index
275
+ self.anthropic_mode = EventMode.TOOL_USE
276
+ self.active_tool_uses[event.index] = {"id": content.id, "name": content.name, "args": ""}
277
+ if event.index not in self._tool_use_start_order:
278
+ self._tool_use_start_order.append(event.index)
279
+
280
+ # Emit an initial tool call delta for this new block
281
+ name = content.name
282
+ call_id = content.id
283
+ # Initialize arguments from the start event's input (often {}) to avoid undefined in UIs
284
+ if name in self.requires_approval_tools:
285
+ if prev_message_type and prev_message_type != "approval_request_message":
286
+ message_index += 1
287
+ tool_call_msg = ApprovalRequestMessage(
288
+ id=self.letta_message_id,
289
+ # Do not emit placeholder arguments here to avoid UI duplicates
290
+ tool_call=ToolCallDelta(name=name, tool_call_id=call_id),
291
+ date=datetime.now(timezone.utc).isoformat(),
292
+ otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
293
+ run_id=self.run_id,
294
+ step_id=self.step_id,
295
+ )
296
+ else:
297
+ if prev_message_type and prev_message_type != "tool_call_message":
298
+ message_index += 1
299
+ tool_call_msg = ToolCallMessage(
300
+ id=self.letta_message_id,
301
+ # Do not emit placeholder arguments here to avoid UI duplicates
302
+ tool_call=ToolCallDelta(name=name, tool_call_id=call_id),
303
+ date=datetime.now(timezone.utc).isoformat(),
304
+ otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
305
+ run_id=self.run_id,
306
+ step_id=self.step_id,
307
+ )
308
+ prev_message_type = tool_call_msg.message_type
309
+ yield tool_call_msg
310
+
311
+ elif isinstance(content, BetaThinkingBlock):
312
+ self.anthropic_mode = EventMode.THINKING
313
+ # TODO: Can capture signature, etc.
314
+
315
+ elif isinstance(content, BetaRedactedThinkingBlock):
316
+ self.anthropic_mode = EventMode.REDACTED_THINKING
317
+
318
+ if prev_message_type and prev_message_type != "hidden_reasoning_message":
319
+ message_index += 1
320
+
321
+ hidden_reasoning_message = HiddenReasoningMessage(
322
+ id=self.letta_message_id,
323
+ state="redacted",
324
+ hidden_reasoning=content.data,
325
+ date=datetime.now(timezone.utc).isoformat(),
326
+ otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
327
+ run_id=self.run_id,
328
+ step_id=self.step_id,
329
+ )
330
+
331
+ self.reasoning_messages.append(hidden_reasoning_message)
332
+ prev_message_type = hidden_reasoning_message.message_type
333
+ yield hidden_reasoning_message
334
+
335
+ elif isinstance(event, BetaRawContentBlockDeltaEvent):
336
+ delta = event.delta
337
+
338
+ if isinstance(delta, BetaTextDelta):
339
+ # Safety check
340
+ if not self.anthropic_mode == EventMode.TEXT:
341
+ raise RuntimeError(f"Streaming integrity failed - received BetaTextDelta object while not in TEXT EventMode: {delta}")
342
+
343
+ if prev_message_type and prev_message_type != "assistant_message":
344
+ message_index += 1
345
+
346
+ assistant_msg = AssistantMessage(
347
+ id=self.letta_message_id,
348
+ # content=[TextContent(text=delta.text)],
349
+ content=delta.text,
350
+ date=datetime.now(timezone.utc).isoformat(),
351
+ otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
352
+ run_id=self.run_id,
353
+ step_id=self.step_id,
354
+ )
355
+ # self.assistant_messages.append(assistant_msg)
356
+ self.reasoning_messages.append(assistant_msg)
357
+ prev_message_type = assistant_msg.message_type
358
+ yield assistant_msg
359
+
360
+ elif isinstance(delta, BetaInputJSONDelta):
361
+ # Append partial JSON for the specific tool_use block at this index
362
+ if not self.anthropic_mode == EventMode.TOOL_USE:
363
+ raise RuntimeError(
364
+ f"Streaming integrity failed - received BetaInputJSONDelta object while not in TOOL_USE EventMode: {delta}"
365
+ )
366
+
367
+ ctx = self.active_tool_uses.get(event.index)
368
+ if ctx is None:
369
+ # Defensive: initialize if missing
370
+ self.active_tool_uses[event.index] = {"id": self.tool_call_id or "", "name": self.tool_call_name or "", "args": ""}
371
+ ctx = self.active_tool_uses[event.index]
372
+
373
+ # Append only non-empty partials
374
+ if delta.partial_json:
375
+ ctx["args"] += delta.partial_json
376
+ else:
377
+ # Skip streaming a no-op delta to prevent duplicate placeholders in UI
378
+ return
379
+
380
+ name = ctx.get("name")
381
+ call_id = ctx.get("id")
382
+
383
+ if name in self.requires_approval_tools:
384
+ if prev_message_type and prev_message_type != "approval_request_message":
385
+ message_index += 1
386
+ tool_call_msg = ApprovalRequestMessage(
387
+ id=self.letta_message_id,
388
+ tool_call=ToolCallDelta(name=name, tool_call_id=call_id, arguments=delta.partial_json),
389
+ date=datetime.now(timezone.utc).isoformat(),
390
+ otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
391
+ run_id=self.run_id,
392
+ step_id=self.step_id,
393
+ )
394
+ else:
395
+ if prev_message_type and prev_message_type != "tool_call_message":
396
+ message_index += 1
397
+ tool_call_msg = ToolCallMessage(
398
+ id=self.letta_message_id,
399
+ tool_call=ToolCallDelta(name=name, tool_call_id=call_id, arguments=delta.partial_json),
400
+ date=datetime.now(timezone.utc).isoformat(),
401
+ otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
402
+ run_id=self.run_id,
403
+ step_id=self.step_id,
404
+ )
405
+
406
+ yield tool_call_msg
407
+
408
+ elif isinstance(delta, BetaThinkingDelta):
409
+ # Safety check
410
+ if not self.anthropic_mode == EventMode.THINKING:
411
+ raise RuntimeError(
412
+ f"Streaming integrity failed - received BetaThinkingBlock object while not in THINKING EventMode: {delta}"
413
+ )
414
+
415
+ if prev_message_type and prev_message_type != "reasoning_message":
416
+ message_index += 1
417
+ reasoning_message = ReasoningMessage(
418
+ id=self.letta_message_id,
419
+ source="reasoner_model",
420
+ reasoning=delta.thinking,
421
+ date=datetime.now(timezone.utc).isoformat(),
422
+ otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
423
+ run_id=self.run_id,
424
+ step_id=self.step_id,
425
+ )
426
+ self.reasoning_messages.append(reasoning_message)
427
+ prev_message_type = reasoning_message.message_type
428
+ yield reasoning_message
429
+
430
+ elif isinstance(delta, BetaSignatureDelta):
431
+ # Safety check
432
+ if not self.anthropic_mode == EventMode.THINKING:
433
+ raise RuntimeError(
434
+ f"Streaming integrity failed - received BetaSignatureDelta object while not in THINKING EventMode: {delta}"
435
+ )
436
+
437
+ if prev_message_type and prev_message_type != "reasoning_message":
438
+ message_index += 1
439
+ reasoning_message = ReasoningMessage(
440
+ id=self.letta_message_id,
441
+ source="reasoner_model",
442
+ reasoning="",
443
+ date=datetime.now(timezone.utc).isoformat(),
444
+ signature=delta.signature,
445
+ otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
446
+ run_id=self.run_id,
447
+ step_id=self.step_id,
448
+ )
449
+ self.reasoning_messages.append(reasoning_message)
450
+ prev_message_type = reasoning_message.message_type
451
+ yield reasoning_message
452
+
453
+ elif isinstance(event, BetaRawMessageStartEvent):
454
+ self.message_id = event.message.id
455
+ self.input_tokens += event.message.usage.input_tokens
456
+ self.output_tokens += event.message.usage.output_tokens
457
+ self.model = event.message.model
458
+
459
+ elif isinstance(event, BetaRawMessageDeltaEvent):
460
+ self.output_tokens += event.usage.output_tokens
461
+
462
+ elif isinstance(event, BetaRawMessageStopEvent):
463
+ # Don't do anything here! We don't want to stop the stream.
464
+ pass
465
+
466
+ elif isinstance(event, BetaRawContentBlockStopEvent):
467
+ # Finalize the tool_use block at this index using accumulated deltas
468
+ ctx = self.active_tool_uses.pop(event.index, None)
469
+ if ctx is not None and ctx.get("id") and ctx.get("name") is not None:
470
+ raw_args = ctx.get("args", "")
471
+ try:
472
+ # Prefer strict JSON load, fallback to permissive parser
473
+ tool_input = json.loads(raw_args) if raw_args else {}
474
+ except json.JSONDecodeError:
475
+ try:
476
+ tool_input = self.json_parser.parse(raw_args) if raw_args else {}
477
+ except Exception:
478
+ tool_input = {}
479
+
480
+ arguments = json.dumps(tool_input)
481
+ finalized = ToolCall(id=ctx["id"], function=FunctionCall(arguments=arguments, name=ctx["name"]))
482
+ # Keep both raw list and indexed list for compatibility
483
+ self.collected_tool_calls.append(finalized)
484
+ self._collected_indexed.append((event.index, finalized))
485
+
486
+ # Reset mode when a content block ends
487
+ self.anthropic_mode = None
@@ -420,6 +420,17 @@ class OpenAIClient(LLMClientBase):
420
420
  logger.warning(f"Model type not set in llm_config: {llm_config.model_dump_json(indent=4)}")
421
421
  model = None
422
422
 
423
+ # TODO: we may need to extend this to more models using proxy?
424
+ is_openrouter = (llm_config.model_endpoint and "openrouter.ai" in llm_config.model_endpoint) or (
425
+ llm_config.provider_name == "openrouter"
426
+ )
427
+ if is_openrouter:
428
+ try:
429
+ model = llm_config.handle.split("/", 1)[-1]
430
+ except:
431
+ # don't raise error since this isn't robust against edge cases
432
+ pass
433
+
423
434
  # force function calling for reliability, see https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice
424
435
  # TODO(matt) move into LLMConfig
425
436
  # TODO: This vllm checking is very brittle and is a patch at most
@@ -3,6 +3,8 @@ from typing import Optional
3
3
  from pydantic import Field
4
4
 
5
5
  from letta.schemas.letta_base import LettaBase, OrmMetadataBase
6
+ from letta.schemas.secret import Secret
7
+ from letta.settings import settings
6
8
 
7
9
 
8
10
  # Base Environment Variable
@@ -13,6 +15,28 @@ class EnvironmentVariableBase(OrmMetadataBase):
13
15
  description: Optional[str] = Field(None, description="An optional description of the environment variable.")
14
16
  organization_id: Optional[str] = Field(None, description="The ID of the organization this environment variable belongs to.")
15
17
 
18
+ # Encrypted field (stored as Secret object, serialized to string for DB)
19
+ # Secret class handles validation and serialization automatically via __get_pydantic_core_schema__
20
+ value_enc: Secret | None = Field(None, description="Encrypted value as Secret object")
21
+
22
+ def get_value_secret(self) -> Secret:
23
+ """Get the value as a Secret object, preferring encrypted over plaintext."""
24
+ # If value_enc is already a Secret, return it
25
+ if self.value_enc is not None:
26
+ return self.value_enc
27
+ # Otherwise, create from plaintext value
28
+ return Secret.from_db(None, self.value)
29
+
30
+ def set_value_secret(self, secret: Secret) -> None:
31
+ """Set value from a Secret object, directly storing the Secret."""
32
+ self.value_enc = secret
33
+ # Also update plaintext field for dual-write during migration
34
+ secret_dict = secret.to_dict()
35
+ if not secret.was_encrypted:
36
+ self.value = secret_dict["plaintext"]
37
+ else:
38
+ self.value = None
39
+
16
40
 
17
41
  class EnvironmentVariableCreateBase(LettaBase):
18
42
  key: str = Field(..., description="The name of the environment variable.")
@@ -8,6 +8,7 @@ from letta.schemas.enums import ProviderCategory, ProviderType
8
8
  from letta.schemas.letta_base import LettaBase
9
9
  from letta.schemas.llm_config import LLMConfig
10
10
  from letta.schemas.llm_config_overrides import LLM_HANDLE_OVERRIDES
11
+ from letta.schemas.secret import Secret
11
12
  from letta.settings import model_settings
12
13
 
13
14
 
@@ -28,8 +29,14 @@ class Provider(ProviderBase):
28
29
  organization_id: str | None = Field(None, description="The organization id of the user")
29
30
  updated_at: datetime | None = Field(None, description="The last update timestamp of the provider.")
30
31
 
32
+ # Encrypted fields (stored as Secret objects, serialized to strings for DB)
33
+ # Secret class handles validation and serialization automatically via __get_pydantic_core_schema__
34
+ api_key_enc: Secret | None = Field(None, description="Encrypted API key as Secret object")
35
+ access_key_enc: Secret | None = Field(None, description="Encrypted access key as Secret object")
36
+
31
37
  @model_validator(mode="after")
32
38
  def default_base_url(self):
39
+ # Set default base URL
33
40
  if self.provider_type == ProviderType.openai and self.base_url is None:
34
41
  self.base_url = model_settings.openai_api_base
35
42
  return self
@@ -38,6 +45,42 @@ class Provider(ProviderBase):
38
45
  if not self.id:
39
46
  self.id = ProviderBase.generate_id(prefix=ProviderBase.__id_prefix__)
40
47
 
48
+ def get_api_key_secret(self) -> Secret:
49
+ """Get the API key as a Secret object, preferring encrypted over plaintext."""
50
+ # If api_key_enc is already a Secret, return it
51
+ if self.api_key_enc is not None:
52
+ return self.api_key_enc
53
+ # Otherwise, create from plaintext api_key
54
+ return Secret.from_db(None, self.api_key)
55
+
56
+ def get_access_key_secret(self) -> Secret:
57
+ """Get the access key as a Secret object, preferring encrypted over plaintext."""
58
+ # If access_key_enc is already a Secret, return it
59
+ if self.access_key_enc is not None:
60
+ return self.access_key_enc
61
+ # Otherwise, create from plaintext access_key
62
+ return Secret.from_db(None, self.access_key)
63
+
64
+ def set_api_key_secret(self, secret: Secret) -> None:
65
+ """Set API key from a Secret object, directly storing the Secret."""
66
+ self.api_key_enc = secret
67
+ # Also update plaintext field for dual-write during migration
68
+ secret_dict = secret.to_dict()
69
+ if not secret.was_encrypted:
70
+ self.api_key = secret_dict["plaintext"]
71
+ else:
72
+ self.api_key = None
73
+
74
+ def set_access_key_secret(self, secret: Secret) -> None:
75
+ """Set access key from a Secret object, directly storing the Secret."""
76
+ self.access_key_enc = secret
77
+ # Also update plaintext field for dual-write during migration
78
+ secret_dict = secret.to_dict()
79
+ if not secret.was_encrypted:
80
+ self.access_key = secret_dict["plaintext"]
81
+ else:
82
+ self.access_key = None
83
+
41
84
  async def check_api_key(self):
42
85
  """Check if the API key is valid for the provider"""
43
86
  raise NotImplementedError
letta/schemas/secret.py CHANGED
@@ -2,6 +2,7 @@ import json
2
2
  from typing import Any, Dict, Optional
3
3
 
4
4
  from pydantic import BaseModel, ConfigDict, PrivateAttr
5
+ from pydantic_core import core_schema
5
6
 
6
7
  from letta.helpers.crypto_utils import CryptoUtils
7
8
  from letta.log import get_logger
@@ -19,16 +20,16 @@ class Secret(BaseModel):
19
20
  TODO: Once we deprecate plaintext columns in the database:
20
21
  - Remove the dual-write logic in to_dict()
21
22
  - Remove the from_db() method's plaintext_value parameter
22
- - Remove the _was_encrypted flag (no longer needed for migration)
23
+ - Remove the was_encrypted flag (no longer needed for migration)
23
24
  - Simplify get_plaintext() to only handle encrypted values
24
25
  """
25
26
 
26
- # Store the encrypted value
27
- _encrypted_value: Optional[str] = PrivateAttr(default=None)
28
- # Cache the decrypted value to avoid repeated decryption
27
+ # Store the encrypted value as a regular field
28
+ encrypted_value: Optional[str] = None
29
+ # Cache the decrypted value to avoid repeated decryption (not serialized for security)
29
30
  _plaintext_cache: Optional[str] = PrivateAttr(default=None)
30
31
  # Flag to indicate if the value was originally encrypted
31
- _was_encrypted: bool = PrivateAttr(default=False)
32
+ was_encrypted: bool = False
32
33
 
33
34
  model_config = ConfigDict(frozen=True)
34
35
 
@@ -44,18 +45,16 @@ class Secret(BaseModel):
44
45
  A Secret instance with the encrypted value, or plaintext if encryption unavailable
45
46
  """
46
47
  if value is None:
47
- instance = cls()
48
- instance._encrypted_value = None
49
- instance._was_encrypted = False
50
- return instance
48
+ return cls.model_construct(encrypted_value=None, was_encrypted=False)
49
+
50
+ # Guard against double encryption - check if value is already encrypted
51
+ if CryptoUtils.is_encrypted(value):
52
+ logger.warning("Creating Secret from already-encrypted value. This can be dangerous.")
51
53
 
52
54
  # Try to encrypt, but fall back to plaintext if no encryption key
53
55
  try:
54
56
  encrypted = CryptoUtils.encrypt(value)
55
- instance = cls()
56
- instance._encrypted_value = encrypted
57
- instance._was_encrypted = False
58
- return instance
57
+ return cls.model_construct(encrypted_value=encrypted, was_encrypted=False)
59
58
  except ValueError as e:
60
59
  # No encryption key available, store as plaintext
61
60
  if "No encryption key configured" in str(e):
@@ -63,10 +62,8 @@ class Secret(BaseModel):
63
62
  "No encryption key configured. Storing Secret value as plaintext. "
64
63
  "Set LETTA_ENCRYPTION_KEY environment variable to enable encryption."
65
64
  )
66
- instance = cls()
67
- instance._encrypted_value = value # Store plaintext
65
+ instance = cls.model_construct(encrypted_value=value, was_encrypted=False)
68
66
  instance._plaintext_cache = value # Cache it
69
- instance._was_encrypted = False
70
67
  return instance
71
68
  raise # Re-raise if it's a different error
72
69
 
@@ -81,10 +78,7 @@ class Secret(BaseModel):
81
78
  Returns:
82
79
  A Secret instance
83
80
  """
84
- instance = cls()
85
- instance._encrypted_value = encrypted_value
86
- instance._was_encrypted = True
87
- return instance
81
+ return cls.model_construct(encrypted_value=encrypted_value, was_encrypted=True)
88
82
 
89
83
  @classmethod
90
84
  def from_db(cls, encrypted_value: Optional[str], plaintext_value: Optional[str]) -> "Secret":
@@ -114,7 +108,7 @@ class Secret(BaseModel):
114
108
  Returns:
115
109
  The encrypted value, or None if the secret is empty
116
110
  """
117
- return self._encrypted_value
111
+ return self.encrypted_value
118
112
 
119
113
  def get_plaintext(self) -> Optional[str]:
120
114
  """
@@ -126,7 +120,7 @@ class Secret(BaseModel):
126
120
  Returns:
127
121
  The decrypted plaintext value
128
122
  """
129
- if self._encrypted_value is None:
123
+ if self.encrypted_value is None:
130
124
  return None
131
125
 
132
126
  # Use cached value if available, but only if it looks like plaintext
@@ -134,14 +128,14 @@ class Secret(BaseModel):
134
128
  if self._plaintext_cache is not None:
135
129
  # If we have a cache but the stored value looks encrypted and we have no key,
136
130
  # we should not use the cache
137
- if CryptoUtils.is_encrypted(self._encrypted_value) and not CryptoUtils.is_encryption_available():
131
+ if CryptoUtils.is_encrypted(self.encrypted_value) and not CryptoUtils.is_encryption_available():
138
132
  self._plaintext_cache = None # Clear invalid cache
139
133
  else:
140
134
  return self._plaintext_cache
141
135
 
142
136
  # Decrypt and cache
143
137
  try:
144
- plaintext = CryptoUtils.decrypt(self._encrypted_value)
138
+ plaintext = CryptoUtils.decrypt(self.encrypted_value)
145
139
  # Cache the decrypted value (PrivateAttr fields can be mutated even with frozen=True)
146
140
  self._plaintext_cache = plaintext
147
141
  return plaintext
@@ -151,7 +145,7 @@ class Secret(BaseModel):
151
145
  # Handle missing encryption key
152
146
  if "No encryption key configured" in error_msg:
153
147
  # Check if the value looks encrypted
154
- if CryptoUtils.is_encrypted(self._encrypted_value):
148
+ if CryptoUtils.is_encrypted(self.encrypted_value):
155
149
  # Value was encrypted, but now we have no key - can't decrypt
156
150
  logger.warning(
157
151
  "Cannot decrypt Secret value - no encryption key configured. "
@@ -162,26 +156,26 @@ class Secret(BaseModel):
162
156
  else:
163
157
  # Value is plaintext (stored when no key was available)
164
158
  logger.debug("Secret value is plaintext (stored without encryption)")
165
- self._plaintext_cache = self._encrypted_value
166
- return self._encrypted_value
159
+ self._plaintext_cache = self.encrypted_value
160
+ return self.encrypted_value
167
161
 
168
162
  # Handle decryption failure (might be plaintext stored as such)
169
163
  elif "Failed to decrypt data" in error_msg:
170
164
  # Check if it might be plaintext
171
- if not CryptoUtils.is_encrypted(self._encrypted_value):
165
+ if not CryptoUtils.is_encrypted(self.encrypted_value):
172
166
  # It's plaintext that was stored when no key was available
173
167
  logger.debug("Secret value appears to be plaintext (stored without encryption)")
174
- self._plaintext_cache = self._encrypted_value
175
- return self._encrypted_value
168
+ self._plaintext_cache = self.encrypted_value
169
+ return self.encrypted_value
176
170
  # Otherwise, it's corrupted or wrong key
177
171
  logger.error("Failed to decrypt Secret value - data may be corrupted or wrong key")
178
172
  raise
179
173
 
180
174
  # Migration case: handle legacy plaintext
181
- elif not self._was_encrypted:
182
- if self._encrypted_value and not CryptoUtils.is_encrypted(self._encrypted_value):
183
- self._plaintext_cache = self._encrypted_value
184
- return self._encrypted_value
175
+ elif not self.was_encrypted:
176
+ if self.encrypted_value and not CryptoUtils.is_encrypted(self.encrypted_value):
177
+ self._plaintext_cache = self.encrypted_value
178
+ return self.encrypted_value
185
179
  return None
186
180
 
187
181
  # Re-raise for other errors
@@ -189,7 +183,7 @@ class Secret(BaseModel):
189
183
 
190
184
  def is_empty(self) -> bool:
191
185
  """Check if the secret is empty/None."""
192
- return self._encrypted_value is None
186
+ return self.encrypted_value is None
193
187
 
194
188
  def __str__(self) -> str:
195
189
  """String representation that doesn't expose the actual value."""
@@ -207,7 +201,7 @@ class Secret(BaseModel):
207
201
 
208
202
  Returns both encrypted and plaintext values for dual-write during migration.
209
203
  """
210
- return {"encrypted": self.get_encrypted(), "plaintext": self.get_plaintext() if not self._was_encrypted else None}
204
+ return {"encrypted": self.get_encrypted(), "plaintext": self.get_plaintext() if not self.was_encrypted else None}
211
205
 
212
206
  def __eq__(self, other: Any) -> bool:
213
207
  """
@@ -219,6 +213,79 @@ class Secret(BaseModel):
219
213
  return False
220
214
  return self.get_plaintext() == other.get_plaintext()
221
215
 
216
+ @classmethod
217
+ def __get_pydantic_core_schema__(cls, source_type: Any, handler) -> core_schema.CoreSchema:
218
+ """
219
+ Customize Pydantic's validation and serialization behavior for Secret fields.
220
+
221
+ This allows Secret fields to automatically:
222
+ - Deserialize: Convert encrypted strings from DB → Secret objects
223
+ - Serialize: Convert Secret objects → encrypted strings for DB
224
+ """
225
+
226
+ def validate_secret(value: Any) -> "Secret":
227
+ """Convert various input types to Secret objects."""
228
+ if isinstance(value, Secret):
229
+ return value
230
+ elif isinstance(value, str):
231
+ # String from DB is assumed to be encrypted
232
+ return Secret.from_encrypted(value)
233
+ elif isinstance(value, dict):
234
+ # Dict might be from Pydantic serialization - check for encrypted_value key
235
+ if "encrypted_value" in value:
236
+ # This is a serialized Secret being deserialized
237
+ return cls(**value)
238
+ elif not value or value == {}:
239
+ # Empty dict means None
240
+ return Secret.from_plaintext(None)
241
+ else:
242
+ raise ValueError(f"Cannot convert dict to Secret: {value}")
243
+ elif value is None:
244
+ return Secret.from_plaintext(None)
245
+ else:
246
+ raise ValueError(f"Cannot convert {type(value)} to Secret")
247
+
248
+ def serialize_secret(secret: "Secret") -> Optional[str]:
249
+ """Serialize Secret to encrypted string."""
250
+ if secret is None:
251
+ return None
252
+ return secret.get_encrypted()
253
+
254
+ python_schema = core_schema.chain_schema(
255
+ [
256
+ core_schema.no_info_plain_validator_function(validate_secret),
257
+ core_schema.is_instance_schema(cls),
258
+ ]
259
+ )
260
+
261
+ return core_schema.json_or_python_schema(
262
+ json_schema=python_schema,
263
+ python_schema=python_schema,
264
+ serialization=core_schema.plain_serializer_function_ser_schema(
265
+ serialize_secret,
266
+ when_used="always",
267
+ ),
268
+ )
269
+
270
+ @classmethod
271
+ def __get_pydantic_json_schema__(cls, core_schema: core_schema.CoreSchema, handler) -> Dict[str, Any]:
272
+ """
273
+ Define JSON schema representation for Secret fields.
274
+ In JSON schema (OpenAPI docs), Secret fields appear as nullable strings.
275
+ The actual encryption/decryption happens at runtime via __get_pydantic_core_schema__.
276
+ Args:
277
+ core_schema: The core schema for this type
278
+ handler: Handler for generating JSON schema
279
+ Returns:
280
+ A JSON schema dict representing this type as a nullable string
281
+ """
282
+ # Return a simple string schema for JSON schema generation
283
+ return {
284
+ "type": "string",
285
+ "nullable": True,
286
+ "description": "Encrypted secret value (stored as encrypted string)",
287
+ }
288
+
222
289
 
223
290
  class SecretDict(BaseModel):
224
291
  """
letta/settings.py CHANGED
@@ -329,6 +329,9 @@ class Settings(BaseSettings):
329
329
  file_processing_timeout_minutes: int = 30
330
330
  file_processing_timeout_error_message: str = "File processing timed out after {} minutes. Please try again."
331
331
 
332
+ # enabling letta_agent_v1 architecture
333
+ use_letta_v1_agent: bool = False
334
+
332
335
  @property
333
336
  def letta_pg_uri(self) -> str:
334
337
  if self.pg_uri:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: letta-nightly
3
- Version: 0.12.0.dev20251009203644
3
+ Version: 0.12.1.dev20251010012355
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  Author-email: Letta Team <contact@letta.com>
6
6
  License: Apache License
@@ -1,4 +1,4 @@
1
- letta/__init__.py,sha256=YF1Hcr7H49Jrjsp2dkyB-FWzee4pnuGLJnLBERmmsi0,1620
1
+ letta/__init__.py,sha256=dpn9tUBW7H4GEY_umGCKhKfZA9qu8S1F1ZPBPFuJJIA,1620
2
2
  letta/agent.py,sha256=xP13DDysFq-h3Ny7DiTN5ZC2txRripMNvPFt8dfFDbE,89498
3
3
  letta/config.py,sha256=JFGY4TWW0Wm5fTbZamOwWqk5G8Nn-TXyhgByGoAqy2c,12375
4
4
  letta/constants.py,sha256=Yxp2DGlVnesnevIQbXqfYadxvVuYYk_6-lsF7YmHixs,16255
@@ -10,7 +10,7 @@ letta/log.py,sha256=-hkEaSIru3cvO8ynHPWb91jemJHzgWjmVc3fqGJrjvU,2294
10
10
  letta/main.py,sha256=A0e4EXoW9XdlCzJ8EyMQaukjNhu_8XI0_FIiwyHTENM,458
11
11
  letta/memory.py,sha256=xE1gAm99_PN0mWi4EL5CEIE95y9XFD4G0rHKTkO17-w,4325
12
12
  letta/pytest.ini,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- letta/settings.py,sha256=CbSbYJ21q8MSrWJBie0YxXC2ijmPLNr7mojU4DPZuH4,15810
13
+ letta/settings.py,sha256=r7yRYXiWsdJnBUvFmEPLVHuiBTuvtrDem-TtUsmNPM4,15891
14
14
  letta/streaming_interface.py,sha256=rPMfwUcjqITWk2tVqFQm1hmP99tU2IOHg9gU2dgPSo8,16400
15
15
  letta/streaming_utils.py,sha256=ZRFGFpQqn9ujCEbgZdLM7yTjiuNNvqQ47sNhV8ix-yQ,16553
16
16
  letta/system.py,sha256=NClLhwB9Dw20L6IDPpfcnmxXqYQlA5E92xzJ0hGPLo0,8993
@@ -19,7 +19,7 @@ letta/adapters/letta_llm_adapter.py,sha256=Uo9l2lUI-n9mymWlK8lTResdWx0mc-F_ttmmK
19
19
  letta/adapters/letta_llm_request_adapter.py,sha256=ApEaMOkvtH5l09NvVgm0r2qaqTnGSqwVqAFA7n5N1HA,4757
20
20
  letta/adapters/letta_llm_stream_adapter.py,sha256=p31xPQ-pU4SYPImlC6frl3cnVltSdCzO-NDD2Q7W8L0,7859
21
21
  letta/adapters/simple_llm_request_adapter.py,sha256=LmzawoEqqcqAeqvdmWikRUZRnb3BriMN71IyyOm-uJo,3734
22
- letta/adapters/simple_llm_stream_adapter.py,sha256=a7SK4qBFGEEH1ttq6I2H5f-cfPojwvmyYBI5Q1nscPY,8879
22
+ letta/adapters/simple_llm_stream_adapter.py,sha256=Rq-m48GQ7jdWaCAsDQd6li6HL0s_7VREi8pE2OLYIO0,8898
23
23
  letta/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  letta/agents/agent_loop.py,sha256=IODUozq61N73V55stSN65QLBs9nCD9IHtFTJHr51E0c,1120
25
25
  letta/agents/base_agent.py,sha256=TAc5quD9KRMKOurz_-ZHoAPZe13ZWFlE6sDjaPqN-fk,8518
@@ -30,7 +30,7 @@ letta/agents/exceptions.py,sha256=BQY4D4w32OYHM63CM19ko7dPwZiAzUs3NbKvzmCTcJg,31
30
30
  letta/agents/helpers.py,sha256=Hz8dKczPxMmDh-ILiUAEHgT55hMgIHO1yV8Vo0PORcw,16837
31
31
  letta/agents/letta_agent.py,sha256=4SSx6aORD28B1IogEoWeCTQr4jBaniX9jJolD4pssaQ,98156
32
32
  letta/agents/letta_agent_batch.py,sha256=NizNeIHvFtG4XpZCIplOSF8GohwHvNEkAUvsFBSdtSs,27950
33
- letta/agents/letta_agent_v2.py,sha256=MBoXVMIPixozb3xts2Bl8BCbExMIv3vtGbKZK1RitT4,60101
33
+ letta/agents/letta_agent_v2.py,sha256=vRUDrkFhRl-HRfoYMm3z_T6msjuMo7kt5itQdIHE09A,60121
34
34
  letta/agents/letta_agent_v3.py,sha256=i8idcEKVcZxJk67T_g0zLFBcrlHfygcuN9V_xk4Sj0c,48081
35
35
  letta/agents/voice_agent.py,sha256=DP7g7TPFhaIAxmpur9yyJVI8Sb_MnoWfpHm7Q-t7mJI,23177
36
36
  letta/agents/voice_sleeptime_agent.py,sha256=_JzCbWBOKrmo1cTaqZFTrQudpJEapwAyrXYtAHUILGo,8675
@@ -88,6 +88,7 @@ letta/humans/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
88
88
  letta/humans/examples/basic.txt,sha256=Lcp8YESTWvOJgO4Yf_yyQmgo5bKakeB1nIVrwEGG6PA,17
89
89
  letta/humans/examples/cs_phd.txt,sha256=9C9ZAV_VuG7GB31ksy3-_NAyk8rjE6YtVOkhp08k1xw,297
90
90
  letta/interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
91
+ letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py,sha256=c65ULgokOuO-zbg_ek4cnHytIxcNx9AfTYOOaAnCMLY,21820
91
92
  letta/interfaces/anthropic_streaming_interface.py,sha256=VaqI_aLAOk8EbVmDLe8PMOb5LsKNhVqpgVbLe-WbjMg,44780
92
93
  letta/interfaces/gemini_streaming_interface.py,sha256=1nJwGl3nKAL7yfDX0zzpWrrjjpqzRFo82JpsEF6u_cA,12406
93
94
  letta/interfaces/openai_chat_completions_streaming_interface.py,sha256=3xHXh8cW79EkiMUTYfvcH_s92nkLjxXfvtVOVC3bfLo,5050
@@ -113,7 +114,7 @@ letta/llm_api/llm_client.py,sha256=iXiPbrhluP2DBczv9nkFlAXdwWGOkg0lNDA9LzLrG4o,3
113
114
  letta/llm_api/llm_client_base.py,sha256=31I1ancGiAaNBRMGbVkaBDgJ0Tu1cc0XLlTnjUllqbM,10085
114
115
  letta/llm_api/mistral.py,sha256=ruOTBt07Uzx7S30_eXhedVWngtpjtlzG6Ox1Iw0_mQs,662
115
116
  letta/llm_api/openai.py,sha256=loY0N97Ot_TyNZhNyPCZsFv3NHOXZyLNiecvLvZT1hs,28771
116
- letta/llm_api/openai_client.py,sha256=nY_X7OWl1OUwuG2mcTr19dRhX9LC23vKflCokegEzhg,41606
117
+ letta/llm_api/openai_client.py,sha256=hKBU0PshJNeYXLvio3fvaDUEHjQbDoqJNSyOUE6wi3o,42070
117
118
  letta/llm_api/together_client.py,sha256=HeDMDDa525yfDTKciODDfX_t93QBfFmX0n2P-FT1QTU,2284
118
119
  letta/llm_api/xai_client.py,sha256=_hlZrNeGy86hix-vTUcDe_3TrVnn6wSBHv1tI2YtGAQ,3941
119
120
  letta/llm_api/sample_response_jsons/aws_bedrock.json,sha256=RS3VqyxPB9hQQCPm42hWoga0bisKv_0e8ZF-c3Ag1FA,930
@@ -253,7 +254,7 @@ letta/schemas/block.py,sha256=SIHjoxgwl9JmJROmntznIZSA0OJBdbj4R58OUjg3RdU,7960
253
254
  letta/schemas/embedding_config.py,sha256=ZaD40UeeAX6A9C6bQVhrKwNDiMEuOn7-5uHcj9_T_D0,3740
254
255
  letta/schemas/embedding_config_overrides.py,sha256=lkTa4y-EQ2RnaEKtKDM0sEAk7EwNa67REw8DGNNtGQY,84
255
256
  letta/schemas/enums.py,sha256=9G968IVdkrCzJ-cU0_GJZDosQdk7qMlvC8_a0ylh_v8,5704
256
- letta/schemas/environment_variables.py,sha256=VRtzOjdeQdHcSHXisk7oJUQlheruxhSWNS0xqlfGzbs,2429
257
+ letta/schemas/environment_variables.py,sha256=XX_fk_rDaBd3r5ORnynewNIml0he5JdWdGE3NS5cTyM,3533
257
258
  letta/schemas/file.py,sha256=78aTJuYqhhTu95hKFmcJmCa5_wJMZ521aiRzPM2plAM,6242
258
259
  letta/schemas/folder.py,sha256=OpTj9idfGx6CEKDySeDEu3ZNDYjl8jJ02CH96RWPAVk,3309
259
260
  letta/schemas/group.py,sha256=X4ioNEUraBa_pGN1bNBNugkQieGMLvh6rXisFgcHwoo,8663
@@ -284,7 +285,7 @@ letta/schemas/response_format.py,sha256=b2onyfSDCxnNkSHd4NsfJg_4ni5qBIK_F6zeJoMv
284
285
  letta/schemas/run.py,sha256=wjpDNsgme51WVr5EJ12Px6qhtOYbw-PMpJP32lche14,3630
285
286
  letta/schemas/run_metrics.py,sha256=xbeObgpP-0guO3RRvEfEK5YNV6G7ol4whx3_ds88mIk,1147
286
287
  letta/schemas/sandbox_config.py,sha256=iw3-QS7PNy649tdynTJUxBbaruprykYAuGO6q28w-gU,5974
287
- letta/schemas/secret.py,sha256=1vq33Z-Oe2zSeVnGuj6j04YoO0Y9LxX1bmkprg6C1NA,15659
288
+ letta/schemas/secret.py,sha256=grMrOGFDRf5JHw9xtacD0KxCk89OGdTzO67zkFMYr8c,18737
288
289
  letta/schemas/source.py,sha256=Uxsm8-XA3vuIt5Ihu_l2Aau7quuqmyIDg7qIryklUqY,3565
289
290
  letta/schemas/source_metadata.py,sha256=_dGjuXhGcVMlc53ja9yuk16Uj64ggEzilRDgmkqYfNs,1334
290
291
  letta/schemas/step.py,sha256=rPT20AvHk31iGZRblp1tXbAgKedE4JZQKhu3RkvymrE,3572
@@ -303,7 +304,7 @@ letta/schemas/openai/responses_request.py,sha256=7UlZt2XProhRZ9L_oUQ5HrD9wPxDzvB
303
304
  letta/schemas/providers/__init__.py,sha256=3_8xegx1svx31qAupfpIZ4MmeW1_2FRmxZbhjfLnSZo,1405
304
305
  letta/schemas/providers/anthropic.py,sha256=CWvCN2wAf6PyrgQy1dDv-MoYwYSaw3f-wzzNUaeXTkY,6835
305
306
  letta/schemas/providers/azure.py,sha256=c0aSfKdVwS_4wWnqng4fe_jmXGmvICRES-8nmqzoEpg,7692
306
- letta/schemas/providers/base.py,sha256=xGIsj7py0p8xp4LXvarDxhTxU93LF88HhCpvenBB0eo,10074
307
+ letta/schemas/providers/base.py,sha256=_Zven0SepeI3kBwpTKA3oapXWBxoUk4FdBAxckgLvTI,12121
307
308
  letta/schemas/providers/bedrock.py,sha256=_8HPjLQ3KKj2x2zINUVbqbHxToXDOhkbQA2A3bDFntU,3634
308
309
  letta/schemas/providers/cerebras.py,sha256=tFPN_LTJL_rzRE0DvXHb5Ckyaw4534X2kmibhWjHlA8,3208
309
310
  letta/schemas/providers/deepseek.py,sha256=FXu8qPedeQC2Cn_L-k-1sQjj4feB8mbmW9XMJYx5hjE,2623
@@ -476,8 +477,8 @@ letta/templates/sandbox_code_file.py.j2,sha256=eXga5J_04Z8-pGdwfOCDjcRnMceIqcF5i
476
477
  letta/templates/sandbox_code_file_async.py.j2,sha256=lb7nh_P2W9VZHzU_9TxSCEMUod7SDziPXgvT75xVds0,2748
477
478
  letta/templates/summary_request_text.j2,sha256=ZttQwXonW2lk4pJLYzLK0pmo4EO4EtUUIXjgXKiizuc,842
478
479
  letta/types/__init__.py,sha256=hokKjCVFGEfR7SLMrtZsRsBfsC7yTIbgKPLdGg4K1eY,147
479
- letta_nightly-0.12.0.dev20251009203644.dist-info/METADATA,sha256=P1HTklX5YTlDe3BCg-vOF-NsHMseJx7r19qrHXzPft4,24468
480
- letta_nightly-0.12.0.dev20251009203644.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
481
- letta_nightly-0.12.0.dev20251009203644.dist-info/entry_points.txt,sha256=m-94Paj-kxiR6Ktu0us0_2qfhn29DzF2oVzqBE6cu8w,41
482
- letta_nightly-0.12.0.dev20251009203644.dist-info/licenses/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
483
- letta_nightly-0.12.0.dev20251009203644.dist-info/RECORD,,
480
+ letta_nightly-0.12.1.dev20251010012355.dist-info/METADATA,sha256=aoPdv79qrEbohSKR-WbKaoRViQCJCcCzNcjjXtsTvSs,24468
481
+ letta_nightly-0.12.1.dev20251010012355.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
482
+ letta_nightly-0.12.1.dev20251010012355.dist-info/entry_points.txt,sha256=m-94Paj-kxiR6Ktu0us0_2qfhn29DzF2oVzqBE6cu8w,41
483
+ letta_nightly-0.12.1.dev20251010012355.dist-info/licenses/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
484
+ letta_nightly-0.12.1.dev20251010012355.dist-info/RECORD,,