letta-nightly 0.13.0.dev20251031104146__py3-none-any.whl → 0.13.1.dev20251101010313__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.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +1 -1
- letta/adapters/simple_llm_stream_adapter.py +1 -0
- letta/agents/letta_agent_v2.py +8 -0
- letta/agents/letta_agent_v3.py +127 -27
- letta/agents/temporal/activities/__init__.py +25 -0
- letta/agents/temporal/activities/create_messages.py +26 -0
- letta/agents/temporal/activities/create_step.py +57 -0
- letta/agents/temporal/activities/example_activity.py +9 -0
- letta/agents/temporal/activities/execute_tool.py +130 -0
- letta/agents/temporal/activities/llm_request.py +114 -0
- letta/agents/temporal/activities/prepare_messages.py +27 -0
- letta/agents/temporal/activities/refresh_context.py +160 -0
- letta/agents/temporal/activities/summarize_conversation_history.py +77 -0
- letta/agents/temporal/activities/update_message_ids.py +25 -0
- letta/agents/temporal/activities/update_run.py +43 -0
- letta/agents/temporal/constants.py +59 -0
- letta/agents/temporal/temporal_agent_workflow.py +704 -0
- letta/agents/temporal/types.py +275 -0
- letta/constants.py +11 -0
- letta/errors.py +4 -0
- letta/functions/function_sets/base.py +0 -11
- letta/groups/helpers.py +7 -1
- letta/groups/sleeptime_multi_agent_v4.py +4 -3
- letta/interfaces/anthropic_streaming_interface.py +0 -1
- letta/interfaces/openai_streaming_interface.py +103 -100
- letta/llm_api/anthropic_client.py +57 -12
- letta/llm_api/bedrock_client.py +1 -0
- letta/llm_api/deepseek_client.py +3 -2
- letta/llm_api/google_vertex_client.py +5 -4
- letta/llm_api/groq_client.py +1 -0
- letta/llm_api/llm_client_base.py +15 -1
- letta/llm_api/openai.py +2 -2
- letta/llm_api/openai_client.py +17 -3
- letta/llm_api/xai_client.py +1 -0
- letta/orm/agent.py +3 -0
- letta/orm/organization.py +4 -0
- letta/orm/sqlalchemy_base.py +7 -0
- letta/otel/tracing.py +131 -4
- letta/schemas/agent.py +108 -40
- letta/schemas/agent_file.py +10 -10
- letta/schemas/block.py +22 -3
- letta/schemas/enums.py +21 -0
- letta/schemas/environment_variables.py +3 -2
- letta/schemas/group.py +3 -3
- letta/schemas/letta_response.py +36 -4
- letta/schemas/llm_batch_job.py +3 -3
- letta/schemas/llm_config.py +123 -4
- letta/schemas/mcp.py +3 -2
- letta/schemas/mcp_server.py +3 -2
- letta/schemas/message.py +167 -49
- letta/schemas/model.py +265 -0
- letta/schemas/organization.py +2 -1
- letta/schemas/passage.py +2 -1
- letta/schemas/provider_trace.py +2 -1
- letta/schemas/providers/openrouter.py +1 -2
- letta/schemas/run_metrics.py +2 -1
- letta/schemas/sandbox_config.py +3 -1
- letta/schemas/step_metrics.py +2 -1
- letta/schemas/tool_rule.py +2 -2
- letta/schemas/user.py +2 -1
- letta/server/rest_api/app.py +5 -1
- letta/server/rest_api/routers/v1/__init__.py +4 -0
- letta/server/rest_api/routers/v1/agents.py +71 -9
- letta/server/rest_api/routers/v1/blocks.py +7 -7
- letta/server/rest_api/routers/v1/groups.py +40 -0
- letta/server/rest_api/routers/v1/identities.py +2 -2
- letta/server/rest_api/routers/v1/internal_agents.py +31 -0
- letta/server/rest_api/routers/v1/internal_blocks.py +177 -0
- letta/server/rest_api/routers/v1/internal_runs.py +25 -1
- letta/server/rest_api/routers/v1/runs.py +2 -22
- letta/server/rest_api/routers/v1/tools.py +12 -1
- letta/server/server.py +20 -4
- letta/services/agent_manager.py +4 -4
- letta/services/archive_manager.py +16 -0
- letta/services/group_manager.py +44 -0
- letta/services/helpers/run_manager_helper.py +2 -2
- letta/services/lettuce/lettuce_client.py +148 -0
- letta/services/mcp/base_client.py +9 -3
- letta/services/run_manager.py +148 -37
- letta/services/source_manager.py +91 -3
- letta/services/step_manager.py +2 -3
- letta/services/streaming_service.py +52 -13
- letta/services/summarizer/summarizer.py +28 -2
- letta/services/tool_executor/builtin_tool_executor.py +1 -1
- letta/services/tool_executor/core_tool_executor.py +2 -117
- letta/services/tool_sandbox/e2b_sandbox.py +4 -1
- letta/services/tool_schema_generator.py +2 -2
- letta/validators.py +21 -0
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/METADATA +1 -1
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/RECORD +93 -87
- letta/agent.py +0 -1758
- letta/cli/cli_load.py +0 -16
- letta/client/__init__.py +0 -0
- letta/client/streaming.py +0 -95
- letta/client/utils.py +0 -78
- letta/functions/async_composio_toolset.py +0 -109
- letta/functions/composio_helpers.py +0 -96
- letta/helpers/composio_helpers.py +0 -38
- letta/orm/job_messages.py +0 -33
- letta/schemas/providers.py +0 -1617
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -132
- letta/services/tool_executor/composio_tool_executor.py +0 -57
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/WHEEL +0 -0
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251101010313.dist-info}/licenses/LICENSE +0 -0
letta/schemas/llm_config.py
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING, Literal, Optional
|
|
1
|
+
from typing import TYPE_CHECKING, Annotated, Literal, Optional, Union
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
4
4
|
|
|
5
5
|
from letta.constants import LETTA_MODEL_ENDPOINT
|
|
6
|
+
from letta.errors import LettaInvalidArgumentError
|
|
6
7
|
from letta.log import get_logger
|
|
7
8
|
from letta.schemas.enums import AgentType, ProviderCategory
|
|
8
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from letta.schemas.model import ModelSettings
|
|
12
|
+
|
|
9
13
|
logger = get_logger(__name__)
|
|
10
14
|
|
|
11
15
|
|
|
@@ -163,6 +167,24 @@ class LLMConfig(BaseModel):
|
|
|
163
167
|
|
|
164
168
|
return values
|
|
165
169
|
|
|
170
|
+
@model_validator(mode="before")
|
|
171
|
+
@classmethod
|
|
172
|
+
def validate_codex_reasoning_effort(cls, values):
|
|
173
|
+
"""
|
|
174
|
+
Validate that gpt-5-codex models do not use 'minimal' reasoning effort.
|
|
175
|
+
Codex models require at least 'low' reasoning effort.
|
|
176
|
+
"""
|
|
177
|
+
from letta.llm_api.openai_client import does_not_support_minimal_reasoning
|
|
178
|
+
|
|
179
|
+
model = values.get("model")
|
|
180
|
+
reasoning_effort = values.get("reasoning_effort")
|
|
181
|
+
|
|
182
|
+
if model and does_not_support_minimal_reasoning(model) and reasoning_effort == "minimal":
|
|
183
|
+
raise LettaInvalidArgumentError(
|
|
184
|
+
f"Model '{model}' does not support 'minimal' reasoning effort. Please use 'low', 'medium', or 'high' instead."
|
|
185
|
+
)
|
|
186
|
+
return values
|
|
187
|
+
|
|
166
188
|
@classmethod
|
|
167
189
|
def default_config(cls, model_name: str):
|
|
168
190
|
"""
|
|
@@ -233,6 +255,98 @@ class LLMConfig(BaseModel):
|
|
|
233
255
|
+ (f" [ip={self.model_endpoint}]" if self.model_endpoint else "")
|
|
234
256
|
)
|
|
235
257
|
|
|
258
|
+
def _to_model(self) -> "ModelSettings":
|
|
259
|
+
"""
|
|
260
|
+
Convert LLMConfig back into a Model schema (OpenAIModelSettings, AnthropicModelSettings, etc.).
|
|
261
|
+
This is the inverse of the _to_legacy_config_params() methods in model.py.
|
|
262
|
+
"""
|
|
263
|
+
from letta.schemas.model import (
|
|
264
|
+
AnthropicModelSettings,
|
|
265
|
+
AnthropicThinking,
|
|
266
|
+
AzureModelSettings,
|
|
267
|
+
BedrockModelSettings,
|
|
268
|
+
DeepseekModelSettings,
|
|
269
|
+
GeminiThinkingConfig,
|
|
270
|
+
GoogleAIModelSettings,
|
|
271
|
+
GoogleVertexModelSettings,
|
|
272
|
+
GroqModelSettings,
|
|
273
|
+
Model,
|
|
274
|
+
OpenAIModelSettings,
|
|
275
|
+
OpenAIReasoning,
|
|
276
|
+
TogetherModelSettings,
|
|
277
|
+
XAIModelSettings,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if self.model_endpoint_type == "openai":
|
|
281
|
+
return OpenAIModelSettings(
|
|
282
|
+
model=self.model,
|
|
283
|
+
max_output_tokens=self.max_tokens or 4096,
|
|
284
|
+
temperature=self.temperature,
|
|
285
|
+
reasoning=OpenAIReasoning(reasoning_effort=self.reasoning_effort or "minimal"),
|
|
286
|
+
)
|
|
287
|
+
elif self.model_endpoint_type == "anthropic":
|
|
288
|
+
thinking_type = "enabled" if self.enable_reasoner else "disabled"
|
|
289
|
+
return AnthropicModelSettings(
|
|
290
|
+
model=self.model,
|
|
291
|
+
max_output_tokens=self.max_tokens or 4096,
|
|
292
|
+
temperature=self.temperature,
|
|
293
|
+
thinking=AnthropicThinking(type=thinking_type, budget_tokens=self.max_reasoning_tokens or 1024),
|
|
294
|
+
verbosity=self.verbosity,
|
|
295
|
+
)
|
|
296
|
+
elif self.model_endpoint_type == "google_ai":
|
|
297
|
+
return GoogleAIModelSettings(
|
|
298
|
+
model=self.model,
|
|
299
|
+
max_output_tokens=self.max_tokens or 65536,
|
|
300
|
+
temperature=self.temperature,
|
|
301
|
+
thinking_config=GeminiThinkingConfig(
|
|
302
|
+
include_thoughts=self.max_reasoning_tokens > 0, thinking_budget=self.max_reasoning_tokens or 1024
|
|
303
|
+
),
|
|
304
|
+
)
|
|
305
|
+
elif self.model_endpoint_type == "google_vertex":
|
|
306
|
+
return GoogleVertexModelSettings(
|
|
307
|
+
model=self.model,
|
|
308
|
+
max_output_tokens=self.max_tokens or 65536,
|
|
309
|
+
temperature=self.temperature,
|
|
310
|
+
thinking_config=GeminiThinkingConfig(
|
|
311
|
+
include_thoughts=self.max_reasoning_tokens > 0, thinking_budget=self.max_reasoning_tokens or 1024
|
|
312
|
+
),
|
|
313
|
+
)
|
|
314
|
+
elif self.model_endpoint_type == "azure":
|
|
315
|
+
return AzureModelSettings(
|
|
316
|
+
model=self.model,
|
|
317
|
+
max_output_tokens=self.max_tokens or 4096,
|
|
318
|
+
temperature=self.temperature,
|
|
319
|
+
)
|
|
320
|
+
elif self.model_endpoint_type == "xai":
|
|
321
|
+
return XAIModelSettings(
|
|
322
|
+
model=self.model,
|
|
323
|
+
max_output_tokens=self.max_tokens or 4096,
|
|
324
|
+
temperature=self.temperature,
|
|
325
|
+
)
|
|
326
|
+
elif self.model_endpoint_type == "groq":
|
|
327
|
+
return GroqModelSettings(
|
|
328
|
+
model=self.model,
|
|
329
|
+
max_output_tokens=self.max_tokens or 4096,
|
|
330
|
+
temperature=self.temperature,
|
|
331
|
+
)
|
|
332
|
+
elif self.model_endpoint_type == "deepseek":
|
|
333
|
+
return DeepseekModelSettings(
|
|
334
|
+
model=self.model,
|
|
335
|
+
max_output_tokens=self.max_tokens or 4096,
|
|
336
|
+
temperature=self.temperature,
|
|
337
|
+
)
|
|
338
|
+
elif self.model_endpoint_type == "together":
|
|
339
|
+
return TogetherModelSettings(
|
|
340
|
+
model=self.model,
|
|
341
|
+
max_output_tokens=self.max_tokens or 4096,
|
|
342
|
+
temperature=self.temperature,
|
|
343
|
+
)
|
|
344
|
+
elif self.model_endpoint_type == "bedrock":
|
|
345
|
+
return Model(model=self.model, max_output_tokens=self.max_tokens or 4096)
|
|
346
|
+
else:
|
|
347
|
+
# If we don't know the model type, use the default Model schema
|
|
348
|
+
return Model(model=self.model, max_output_tokens=self.max_tokens or 4096)
|
|
349
|
+
|
|
236
350
|
@classmethod
|
|
237
351
|
def is_openai_reasoning_model(cls, config: "LLMConfig") -> bool:
|
|
238
352
|
from letta.llm_api.openai_client import is_openai_reasoning_model
|
|
@@ -277,6 +391,8 @@ class LLMConfig(BaseModel):
|
|
|
277
391
|
- Google Gemini (2.5 family): force disabled until native reasoning supported
|
|
278
392
|
- All others: disabled (no simulated reasoning via kwargs)
|
|
279
393
|
"""
|
|
394
|
+
from letta.llm_api.openai_client import does_not_support_minimal_reasoning
|
|
395
|
+
|
|
280
396
|
# V1 agent policy: do not allow simulated reasoning for non-native models
|
|
281
397
|
if agent_type is not None and agent_type == AgentType.letta_v1_agent:
|
|
282
398
|
# OpenAI native reasoning models: always on
|
|
@@ -284,7 +400,8 @@ class LLMConfig(BaseModel):
|
|
|
284
400
|
config.put_inner_thoughts_in_kwargs = False
|
|
285
401
|
config.enable_reasoner = True
|
|
286
402
|
if config.reasoning_effort is None:
|
|
287
|
-
|
|
403
|
+
# Codex models cannot use "minimal" reasoning effort
|
|
404
|
+
if config.model.startswith("gpt-5") and not does_not_support_minimal_reasoning(config.model):
|
|
288
405
|
config.reasoning_effort = "minimal"
|
|
289
406
|
else:
|
|
290
407
|
config.reasoning_effort = "medium"
|
|
@@ -324,7 +441,8 @@ class LLMConfig(BaseModel):
|
|
|
324
441
|
config.enable_reasoner = True
|
|
325
442
|
if config.reasoning_effort is None:
|
|
326
443
|
# GPT-5 models default to minimal, others to medium
|
|
327
|
-
|
|
444
|
+
# Codex models cannot use "minimal" reasoning effort
|
|
445
|
+
if config.model.startswith("gpt-5") and not does_not_support_minimal_reasoning(config.model):
|
|
328
446
|
config.reasoning_effort = "minimal"
|
|
329
447
|
else:
|
|
330
448
|
config.reasoning_effort = "medium"
|
|
@@ -357,7 +475,8 @@ class LLMConfig(BaseModel):
|
|
|
357
475
|
config.put_inner_thoughts_in_kwargs = False
|
|
358
476
|
if config.reasoning_effort is None:
|
|
359
477
|
# GPT-5 models default to minimal, others to medium
|
|
360
|
-
|
|
478
|
+
# Codex models cannot use "minimal" reasoning effort
|
|
479
|
+
if config.model.startswith("gpt-5") and not does_not_support_minimal_reasoning(config.model):
|
|
361
480
|
config.reasoning_effort = "minimal"
|
|
362
481
|
else:
|
|
363
482
|
config.reasoning_effort = "medium"
|
letta/schemas/mcp.py
CHANGED
|
@@ -13,13 +13,14 @@ from letta.functions.mcp_client.types import (
|
|
|
13
13
|
StreamableHTTPServerConfig,
|
|
14
14
|
)
|
|
15
15
|
from letta.orm.mcp_oauth import OAuthSessionStatus
|
|
16
|
+
from letta.schemas.enums import PrimitiveType
|
|
16
17
|
from letta.schemas.letta_base import LettaBase
|
|
17
18
|
from letta.schemas.secret import Secret
|
|
18
19
|
from letta.settings import settings
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class BaseMCPServer(LettaBase):
|
|
22
|
-
__id_prefix__ =
|
|
23
|
+
__id_prefix__ = PrimitiveType.MCP_SERVER.value
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class MCPServer(BaseMCPServer):
|
|
@@ -178,7 +179,7 @@ UpdateMCPServer = Union[UpdateSSEMCPServer, UpdateStdioMCPServer, UpdateStreamab
|
|
|
178
179
|
|
|
179
180
|
# OAuth-related schemas
|
|
180
181
|
class BaseMCPOAuth(LettaBase):
|
|
181
|
-
__id_prefix__ =
|
|
182
|
+
__id_prefix__ = PrimitiveType.MCP_OAUTH.value
|
|
182
183
|
|
|
183
184
|
|
|
184
185
|
class MCPOAuthSession(BaseMCPOAuth):
|
letta/schemas/mcp_server.py
CHANGED
|
@@ -13,12 +13,13 @@ from letta.functions.mcp_client.types import (
|
|
|
13
13
|
StreamableHTTPServerConfig,
|
|
14
14
|
)
|
|
15
15
|
from letta.orm.mcp_oauth import OAuthSessionStatus
|
|
16
|
+
from letta.schemas.enums import PrimitiveType
|
|
16
17
|
from letta.schemas.letta_base import LettaBase
|
|
17
18
|
from letta.schemas.secret import Secret
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class BaseMCPServer(LettaBase):
|
|
21
|
-
__id_prefix__ =
|
|
22
|
+
__id_prefix__ = PrimitiveType.MCP_SERVER.value
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
# Create Schemas (for POST requests)
|
|
@@ -101,7 +102,7 @@ UpdateMCPServerUnion = Union[UpdateStdioMCPServer, UpdateSSEMCPServer, UpdateStr
|
|
|
101
102
|
|
|
102
103
|
# OAuth-related schemas
|
|
103
104
|
class BaseMCPOAuth(LettaBase):
|
|
104
|
-
__id_prefix__ =
|
|
105
|
+
__id_prefix__ = PrimitiveType.MCP_OAUTH.value
|
|
105
106
|
|
|
106
107
|
|
|
107
108
|
class MCPOAuthSession(BaseMCPOAuth):
|
letta/schemas/message.py
CHANGED
|
@@ -13,7 +13,6 @@ from datetime import datetime, timezone
|
|
|
13
13
|
from enum import Enum
|
|
14
14
|
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
|
|
15
15
|
|
|
16
|
-
from letta_client import LettaMessageUnion
|
|
17
16
|
from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall, Function as OpenAIFunction
|
|
18
17
|
from openai.types.responses import ResponseReasoningItem
|
|
19
18
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
@@ -57,6 +56,14 @@ from letta.system import unpack_message
|
|
|
57
56
|
from letta.utils import parse_json, validate_function_response
|
|
58
57
|
|
|
59
58
|
|
|
59
|
+
def truncate_tool_return(content: Optional[str], limit: Optional[int]) -> Optional[str]:
|
|
60
|
+
if limit is None or content is None:
|
|
61
|
+
return content
|
|
62
|
+
if len(content) <= limit:
|
|
63
|
+
return content
|
|
64
|
+
return content[:limit] + f"... [truncated {len(content) - limit} chars]"
|
|
65
|
+
|
|
66
|
+
|
|
60
67
|
def add_inner_thoughts_to_tool_call(
|
|
61
68
|
tool_call: OpenAIToolCall,
|
|
62
69
|
inner_thoughts: str,
|
|
@@ -1091,6 +1098,7 @@ class Message(BaseMessage):
|
|
|
1091
1098
|
# if true, then treat the content field as AssistantMessage
|
|
1092
1099
|
native_content: bool = False,
|
|
1093
1100
|
strip_request_heartbeat: bool = False,
|
|
1101
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
1094
1102
|
) -> dict | None:
|
|
1095
1103
|
"""Go from Message class to ChatCompletion message object"""
|
|
1096
1104
|
assert not (native_content and put_inner_thoughts_in_kwargs), "native_content and put_inner_thoughts_in_kwargs cannot both be true"
|
|
@@ -1139,8 +1147,14 @@ class Message(BaseMessage):
|
|
|
1139
1147
|
assert self.tool_calls is not None or text_content is not None, vars(self)
|
|
1140
1148
|
except AssertionError as e:
|
|
1141
1149
|
# relax check if this message only contains reasoning content
|
|
1142
|
-
if self.content is not None and len(self.content) > 0
|
|
1143
|
-
|
|
1150
|
+
if self.content is not None and len(self.content) > 0:
|
|
1151
|
+
# Check if all non-empty content is reasoning-related
|
|
1152
|
+
all_reasoning = all(
|
|
1153
|
+
isinstance(c, (ReasoningContent, SummarizedReasoningContent, OmittedReasoningContent, RedactedReasoningContent))
|
|
1154
|
+
for c in self.content
|
|
1155
|
+
)
|
|
1156
|
+
if all_reasoning:
|
|
1157
|
+
return None
|
|
1144
1158
|
raise e
|
|
1145
1159
|
|
|
1146
1160
|
# if native content, then put it directly inside the content
|
|
@@ -1181,12 +1195,26 @@ class Message(BaseMessage):
|
|
|
1181
1195
|
tool_call_dict["id"] = tool_call_dict["id"][:max_tool_id_length]
|
|
1182
1196
|
|
|
1183
1197
|
elif self.role == "tool":
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1198
|
+
# Handle tool returns - if tool_returns exists, use the first one
|
|
1199
|
+
if self.tool_returns and len(self.tool_returns) > 0:
|
|
1200
|
+
tool_return = self.tool_returns[0]
|
|
1201
|
+
if not tool_return.tool_call_id:
|
|
1202
|
+
raise TypeError("OpenAI API requires tool_call_id to be set.")
|
|
1203
|
+
func_response = truncate_tool_return(tool_return.func_response, tool_return_truncation_chars)
|
|
1204
|
+
openai_message = {
|
|
1205
|
+
"content": func_response,
|
|
1206
|
+
"role": self.role,
|
|
1207
|
+
"tool_call_id": tool_return.tool_call_id[:max_tool_id_length] if max_tool_id_length else tool_return.tool_call_id,
|
|
1208
|
+
}
|
|
1209
|
+
else:
|
|
1210
|
+
# Legacy fallback for old message format
|
|
1211
|
+
assert self.tool_call_id is not None, vars(self)
|
|
1212
|
+
legacy_content = truncate_tool_return(text_content, tool_return_truncation_chars)
|
|
1213
|
+
openai_message = {
|
|
1214
|
+
"content": legacy_content,
|
|
1215
|
+
"role": self.role,
|
|
1216
|
+
"tool_call_id": self.tool_call_id[:max_tool_id_length] if max_tool_id_length else self.tool_call_id,
|
|
1217
|
+
}
|
|
1190
1218
|
|
|
1191
1219
|
else:
|
|
1192
1220
|
raise ValueError(self.role)
|
|
@@ -1215,22 +1243,42 @@ class Message(BaseMessage):
|
|
|
1215
1243
|
max_tool_id_length: int = TOOL_CALL_ID_MAX_LEN,
|
|
1216
1244
|
put_inner_thoughts_in_kwargs: bool = False,
|
|
1217
1245
|
use_developer_message: bool = False,
|
|
1246
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
1218
1247
|
) -> List[dict]:
|
|
1219
1248
|
messages = Message.filter_messages_for_llm_api(messages)
|
|
1220
|
-
result = [
|
|
1221
|
-
|
|
1249
|
+
result: List[dict] = []
|
|
1250
|
+
|
|
1251
|
+
for m in messages:
|
|
1252
|
+
# Special case: OpenAI Chat Completions requires a separate tool message per tool_call_id
|
|
1253
|
+
# If we have multiple explicit tool_returns on a single Message, expand into one dict per return
|
|
1254
|
+
if m.role == MessageRole.tool and m.tool_returns and len(m.tool_returns) > 0:
|
|
1255
|
+
for tr in m.tool_returns:
|
|
1256
|
+
if not tr.tool_call_id:
|
|
1257
|
+
raise TypeError("ToolReturn came back without a tool_call_id.")
|
|
1258
|
+
result.append(
|
|
1259
|
+
{
|
|
1260
|
+
"content": tr.func_response,
|
|
1261
|
+
"role": "tool",
|
|
1262
|
+
"tool_call_id": tr.tool_call_id[:max_tool_id_length] if max_tool_id_length else tr.tool_call_id,
|
|
1263
|
+
}
|
|
1264
|
+
)
|
|
1265
|
+
continue
|
|
1266
|
+
|
|
1267
|
+
d = m.to_openai_dict(
|
|
1222
1268
|
max_tool_id_length=max_tool_id_length,
|
|
1223
1269
|
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
|
1224
1270
|
use_developer_message=use_developer_message,
|
|
1271
|
+
tool_return_truncation_chars=tool_return_truncation_chars,
|
|
1225
1272
|
)
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1273
|
+
if d is not None:
|
|
1274
|
+
result.append(d)
|
|
1275
|
+
|
|
1229
1276
|
return result
|
|
1230
1277
|
|
|
1231
1278
|
def to_openai_responses_dicts(
|
|
1232
1279
|
self,
|
|
1233
1280
|
max_tool_id_length: int = TOOL_CALL_ID_MAX_LEN,
|
|
1281
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
1234
1282
|
) -> List[dict]:
|
|
1235
1283
|
"""Go from Message class to ChatCompletion message object"""
|
|
1236
1284
|
|
|
@@ -1306,15 +1354,31 @@ class Message(BaseMessage):
|
|
|
1306
1354
|
)
|
|
1307
1355
|
|
|
1308
1356
|
elif self.role == "tool":
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1357
|
+
# Handle tool returns - similar pattern to Anthropic
|
|
1358
|
+
if self.tool_returns:
|
|
1359
|
+
for tool_return in self.tool_returns:
|
|
1360
|
+
if not tool_return.tool_call_id:
|
|
1361
|
+
raise TypeError("OpenAI Responses API requires tool_call_id to be set.")
|
|
1362
|
+
func_response = truncate_tool_return(tool_return.func_response, tool_return_truncation_chars)
|
|
1363
|
+
message_dicts.append(
|
|
1364
|
+
{
|
|
1365
|
+
"type": "function_call_output",
|
|
1366
|
+
"call_id": tool_return.tool_call_id[:max_tool_id_length] if max_tool_id_length else tool_return.tool_call_id,
|
|
1367
|
+
"output": func_response,
|
|
1368
|
+
}
|
|
1369
|
+
)
|
|
1370
|
+
else:
|
|
1371
|
+
# Legacy fallback for old message format
|
|
1372
|
+
assert self.tool_call_id is not None, vars(self)
|
|
1373
|
+
assert len(self.content) == 1 and isinstance(self.content[0], TextContent), vars(self)
|
|
1374
|
+
legacy_output = truncate_tool_return(self.content[0].text, tool_return_truncation_chars)
|
|
1375
|
+
message_dicts.append(
|
|
1376
|
+
{
|
|
1377
|
+
"type": "function_call_output",
|
|
1378
|
+
"call_id": self.tool_call_id[:max_tool_id_length] if max_tool_id_length else self.tool_call_id,
|
|
1379
|
+
"output": legacy_output,
|
|
1380
|
+
}
|
|
1381
|
+
)
|
|
1318
1382
|
|
|
1319
1383
|
else:
|
|
1320
1384
|
raise ValueError(self.role)
|
|
@@ -1325,11 +1389,16 @@ class Message(BaseMessage):
|
|
|
1325
1389
|
def to_openai_responses_dicts_from_list(
|
|
1326
1390
|
messages: List[Message],
|
|
1327
1391
|
max_tool_id_length: int = TOOL_CALL_ID_MAX_LEN,
|
|
1392
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
1328
1393
|
) -> List[dict]:
|
|
1329
1394
|
messages = Message.filter_messages_for_llm_api(messages)
|
|
1330
1395
|
result = []
|
|
1331
1396
|
for message in messages:
|
|
1332
|
-
result.extend(
|
|
1397
|
+
result.extend(
|
|
1398
|
+
message.to_openai_responses_dicts(
|
|
1399
|
+
max_tool_id_length=max_tool_id_length, tool_return_truncation_chars=tool_return_truncation_chars
|
|
1400
|
+
)
|
|
1401
|
+
)
|
|
1333
1402
|
return result
|
|
1334
1403
|
|
|
1335
1404
|
def to_anthropic_dict(
|
|
@@ -1340,6 +1409,7 @@ class Message(BaseMessage):
|
|
|
1340
1409
|
# if true, then treat the content field as AssistantMessage
|
|
1341
1410
|
native_content: bool = False,
|
|
1342
1411
|
strip_request_heartbeat: bool = False,
|
|
1412
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
1343
1413
|
) -> dict | None:
|
|
1344
1414
|
"""
|
|
1345
1415
|
Convert to an Anthropic message dictionary
|
|
@@ -1515,11 +1585,12 @@ class Message(BaseMessage):
|
|
|
1515
1585
|
for tool_return in self.tool_returns:
|
|
1516
1586
|
if not tool_return.tool_call_id:
|
|
1517
1587
|
raise TypeError("Anthropic API requires tool_use_id to be set.")
|
|
1588
|
+
func_response = truncate_tool_return(tool_return.func_response, tool_return_truncation_chars)
|
|
1518
1589
|
content.append(
|
|
1519
1590
|
{
|
|
1520
1591
|
"type": "tool_result",
|
|
1521
1592
|
"tool_use_id": tool_return.tool_call_id,
|
|
1522
|
-
"content":
|
|
1593
|
+
"content": func_response,
|
|
1523
1594
|
}
|
|
1524
1595
|
)
|
|
1525
1596
|
if content:
|
|
@@ -1532,6 +1603,7 @@ class Message(BaseMessage):
|
|
|
1532
1603
|
raise TypeError("Anthropic API requires tool_use_id to be set.")
|
|
1533
1604
|
|
|
1534
1605
|
# This is for legacy reasons
|
|
1606
|
+
legacy_content = truncate_tool_return(text_content, tool_return_truncation_chars)
|
|
1535
1607
|
anthropic_message = {
|
|
1536
1608
|
"role": "user", # NOTE: diff
|
|
1537
1609
|
"content": [
|
|
@@ -1539,7 +1611,7 @@ class Message(BaseMessage):
|
|
|
1539
1611
|
{
|
|
1540
1612
|
"type": "tool_result",
|
|
1541
1613
|
"tool_use_id": self.tool_call_id,
|
|
1542
|
-
"content":
|
|
1614
|
+
"content": legacy_content,
|
|
1543
1615
|
}
|
|
1544
1616
|
],
|
|
1545
1617
|
}
|
|
@@ -1558,6 +1630,7 @@ class Message(BaseMessage):
|
|
|
1558
1630
|
# if true, then treat the content field as AssistantMessage
|
|
1559
1631
|
native_content: bool = False,
|
|
1560
1632
|
strip_request_heartbeat: bool = False,
|
|
1633
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
1561
1634
|
) -> List[dict]:
|
|
1562
1635
|
messages = Message.filter_messages_for_llm_api(messages)
|
|
1563
1636
|
result = [
|
|
@@ -1567,6 +1640,7 @@ class Message(BaseMessage):
|
|
|
1567
1640
|
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
|
1568
1641
|
native_content=native_content,
|
|
1569
1642
|
strip_request_heartbeat=strip_request_heartbeat,
|
|
1643
|
+
tool_return_truncation_chars=tool_return_truncation_chars,
|
|
1570
1644
|
)
|
|
1571
1645
|
for m in messages
|
|
1572
1646
|
]
|
|
@@ -1580,6 +1654,7 @@ class Message(BaseMessage):
|
|
|
1580
1654
|
# if true, then treat the content field as AssistantMessage
|
|
1581
1655
|
native_content: bool = False,
|
|
1582
1656
|
strip_request_heartbeat: bool = False,
|
|
1657
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
1583
1658
|
) -> dict | None:
|
|
1584
1659
|
"""
|
|
1585
1660
|
Go from Message class to Google AI REST message object
|
|
@@ -1717,34 +1792,75 @@ class Message(BaseMessage):
|
|
|
1717
1792
|
|
|
1718
1793
|
elif self.role == "tool":
|
|
1719
1794
|
# NOTE: Significantly different tool calling format, more similar to function calling format
|
|
1720
|
-
assert self.tool_call_id is not None, vars(self)
|
|
1721
1795
|
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1796
|
+
# Handle tool returns - similar pattern to Anthropic
|
|
1797
|
+
if self.tool_returns:
|
|
1798
|
+
parts = []
|
|
1799
|
+
for tool_return in self.tool_returns:
|
|
1800
|
+
if not tool_return.tool_call_id:
|
|
1801
|
+
raise TypeError("Google AI API requires tool_call_id to be set.")
|
|
1802
|
+
|
|
1803
|
+
# Use the function name if available, otherwise use tool_call_id
|
|
1804
|
+
function_name = self.name if self.name else tool_return.tool_call_id
|
|
1805
|
+
|
|
1806
|
+
# Truncate the tool return if needed
|
|
1807
|
+
func_response = truncate_tool_return(tool_return.func_response, tool_return_truncation_chars)
|
|
1808
|
+
|
|
1809
|
+
# NOTE: Google AI API wants the function response as JSON only, no string
|
|
1810
|
+
try:
|
|
1811
|
+
function_response = parse_json(func_response)
|
|
1812
|
+
except:
|
|
1813
|
+
function_response = {"function_response": func_response}
|
|
1814
|
+
|
|
1815
|
+
parts.append(
|
|
1816
|
+
{
|
|
1817
|
+
"functionResponse": {
|
|
1818
|
+
"name": function_name,
|
|
1819
|
+
"response": {
|
|
1820
|
+
"name": function_name, # NOTE: name twice... why?
|
|
1821
|
+
"content": function_response,
|
|
1822
|
+
},
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
)
|
|
1826
|
+
|
|
1827
|
+
google_ai_message = {
|
|
1828
|
+
"role": "function",
|
|
1829
|
+
"parts": parts,
|
|
1830
|
+
}
|
|
1725
1831
|
else:
|
|
1726
|
-
|
|
1832
|
+
# Legacy fallback for old message format
|
|
1833
|
+
assert self.tool_call_id is not None, vars(self)
|
|
1727
1834
|
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1835
|
+
if self.name is None:
|
|
1836
|
+
logger.warning("Couldn't find function name on tool call, defaulting to tool ID instead.")
|
|
1837
|
+
function_name = self.tool_call_id
|
|
1838
|
+
else:
|
|
1839
|
+
function_name = self.name
|
|
1733
1840
|
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1841
|
+
# Truncate the legacy content if needed
|
|
1842
|
+
legacy_content = truncate_tool_return(text_content, tool_return_truncation_chars)
|
|
1843
|
+
|
|
1844
|
+
# NOTE: Google AI API wants the function response as JSON only, no string
|
|
1845
|
+
try:
|
|
1846
|
+
function_response = parse_json(legacy_content)
|
|
1847
|
+
except:
|
|
1848
|
+
function_response = {"function_response": legacy_content}
|
|
1849
|
+
|
|
1850
|
+
google_ai_message = {
|
|
1851
|
+
"role": "function",
|
|
1852
|
+
"parts": [
|
|
1853
|
+
{
|
|
1854
|
+
"functionResponse": {
|
|
1855
|
+
"name": function_name,
|
|
1856
|
+
"response": {
|
|
1857
|
+
"name": function_name, # NOTE: name twice... why?
|
|
1858
|
+
"content": function_response,
|
|
1859
|
+
},
|
|
1860
|
+
}
|
|
1744
1861
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
}
|
|
1862
|
+
],
|
|
1863
|
+
}
|
|
1748
1864
|
|
|
1749
1865
|
else:
|
|
1750
1866
|
raise ValueError(self.role)
|
|
@@ -1765,6 +1881,7 @@ class Message(BaseMessage):
|
|
|
1765
1881
|
current_model: str,
|
|
1766
1882
|
put_inner_thoughts_in_kwargs: bool = True,
|
|
1767
1883
|
native_content: bool = False,
|
|
1884
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
1768
1885
|
):
|
|
1769
1886
|
messages = Message.filter_messages_for_llm_api(messages)
|
|
1770
1887
|
result = [
|
|
@@ -1772,6 +1889,7 @@ class Message(BaseMessage):
|
|
|
1772
1889
|
current_model=current_model,
|
|
1773
1890
|
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
|
1774
1891
|
native_content=native_content,
|
|
1892
|
+
tool_return_truncation_chars=tool_return_truncation_chars,
|
|
1775
1893
|
)
|
|
1776
1894
|
for m in messages
|
|
1777
1895
|
]
|