lm-deluge 0.0.78__py3-none-any.whl → 0.0.80__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.
- lm_deluge/api_requests/anthropic.py +43 -16
- lm_deluge/api_requests/gemini.py +95 -15
- lm_deluge/client.py +6 -5
- lm_deluge/config.py +10 -1
- lm_deluge/llm_tools/sandbox.py +523 -0
- lm_deluge/models/anthropic.py +15 -0
- lm_deluge/models/google.py +15 -0
- lm_deluge/models/openrouter.py +10 -0
- lm_deluge/prompt.py +62 -24
- lm_deluge/warnings.py +4 -0
- {lm_deluge-0.0.78.dist-info → lm_deluge-0.0.80.dist-info}/METADATA +8 -8
- {lm_deluge-0.0.78.dist-info → lm_deluge-0.0.80.dist-info}/RECORD +15 -14
- {lm_deluge-0.0.78.dist-info → lm_deluge-0.0.80.dist-info}/WHEEL +0 -0
- {lm_deluge-0.0.78.dist-info → lm_deluge-0.0.80.dist-info}/licenses/LICENSE +0 -0
- {lm_deluge-0.0.78.dist-info → lm_deluge-0.0.80.dist-info}/top_level.txt +0 -0
|
@@ -16,6 +16,7 @@ from lm_deluge.util.schema import (
|
|
|
16
16
|
prepare_output_schema,
|
|
17
17
|
transform_schema_for_anthropic,
|
|
18
18
|
)
|
|
19
|
+
from lm_deluge.warnings import maybe_warn
|
|
19
20
|
|
|
20
21
|
from ..models import APIModel
|
|
21
22
|
from .base import APIRequestBase, APIResponse
|
|
@@ -62,20 +63,45 @@ def _build_anthropic_request(
|
|
|
62
63
|
"max_tokens": sampling_params.max_new_tokens,
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
if model.id == "claude-4.5-opus" and sampling_params.global_effort:
|
|
67
|
+
request_json["effort"] = sampling_params.global_effort
|
|
68
|
+
_add_beta(base_headers, "effort-2025-11-24")
|
|
69
|
+
|
|
65
70
|
# handle thinking
|
|
66
|
-
if model.reasoning_model
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
sampling_params.reasoning_effort
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
71
|
+
if model.reasoning_model:
|
|
72
|
+
if (
|
|
73
|
+
sampling_params.thinking_budget is not None
|
|
74
|
+
and sampling_params.reasoning_effort is not None
|
|
75
|
+
):
|
|
76
|
+
maybe_warn("WARN_THINKING_BUDGET_AND_REASONING_EFFORT")
|
|
77
|
+
|
|
78
|
+
if sampling_params.thinking_budget is not None:
|
|
79
|
+
budget = sampling_params.thinking_budget
|
|
80
|
+
elif sampling_params.reasoning_effort is not None:
|
|
81
|
+
# translate reasoning effort of low, medium, high to budget tokens
|
|
82
|
+
budget = {
|
|
83
|
+
"none": 0,
|
|
84
|
+
"minimal": 256,
|
|
85
|
+
"low": 1024,
|
|
86
|
+
"medium": 4096,
|
|
87
|
+
"high": 16384,
|
|
88
|
+
}.get(sampling_params.reasoning_effort)
|
|
89
|
+
assert isinstance(budget, int)
|
|
90
|
+
else:
|
|
91
|
+
budget = 0
|
|
92
|
+
|
|
93
|
+
if budget > 0:
|
|
94
|
+
request_json["thinking"] = {
|
|
95
|
+
"type": "enabled",
|
|
96
|
+
"budget_tokens": budget,
|
|
97
|
+
}
|
|
98
|
+
if "top_p" in request_json:
|
|
99
|
+
request_json["top_p"] = max(request_json["top_p"], 0.95)
|
|
100
|
+
request_json["temperature"] = 1.0
|
|
101
|
+
request_json["max_tokens"] += budget
|
|
102
|
+
else:
|
|
103
|
+
request_json["thinking"] = {"type": "disabled"}
|
|
104
|
+
|
|
79
105
|
else:
|
|
80
106
|
request_json["thinking"] = {"type": "disabled"}
|
|
81
107
|
if sampling_params.reasoning_effort:
|
|
@@ -83,10 +109,11 @@ def _build_anthropic_request(
|
|
|
83
109
|
if system_message is not None:
|
|
84
110
|
request_json["system"] = system_message
|
|
85
111
|
|
|
86
|
-
# handle temp + top_p for opus 4.1/sonnet 4.5
|
|
112
|
+
# handle temp + top_p for opus 4.1/sonnet 4.5.
|
|
113
|
+
# TODO: make clearer / more user-friendly so there can be NotGiven
|
|
114
|
+
# and user can control which one they want to use
|
|
87
115
|
if "4-1" in model.name or "4-5" in model.name:
|
|
88
|
-
|
|
89
|
-
request_json.pop("top_p")
|
|
116
|
+
request_json.pop("top_p")
|
|
90
117
|
|
|
91
118
|
# Handle structured outputs (output_format)
|
|
92
119
|
if context.output_schema:
|
lm_deluge/api_requests/gemini.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
-
from typing import Any
|
|
4
3
|
|
|
5
4
|
from aiohttp import ClientResponse
|
|
6
5
|
|
|
@@ -23,6 +22,21 @@ async def _build_gemini_request(
|
|
|
23
22
|
) -> dict:
|
|
24
23
|
system_message, messages = prompt.to_gemini()
|
|
25
24
|
|
|
25
|
+
# For Gemini 3, inject dummy signatures when missing for function calls
|
|
26
|
+
is_gemini_3 = "gemini-3" in model.name.lower()
|
|
27
|
+
if is_gemini_3:
|
|
28
|
+
dummy_sig = "context_engineering_is_the_way_to_go"
|
|
29
|
+
for msg in messages:
|
|
30
|
+
if "parts" in msg:
|
|
31
|
+
for part in msg["parts"]:
|
|
32
|
+
# For function calls, inject dummy signature if missing
|
|
33
|
+
if "functionCall" in part and "thoughtSignature" not in part:
|
|
34
|
+
part["thoughtSignature"] = dummy_sig
|
|
35
|
+
maybe_warn(
|
|
36
|
+
"WARN_GEMINI3_MISSING_SIGNATURE",
|
|
37
|
+
part_type="function call",
|
|
38
|
+
)
|
|
39
|
+
|
|
26
40
|
request_json = {
|
|
27
41
|
"contents": messages,
|
|
28
42
|
"generationConfig": {
|
|
@@ -37,20 +51,61 @@ async def _build_gemini_request(
|
|
|
37
51
|
request_json["systemInstruction"] = {"parts": [{"text": system_message}]}
|
|
38
52
|
|
|
39
53
|
# Handle reasoning models (thinking)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
thinking_config = {"includeThoughts": False, "thinkingBudget": budget}
|
|
54
|
+
is_gemini_3 = "gemini-3" in model.name.lower()
|
|
55
|
+
if is_gemini_3:
|
|
56
|
+
# gemini3 MUST think
|
|
57
|
+
if not sampling_params.reasoning_effort:
|
|
58
|
+
maybe_warn("WARN_GEMINI3_NO_REASONING")
|
|
59
|
+
effort = "low"
|
|
47
60
|
else:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
level_map = {
|
|
62
|
+
"none": "low",
|
|
63
|
+
"minimal": "low",
|
|
64
|
+
"low": "low",
|
|
65
|
+
"medium": "high", # change when supported
|
|
66
|
+
"high": "high",
|
|
67
|
+
}
|
|
68
|
+
effort = level_map[sampling_params.reasoning_effort]
|
|
69
|
+
thinking_config = {"thinkingLevel": effort}
|
|
70
|
+
request_json["generationConfig"]["thinkingConfig"] = thinking_config
|
|
71
|
+
|
|
72
|
+
elif model.reasoning_model:
|
|
73
|
+
if (
|
|
74
|
+
sampling_params.thinking_budget is not None
|
|
75
|
+
and sampling_params.reasoning_effort is not None
|
|
76
|
+
):
|
|
77
|
+
maybe_warn("WARN_THINKING_BUDGET_AND_REASONING_EFFORT")
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
sampling_params.thinking_budget is not None
|
|
81
|
+
and sampling_params.thinking_budget > 0
|
|
82
|
+
):
|
|
83
|
+
thinking_config = {
|
|
84
|
+
"includeThoughts": True,
|
|
85
|
+
"thinkingBudget": sampling_params.thinking_budget,
|
|
86
|
+
}
|
|
87
|
+
elif sampling_params.thinking_budget == -1:
|
|
88
|
+
# dynamic thinking
|
|
89
|
+
thinking_config = {"includeThoughts": True, "thinkingBudget": -1}
|
|
90
|
+
elif sampling_params.reasoning_effort not in [None, "none"]:
|
|
91
|
+
level_map = {
|
|
92
|
+
"minimal": 256,
|
|
93
|
+
"low": 1024,
|
|
94
|
+
"medium": 4096,
|
|
95
|
+
"high": 16384,
|
|
96
|
+
}
|
|
97
|
+
assert sampling_params.reasoning_effort in level_map
|
|
98
|
+
budget = level_map[sampling_params.reasoning_effort]
|
|
99
|
+
if "flash-lite" in model.id:
|
|
100
|
+
budget = max(budget, 512)
|
|
101
|
+
thinking_config = {"includeThoughts": True, "thinkingBudget": budget}
|
|
102
|
+
elif "2.5-pro" in model.id:
|
|
103
|
+
# 2.5 pro must think.
|
|
104
|
+
thinking_config = {"includeThoughts": True, "thinkingBudget": 128}
|
|
105
|
+
else:
|
|
106
|
+
# no thoughts head empty
|
|
107
|
+
thinking_config = {"includeThoughts": False, "thinkingBudget": 0}
|
|
108
|
+
|
|
54
109
|
request_json["generationConfig"]["thinkingConfig"] = thinking_config
|
|
55
110
|
|
|
56
111
|
else:
|
|
@@ -66,6 +121,21 @@ async def _build_gemini_request(
|
|
|
66
121
|
if sampling_params.json_mode and model.supports_json:
|
|
67
122
|
request_json["generationConfig"]["responseMimeType"] = "application/json"
|
|
68
123
|
|
|
124
|
+
# Handle media_resolution for Gemini 3 (requires v1alpha)
|
|
125
|
+
if sampling_params.media_resolution is not None:
|
|
126
|
+
is_gemini_3 = "gemini-3" in model.name.lower()
|
|
127
|
+
if is_gemini_3:
|
|
128
|
+
# Add global media resolution to generationConfig
|
|
129
|
+
request_json["generationConfig"]["mediaResolution"] = {
|
|
130
|
+
"level": sampling_params.media_resolution
|
|
131
|
+
}
|
|
132
|
+
else:
|
|
133
|
+
# Warn if trying to use media_resolution on non-Gemini-3 models
|
|
134
|
+
maybe_warn(
|
|
135
|
+
"WARN_MEDIA_RESOLUTION_UNSUPPORTED",
|
|
136
|
+
model_name=model.name,
|
|
137
|
+
)
|
|
138
|
+
|
|
69
139
|
return request_json
|
|
70
140
|
|
|
71
141
|
|
|
@@ -137,10 +207,19 @@ class GeminiRequest(APIRequestBase):
|
|
|
137
207
|
candidate = data["candidates"][0]
|
|
138
208
|
if "content" in candidate and "parts" in candidate["content"]:
|
|
139
209
|
for part in candidate["content"]["parts"]:
|
|
210
|
+
# Extract thought signature if present
|
|
211
|
+
thought_sig = part.get("thoughtSignature")
|
|
212
|
+
|
|
140
213
|
if "text" in part:
|
|
141
214
|
parts.append(Text(part["text"]))
|
|
142
215
|
elif "thought" in part:
|
|
143
|
-
|
|
216
|
+
# Thought with optional signature
|
|
217
|
+
parts.append(
|
|
218
|
+
Thinking(
|
|
219
|
+
content=part["thought"],
|
|
220
|
+
thought_signature=thought_sig,
|
|
221
|
+
)
|
|
222
|
+
)
|
|
144
223
|
elif "functionCall" in part:
|
|
145
224
|
func_call = part["functionCall"]
|
|
146
225
|
# Generate a unique ID since Gemini doesn't provide one
|
|
@@ -152,6 +231,7 @@ class GeminiRequest(APIRequestBase):
|
|
|
152
231
|
id=tool_id,
|
|
153
232
|
name=func_call["name"],
|
|
154
233
|
arguments=func_call.get("args", {}),
|
|
234
|
+
thought_signature=thought_sig,
|
|
155
235
|
)
|
|
156
236
|
)
|
|
157
237
|
|
lm_deluge/client.py
CHANGED
|
@@ -79,7 +79,7 @@ class _LLMClient(BaseModel):
|
|
|
79
79
|
background: bool = False
|
|
80
80
|
# sampling params - if provided, and sampling_params is not,
|
|
81
81
|
# these override the defaults
|
|
82
|
-
temperature: float = 0
|
|
82
|
+
temperature: float = 1.0
|
|
83
83
|
top_p: float = 1.0
|
|
84
84
|
json_mode: bool = False
|
|
85
85
|
max_new_tokens: int = 512
|
|
@@ -262,6 +262,7 @@ class _LLMClient(BaseModel):
|
|
|
262
262
|
self.max_tokens_per_minute = max_tokens_per_minute
|
|
263
263
|
if max_concurrent_requests:
|
|
264
264
|
self.max_concurrent_requests = max_concurrent_requests
|
|
265
|
+
return self
|
|
265
266
|
|
|
266
267
|
def _get_tracker(self) -> StatusTracker:
|
|
267
268
|
if self._tracker is None:
|
|
@@ -336,7 +337,7 @@ class _LLMClient(BaseModel):
|
|
|
336
337
|
if "sampling_params" not in data or len(data.get("sampling_params", [])) == 0:
|
|
337
338
|
data["sampling_params"] = [
|
|
338
339
|
SamplingParams(
|
|
339
|
-
temperature=data.get("temperature", 0
|
|
340
|
+
temperature=data.get("temperature", 1.0),
|
|
340
341
|
top_p=data.get("top_p", 1.0),
|
|
341
342
|
json_mode=data.get("json_mode", False),
|
|
342
343
|
max_new_tokens=data.get("max_new_tokens", 512),
|
|
@@ -1066,7 +1067,7 @@ def LLMClient(
|
|
|
1066
1067
|
extra_headers: dict[str, str] | None = None,
|
|
1067
1068
|
use_responses_api: bool = False,
|
|
1068
1069
|
background: bool = False,
|
|
1069
|
-
temperature: float = 0
|
|
1070
|
+
temperature: float = 1.0,
|
|
1070
1071
|
top_p: float = 1.0,
|
|
1071
1072
|
json_mode: bool = False,
|
|
1072
1073
|
max_new_tokens: int = 512,
|
|
@@ -1095,7 +1096,7 @@ def LLMClient(
|
|
|
1095
1096
|
extra_headers: dict[str, str] | None = None,
|
|
1096
1097
|
use_responses_api: bool = False,
|
|
1097
1098
|
background: bool = False,
|
|
1098
|
-
temperature: float = 0
|
|
1099
|
+
temperature: float = 1.0,
|
|
1099
1100
|
top_p: float = 1.0,
|
|
1100
1101
|
json_mode: bool = False,
|
|
1101
1102
|
max_new_tokens: int = 512,
|
|
@@ -1123,7 +1124,7 @@ def LLMClient(
|
|
|
1123
1124
|
extra_headers: dict[str, str] | None = None,
|
|
1124
1125
|
use_responses_api: bool = False,
|
|
1125
1126
|
background: bool = False,
|
|
1126
|
-
temperature: float = 0
|
|
1127
|
+
temperature: float = 1.0,
|
|
1127
1128
|
top_p: float = 1.0,
|
|
1128
1129
|
json_mode: bool = False,
|
|
1129
1130
|
max_new_tokens: int = 512,
|
lm_deluge/config.py
CHANGED
|
@@ -4,14 +4,23 @@ from pydantic import BaseModel
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class SamplingParams(BaseModel):
|
|
7
|
-
temperature: float =
|
|
7
|
+
temperature: float = 1.0 # more typical for new models
|
|
8
8
|
top_p: float = 1.0
|
|
9
9
|
json_mode: bool = False
|
|
10
10
|
max_new_tokens: int = 2_048
|
|
11
|
+
global_effort: Literal["low", "medium", "high"] = "high" # for opus-4.5
|
|
11
12
|
reasoning_effort: Literal["low", "medium", "high", "minimal", "none", None] = None
|
|
13
|
+
thinking_budget: int | None = None
|
|
12
14
|
logprobs: bool = False
|
|
13
15
|
top_logprobs: int | None = None
|
|
14
16
|
strict_tools: bool = True
|
|
17
|
+
# Gemini 3 only - controls multimodal vision processing fidelity
|
|
18
|
+
media_resolution: (
|
|
19
|
+
Literal[
|
|
20
|
+
"media_resolution_low", "media_resolution_medium", "media_resolution_high"
|
|
21
|
+
]
|
|
22
|
+
| None
|
|
23
|
+
) = None
|
|
15
24
|
|
|
16
25
|
def to_vllm(self):
|
|
17
26
|
try:
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import secrets
|
|
2
|
+
|
|
3
|
+
from lm_deluge.tool import Tool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ModalSandbox:
|
|
7
|
+
def __init__(self, app_name: str | None = None, *, block_network: bool = False):
|
|
8
|
+
import modal
|
|
9
|
+
|
|
10
|
+
app_name = app_name or secrets.token_urlsafe(32)
|
|
11
|
+
app = modal.App.lookup(app_name, create_if_missing=True)
|
|
12
|
+
self.app = app
|
|
13
|
+
self.block_network = block_network
|
|
14
|
+
self.sb = modal.Sandbox.create(app=app, block_network=block_network)
|
|
15
|
+
self.last_process = None
|
|
16
|
+
self._destroyed = False
|
|
17
|
+
|
|
18
|
+
def __enter__(self):
|
|
19
|
+
"""Synchronous context manager entry (use async with for async support)."""
|
|
20
|
+
return self
|
|
21
|
+
|
|
22
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
23
|
+
"""Synchronous context manager exit - cleanup sandbox."""
|
|
24
|
+
if not self._destroyed:
|
|
25
|
+
self._destroy()
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
def __del__(self):
|
|
29
|
+
"""Cleanup sandbox when garbage collected (backup cleanup)."""
|
|
30
|
+
if not self._destroyed:
|
|
31
|
+
try:
|
|
32
|
+
self._destroy()
|
|
33
|
+
except Exception:
|
|
34
|
+
# Ignore errors during cleanup in __del__
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
async def _safe_read(process, max_lines: int = 25, max_chars: int = 2500):
|
|
39
|
+
result = await process.stdout.read.aio()
|
|
40
|
+
|
|
41
|
+
if len(result) > max_chars:
|
|
42
|
+
result = result[-max_chars:]
|
|
43
|
+
|
|
44
|
+
lines = result.splitlines()
|
|
45
|
+
lines = lines[-max_lines:]
|
|
46
|
+
|
|
47
|
+
return "\n".join(lines)
|
|
48
|
+
|
|
49
|
+
async def _exec(
|
|
50
|
+
self, cmd: list[str], timeout: int = 5, check: bool = False
|
|
51
|
+
) -> str | None:
|
|
52
|
+
process = await self.sb.exec.aio(*cmd, timeout=timeout)
|
|
53
|
+
self.last_process = process
|
|
54
|
+
if check:
|
|
55
|
+
return await self._safe_read(process)
|
|
56
|
+
|
|
57
|
+
async def _read(self, limit: int = 25):
|
|
58
|
+
if not self.last_process:
|
|
59
|
+
return None
|
|
60
|
+
return await self._safe_read(self.last_process)
|
|
61
|
+
|
|
62
|
+
def _get_credentials(self):
|
|
63
|
+
if self.block_network:
|
|
64
|
+
return None
|
|
65
|
+
creds = self.sb.create_connect_token(user_metadata={"user_id": "foo"})
|
|
66
|
+
|
|
67
|
+
return creds # f"URL: {creds.url}; Token: {creds.token}"
|
|
68
|
+
|
|
69
|
+
def _destroy(self):
|
|
70
|
+
"""Destroy the sandbox and mark as destroyed."""
|
|
71
|
+
if not self._destroyed:
|
|
72
|
+
self.sb.terminate()
|
|
73
|
+
self._destroyed = True
|
|
74
|
+
|
|
75
|
+
def get_tools(self):
|
|
76
|
+
bash_tool = Tool(
|
|
77
|
+
name="bash",
|
|
78
|
+
description=(
|
|
79
|
+
"Execute a bash command in the sandbox environment. "
|
|
80
|
+
"Provide the command as a list of strings (e.g., ['ls', '-la']). "
|
|
81
|
+
"Optionally set a timeout in seconds and check=True to immediately read the output."
|
|
82
|
+
),
|
|
83
|
+
run=self._exec,
|
|
84
|
+
parameters={
|
|
85
|
+
"cmd": {
|
|
86
|
+
"type": "array",
|
|
87
|
+
"description": "The command to execute as a list of strings (e.g., ['python', 'script.py'])",
|
|
88
|
+
"items": {"type": "string"},
|
|
89
|
+
},
|
|
90
|
+
"timeout": {
|
|
91
|
+
"type": "integer",
|
|
92
|
+
"description": "Timeout in seconds for the command execution (default: 5)",
|
|
93
|
+
},
|
|
94
|
+
"check": {
|
|
95
|
+
"type": "boolean",
|
|
96
|
+
"description": "If true, immediately read and return the last line of stdout (default: false)",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
required=["cmd"],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
stdout_tool = Tool(
|
|
103
|
+
name="read_stdout",
|
|
104
|
+
description=(
|
|
105
|
+
"Read the most recent stdout output from the bash shell. "
|
|
106
|
+
"ONLY returns stdout from the most recent command, "
|
|
107
|
+
"cannot be used to get output from previous commands. "
|
|
108
|
+
"Returns the last `limit` lines of stdout (default: 25 lines)."
|
|
109
|
+
),
|
|
110
|
+
run=self._read,
|
|
111
|
+
parameters={
|
|
112
|
+
"limit": {
|
|
113
|
+
"type": "integer",
|
|
114
|
+
"description": "Maximum number of recent lines to return (default: 25)",
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
required=[],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
tunnel_tool = Tool(
|
|
121
|
+
name="tunnel",
|
|
122
|
+
description=(
|
|
123
|
+
"Opens a tunnel on port 8080 and returns a URL and token to connect to it. "
|
|
124
|
+
"Useful for exposing a local server or application to the user. "
|
|
125
|
+
"Only works when network is enabled (block_network=False)."
|
|
126
|
+
),
|
|
127
|
+
run=self._get_credentials,
|
|
128
|
+
parameters={},
|
|
129
|
+
required=[],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return [bash_tool, stdout_tool, tunnel_tool]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class DaytonaSandbox:
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
api_key: str | None = None,
|
|
139
|
+
api_url: str | None = None,
|
|
140
|
+
target: str | None = None,
|
|
141
|
+
sandbox_id: str | None = None,
|
|
142
|
+
language: str = "python",
|
|
143
|
+
auto_start: bool = True,
|
|
144
|
+
):
|
|
145
|
+
"""
|
|
146
|
+
Initialize a Daytona sandbox.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
api_key: Daytona API key (if None, will look for DAYTONA_API_KEY env var)
|
|
150
|
+
api_url: Daytona API URL (if None, will look for DAYTONA_API_URL env var)
|
|
151
|
+
target: Daytona target (if None, will look for DAYTONA_TARGET env var)
|
|
152
|
+
sandbox_id: ID of existing sandbox to connect to (if None, creates a new one)
|
|
153
|
+
language: Programming language for the sandbox (default: python)
|
|
154
|
+
auto_start: Whether to automatically start the sandbox if stopped
|
|
155
|
+
"""
|
|
156
|
+
import os
|
|
157
|
+
|
|
158
|
+
self.api_key = api_key or os.getenv("DAYTONA_API_KEY")
|
|
159
|
+
self.api_url = api_url or os.getenv("DAYTONA_API_URL")
|
|
160
|
+
self.target = target or os.getenv("DAYTONA_TARGET")
|
|
161
|
+
self.sandbox_id = sandbox_id
|
|
162
|
+
self.language = language
|
|
163
|
+
self.auto_start = auto_start
|
|
164
|
+
self.sandbox = None
|
|
165
|
+
self.client = None
|
|
166
|
+
self._initialized = False
|
|
167
|
+
self._destroyed = False
|
|
168
|
+
|
|
169
|
+
async def __aenter__(self):
|
|
170
|
+
"""Async context manager entry - initialize sandbox."""
|
|
171
|
+
await self._ensure_initialized()
|
|
172
|
+
return self
|
|
173
|
+
|
|
174
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
175
|
+
"""Async context manager exit - cleanup sandbox."""
|
|
176
|
+
if not self._destroyed:
|
|
177
|
+
await self._destroy()
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
def __del__(self):
|
|
181
|
+
"""Cleanup sandbox when garbage collected (backup cleanup).
|
|
182
|
+
|
|
183
|
+
Note: This attempts sync cleanup which may not work perfectly for async resources.
|
|
184
|
+
Prefer using 'async with' for guaranteed cleanup.
|
|
185
|
+
"""
|
|
186
|
+
if not self._destroyed and self.sandbox:
|
|
187
|
+
import warnings
|
|
188
|
+
|
|
189
|
+
warnings.warn(
|
|
190
|
+
"DaytonaSandbox was not properly cleaned up. "
|
|
191
|
+
"Use 'async with DaytonaSandbox(...) as sandbox:' for automatic cleanup.",
|
|
192
|
+
ResourceWarning,
|
|
193
|
+
stacklevel=2,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
async def _ensure_initialized(self):
|
|
197
|
+
"""Lazy initialization of sandbox"""
|
|
198
|
+
if self._initialized:
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
from daytona_sdk import ( # type: ignore
|
|
202
|
+
AsyncDaytona,
|
|
203
|
+
CreateSandboxBaseParams,
|
|
204
|
+
DaytonaConfig,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Initialize client with config
|
|
208
|
+
if self.api_key or self.api_url or self.target:
|
|
209
|
+
config = DaytonaConfig(
|
|
210
|
+
api_key=self.api_key, api_url=self.api_url, target=self.target
|
|
211
|
+
)
|
|
212
|
+
self.client = AsyncDaytona(config)
|
|
213
|
+
else:
|
|
214
|
+
# Use environment variables
|
|
215
|
+
self.client = AsyncDaytona()
|
|
216
|
+
|
|
217
|
+
if self.sandbox_id:
|
|
218
|
+
# Connect to existing sandbox - use find_one with id label
|
|
219
|
+
sandboxes = await self.client.list(labels={"id": self.sandbox_id})
|
|
220
|
+
if not sandboxes or not sandboxes.items:
|
|
221
|
+
raise ValueError(f"Sandbox with ID {self.sandbox_id} not found")
|
|
222
|
+
self.sandbox = sandboxes.items[0]
|
|
223
|
+
else:
|
|
224
|
+
# Create new sandbox with default configuration
|
|
225
|
+
params = CreateSandboxBaseParams(language=self.language) # type: ignore
|
|
226
|
+
self.sandbox = await self.client.create(params) # type: ignore
|
|
227
|
+
self.sandbox_id = self.sandbox.id
|
|
228
|
+
|
|
229
|
+
# Start sandbox if needed
|
|
230
|
+
if self.auto_start and self.sandbox.state != "started":
|
|
231
|
+
await self.sandbox.start()
|
|
232
|
+
|
|
233
|
+
self._initialized = True
|
|
234
|
+
|
|
235
|
+
async def _exec(
|
|
236
|
+
self,
|
|
237
|
+
command: str,
|
|
238
|
+
timeout: int = 30,
|
|
239
|
+
cwd: str | None = None,
|
|
240
|
+
env: dict | None = None,
|
|
241
|
+
) -> str:
|
|
242
|
+
"""
|
|
243
|
+
Execute a shell command in the sandbox.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
command: Shell command to execute
|
|
247
|
+
timeout: Timeout in seconds (None for no timeout)
|
|
248
|
+
cwd: Working directory for the command
|
|
249
|
+
env: Environment variables for the command
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Command output and exit code information
|
|
253
|
+
"""
|
|
254
|
+
await self._ensure_initialized()
|
|
255
|
+
|
|
256
|
+
# Execute command using the process interface
|
|
257
|
+
# API: exec(command, cwd=".", env=None, timeout=None) -> ExecutionResponse
|
|
258
|
+
assert self.sandbox, "no sandbox"
|
|
259
|
+
result = await self.sandbox.process.exec(
|
|
260
|
+
command=command, cwd=cwd or ".", env=env, timeout=timeout
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# ExecutionResponse has .result (output) and .exit_code
|
|
264
|
+
output = result.result or ""
|
|
265
|
+
|
|
266
|
+
# Include exit code if non-zero
|
|
267
|
+
if result.exit_code != 0:
|
|
268
|
+
output = f"[Exit code: {result.exit_code}]\n{output}"
|
|
269
|
+
|
|
270
|
+
# Limit output to last 5000 characters to avoid overwhelming the LLM
|
|
271
|
+
if len(output) > 5000:
|
|
272
|
+
output = "...[truncated]...\n" + output[-5000:]
|
|
273
|
+
|
|
274
|
+
return output or "(no output)"
|
|
275
|
+
|
|
276
|
+
async def _read_file(self, path: str, max_size: int = 50000) -> str:
|
|
277
|
+
"""
|
|
278
|
+
Read a file from the sandbox.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
path: Path to the file in the sandbox
|
|
282
|
+
max_size: Maximum file size in bytes to read
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
File contents as string
|
|
286
|
+
"""
|
|
287
|
+
await self._ensure_initialized()
|
|
288
|
+
|
|
289
|
+
# API: download_file(remote_path, timeout=1800) -> bytes
|
|
290
|
+
assert self.sandbox, "no sandbox"
|
|
291
|
+
content_bytes = await self.sandbox.fs.download_file(path)
|
|
292
|
+
content = content_bytes.decode("utf-8", errors="replace")
|
|
293
|
+
|
|
294
|
+
if len(content) > max_size:
|
|
295
|
+
return f"File too large ({len(content)} bytes). First {max_size} bytes:\n{content[:max_size]}"
|
|
296
|
+
|
|
297
|
+
return content
|
|
298
|
+
|
|
299
|
+
async def _write_file(self, path: str, content: str) -> str:
|
|
300
|
+
"""
|
|
301
|
+
Write content to a file in the sandbox.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
path: Path to the file in the sandbox
|
|
305
|
+
content: Content to write
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Success message
|
|
309
|
+
"""
|
|
310
|
+
await self._ensure_initialized()
|
|
311
|
+
assert self.sandbox, "no sandbox"
|
|
312
|
+
|
|
313
|
+
# API: upload_file(file: bytes, remote_path: str, timeout=1800) -> None
|
|
314
|
+
content_bytes = content.encode("utf-8")
|
|
315
|
+
await self.sandbox.fs.upload_file(content_bytes, path)
|
|
316
|
+
return f"Successfully wrote {len(content)} bytes to {path}"
|
|
317
|
+
|
|
318
|
+
async def _list_files(self, path: str = ".", pattern: str | None = None) -> str:
|
|
319
|
+
"""
|
|
320
|
+
List files in a directory.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
path: Directory path to list
|
|
324
|
+
pattern: Optional glob pattern to filter files
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Formatted list of files
|
|
328
|
+
"""
|
|
329
|
+
await self._ensure_initialized()
|
|
330
|
+
assert self.sandbox, "no sandbox"
|
|
331
|
+
|
|
332
|
+
if pattern:
|
|
333
|
+
# API: find_files(path, pattern) -> List[Match]
|
|
334
|
+
matches = await self.sandbox.fs.find_files(path=path, pattern=pattern)
|
|
335
|
+
if not matches:
|
|
336
|
+
return f"No files matching '{pattern}' found in {path}"
|
|
337
|
+
|
|
338
|
+
# Format the matches
|
|
339
|
+
files = [match.file for match in matches]
|
|
340
|
+
return "\n".join(files)
|
|
341
|
+
else:
|
|
342
|
+
# API: list_files(path) -> List[FileInfo]
|
|
343
|
+
file_infos = await self.sandbox.fs.list_files(path=path)
|
|
344
|
+
|
|
345
|
+
if not file_infos:
|
|
346
|
+
return f"No files found in {path}"
|
|
347
|
+
|
|
348
|
+
# Format the output with file info
|
|
349
|
+
lines = []
|
|
350
|
+
for info in file_infos:
|
|
351
|
+
# FileInfo has .name, .size, .mode, .is_dir, etc
|
|
352
|
+
if info.is_dir:
|
|
353
|
+
lines.append(f"{info.name}/")
|
|
354
|
+
else:
|
|
355
|
+
lines.append(f"{info.name} ({info.size} bytes)")
|
|
356
|
+
return "\n".join(lines)
|
|
357
|
+
|
|
358
|
+
async def _get_preview_link(self, port: int = 8080) -> str:
|
|
359
|
+
"""
|
|
360
|
+
Get a preview link for exposing a port.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
port: Port number to expose
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Preview URL and token information
|
|
367
|
+
"""
|
|
368
|
+
await self._ensure_initialized()
|
|
369
|
+
assert self.sandbox, "no sandbox"
|
|
370
|
+
preview = await self.sandbox.get_preview_link(port)
|
|
371
|
+
|
|
372
|
+
result = f"URL: {preview.url}"
|
|
373
|
+
if hasattr(preview, "token") and preview.token:
|
|
374
|
+
result += f"\nToken: {preview.token}"
|
|
375
|
+
|
|
376
|
+
return result
|
|
377
|
+
|
|
378
|
+
async def _get_working_dir(self) -> str:
|
|
379
|
+
"""Get the current working directory in the sandbox."""
|
|
380
|
+
await self._ensure_initialized()
|
|
381
|
+
assert self.sandbox, "no sandbox"
|
|
382
|
+
return await self.sandbox.get_work_dir()
|
|
383
|
+
|
|
384
|
+
async def _destroy(self):
|
|
385
|
+
"""Delete the sandbox and clean up resources."""
|
|
386
|
+
if self.sandbox and not self._destroyed:
|
|
387
|
+
await self.sandbox.delete()
|
|
388
|
+
self._destroyed = True
|
|
389
|
+
self._initialized = False
|
|
390
|
+
self.sandbox = None
|
|
391
|
+
|
|
392
|
+
def get_tools(self):
|
|
393
|
+
"""Return list of tools for LLM use."""
|
|
394
|
+
bash_tool = Tool(
|
|
395
|
+
name="bash",
|
|
396
|
+
description=(
|
|
397
|
+
"Execute a bash command in the Daytona sandbox environment. "
|
|
398
|
+
"The command runs in a persistent Linux environment. "
|
|
399
|
+
"Provide the command as a string (e.g., 'ls -la' or 'python script.py'). "
|
|
400
|
+
"Output is truncated to the last 5000 characters if longer. "
|
|
401
|
+
"Exit codes are included in output if non-zero."
|
|
402
|
+
),
|
|
403
|
+
run=self._exec,
|
|
404
|
+
parameters={
|
|
405
|
+
"command": {
|
|
406
|
+
"type": "string",
|
|
407
|
+
"description": "The shell command to execute (e.g., 'ls -la', 'python script.py')",
|
|
408
|
+
},
|
|
409
|
+
"timeout": {
|
|
410
|
+
"type": "integer",
|
|
411
|
+
"description": "Timeout in seconds for the command execution (default: 30)",
|
|
412
|
+
},
|
|
413
|
+
"cwd": {
|
|
414
|
+
"type": "string",
|
|
415
|
+
"description": "Working directory for the command (default: current directory)",
|
|
416
|
+
},
|
|
417
|
+
"env": {
|
|
418
|
+
"type": "object",
|
|
419
|
+
"description": "Environment variables for the command (optional)",
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
required=["command"],
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
read_file_tool = Tool(
|
|
426
|
+
name="read_file",
|
|
427
|
+
description=(
|
|
428
|
+
"Read the contents of a file from the sandbox filesystem. "
|
|
429
|
+
"Provide the absolute or relative path to the file. "
|
|
430
|
+
"Files larger than 50KB are truncated."
|
|
431
|
+
),
|
|
432
|
+
run=self._read_file,
|
|
433
|
+
parameters={
|
|
434
|
+
"path": {
|
|
435
|
+
"type": "string",
|
|
436
|
+
"description": "Path to the file to read (e.g., '/home/user/script.py')",
|
|
437
|
+
},
|
|
438
|
+
"max_size": {
|
|
439
|
+
"type": "integer",
|
|
440
|
+
"description": "Maximum file size in bytes to read (default: 50000)",
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
required=["path"],
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
write_file_tool = Tool(
|
|
447
|
+
name="write_file",
|
|
448
|
+
description=(
|
|
449
|
+
"Write content to a file in the sandbox filesystem. "
|
|
450
|
+
"Creates the file if it doesn't exist, overwrites if it does. "
|
|
451
|
+
"Parent directories must exist."
|
|
452
|
+
),
|
|
453
|
+
run=self._write_file,
|
|
454
|
+
parameters={
|
|
455
|
+
"path": {
|
|
456
|
+
"type": "string",
|
|
457
|
+
"description": "Path where to write the file (e.g., '/home/user/script.py')",
|
|
458
|
+
},
|
|
459
|
+
"content": {
|
|
460
|
+
"type": "string",
|
|
461
|
+
"description": "Content to write to the file",
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
required=["path", "content"],
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
list_files_tool = Tool(
|
|
468
|
+
name="list_files",
|
|
469
|
+
description=(
|
|
470
|
+
"List files and directories in the sandbox filesystem. "
|
|
471
|
+
"Useful for exploring the sandbox environment and finding files. "
|
|
472
|
+
"Optionally filter by glob pattern (e.g., '*.py', '**/*.txt')."
|
|
473
|
+
),
|
|
474
|
+
run=self._list_files,
|
|
475
|
+
parameters={
|
|
476
|
+
"path": {
|
|
477
|
+
"type": "string",
|
|
478
|
+
"description": "Directory path to list (default: current directory)",
|
|
479
|
+
},
|
|
480
|
+
"pattern": {
|
|
481
|
+
"type": "string",
|
|
482
|
+
"description": "Glob pattern to filter files (e.g., '*.py', '**/*.txt')",
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
required=[],
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
preview_tool = Tool(
|
|
489
|
+
name="get_preview_link",
|
|
490
|
+
description=(
|
|
491
|
+
"Get a public URL to access a port in the sandbox. "
|
|
492
|
+
"Useful for exposing web servers or applications running in the sandbox. "
|
|
493
|
+
"Returns a URL and authentication token if needed."
|
|
494
|
+
),
|
|
495
|
+
run=self._get_preview_link,
|
|
496
|
+
parameters={
|
|
497
|
+
"port": {
|
|
498
|
+
"type": "integer",
|
|
499
|
+
"description": "Port number to expose (default: 8080)",
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
required=[],
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
workdir_tool = Tool(
|
|
506
|
+
name="get_working_directory",
|
|
507
|
+
description=(
|
|
508
|
+
"Get the current working directory path in the sandbox. "
|
|
509
|
+
"Useful for understanding the sandbox environment layout."
|
|
510
|
+
),
|
|
511
|
+
run=self._get_working_dir,
|
|
512
|
+
parameters={},
|
|
513
|
+
required=[],
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
return [
|
|
517
|
+
bash_tool,
|
|
518
|
+
read_file_tool,
|
|
519
|
+
write_file_tool,
|
|
520
|
+
list_files_tool,
|
|
521
|
+
preview_tool,
|
|
522
|
+
workdir_tool,
|
|
523
|
+
]
|
lm_deluge/models/anthropic.py
CHANGED
|
@@ -10,6 +10,19 @@ ANTHROPIC_MODELS = {
|
|
|
10
10
|
# ░███
|
|
11
11
|
# █████
|
|
12
12
|
#
|
|
13
|
+
"claude-4.5-opus": {
|
|
14
|
+
"id": "claude-4.5-opus",
|
|
15
|
+
"name": "claude-opus-4-5-20251101",
|
|
16
|
+
"api_base": "https://api.anthropic.com/v1",
|
|
17
|
+
"api_key_env_var": "ANTHROPIC_API_KEY",
|
|
18
|
+
"supports_json": False,
|
|
19
|
+
"api_spec": "anthropic",
|
|
20
|
+
"input_cost": 5.0,
|
|
21
|
+
"cached_input_cost": 0.50,
|
|
22
|
+
"cache_write_cost": 6.25,
|
|
23
|
+
"output_cost": 25.0,
|
|
24
|
+
"reasoning_model": True,
|
|
25
|
+
},
|
|
13
26
|
"claude-4.5-haiku": {
|
|
14
27
|
"id": "claude-4.5-haiku",
|
|
15
28
|
"name": "claude-haiku-4-5-20251001",
|
|
@@ -21,6 +34,7 @@ ANTHROPIC_MODELS = {
|
|
|
21
34
|
"cached_input_cost": 0.10,
|
|
22
35
|
"cache_write_cost": 1.25,
|
|
23
36
|
"output_cost": 3.0,
|
|
37
|
+
"reasoning_model": True,
|
|
24
38
|
},
|
|
25
39
|
"claude-4.5-sonnet": {
|
|
26
40
|
"id": "claude-4.5-sonnet",
|
|
@@ -33,6 +47,7 @@ ANTHROPIC_MODELS = {
|
|
|
33
47
|
"cached_input_cost": 0.30,
|
|
34
48
|
"cache_write_cost": 3.75,
|
|
35
49
|
"output_cost": 15.0,
|
|
50
|
+
"reasoning_model": True,
|
|
36
51
|
},
|
|
37
52
|
"claude-4.1-opus": {
|
|
38
53
|
"id": "claude-4.1-opus",
|
lm_deluge/models/google.py
CHANGED
|
@@ -138,4 +138,19 @@ GOOGLE_MODELS = {
|
|
|
138
138
|
"output_cost": 0.4,
|
|
139
139
|
"reasoning_model": True,
|
|
140
140
|
},
|
|
141
|
+
# Gemini 3 models - advanced reasoning with thought signatures
|
|
142
|
+
"gemini-3-pro-preview": {
|
|
143
|
+
"id": "gemini-3-pro-preview",
|
|
144
|
+
"name": "gemini-3-pro-preview",
|
|
145
|
+
"api_base": "https://generativelanguage.googleapis.com/v1alpha",
|
|
146
|
+
"api_key_env_var": "GEMINI_API_KEY",
|
|
147
|
+
"supports_json": True,
|
|
148
|
+
"supports_logprobs": False,
|
|
149
|
+
"api_spec": "gemini",
|
|
150
|
+
"input_cost": 2.0, # <200k tokens
|
|
151
|
+
"cached_input_cost": 0.5, # estimated
|
|
152
|
+
"output_cost": 12.0, # <200k tokens
|
|
153
|
+
# Note: >200k tokens pricing is $4/$18 per million
|
|
154
|
+
"reasoning_model": True,
|
|
155
|
+
},
|
|
141
156
|
}
|
lm_deluge/models/openrouter.py
CHANGED
|
@@ -61,4 +61,14 @@ OPENROUTER_MODELS = {
|
|
|
61
61
|
"cache_write_cost": 0.6,
|
|
62
62
|
"output_cost": 2.20,
|
|
63
63
|
},
|
|
64
|
+
"olmo-3-32b-think-openrouter": {
|
|
65
|
+
"id": "olmo-3-32b-think-openrouter",
|
|
66
|
+
"name": "allenai/olmo-3-32b-think",
|
|
67
|
+
"api_base": "https://openrouter.ai/api/v1",
|
|
68
|
+
"api_key_env_var": "OPENROUTER_API_KEY",
|
|
69
|
+
"supports_json": True,
|
|
70
|
+
"api_spec": "openai",
|
|
71
|
+
"input_cost": 0.2,
|
|
72
|
+
"output_cost": 35,
|
|
73
|
+
},
|
|
64
74
|
}
|
lm_deluge/prompt.py
CHANGED
|
@@ -61,6 +61,8 @@ class ToolCall:
|
|
|
61
61
|
built_in: bool = False
|
|
62
62
|
built_in_type: str | None = None
|
|
63
63
|
extra_body: dict | None = None
|
|
64
|
+
# for gemini 3 - thought signatures to maintain reasoning context
|
|
65
|
+
thought_signature: str | None = None
|
|
64
66
|
|
|
65
67
|
@property
|
|
66
68
|
def fingerprint(self) -> str:
|
|
@@ -93,7 +95,10 @@ class ToolCall:
|
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
def gemini(self) -> dict:
|
|
96
|
-
|
|
98
|
+
result = {"functionCall": {"name": self.name, "args": self.arguments}}
|
|
99
|
+
if self.thought_signature is not None:
|
|
100
|
+
result["thoughtSignature"] = self.thought_signature # type: ignore
|
|
101
|
+
return result
|
|
97
102
|
|
|
98
103
|
def mistral(self) -> dict:
|
|
99
104
|
return {
|
|
@@ -253,6 +258,8 @@ class Thinking:
|
|
|
253
258
|
type: str = field(init=False, default="thinking")
|
|
254
259
|
# for openai - to keep conversation chain
|
|
255
260
|
raw_payload: dict | None = None
|
|
261
|
+
# for gemini 3 - thought signatures to maintain reasoning context
|
|
262
|
+
thought_signature: str | None = None
|
|
256
263
|
|
|
257
264
|
@property
|
|
258
265
|
def fingerprint(self) -> str:
|
|
@@ -270,7 +277,10 @@ class Thinking:
|
|
|
270
277
|
return {"type": "thinking", "thinking": self.content}
|
|
271
278
|
|
|
272
279
|
def gemini(self) -> dict:
|
|
273
|
-
|
|
280
|
+
result = {"text": f"[Thinking: {self.content}]"}
|
|
281
|
+
if self.thought_signature is not None:
|
|
282
|
+
result["thoughtSignature"] = self.thought_signature
|
|
283
|
+
return result
|
|
274
284
|
|
|
275
285
|
def mistral(self) -> dict:
|
|
276
286
|
return {"type": "text", "text": f"[Thinking: {self.content}]"}
|
|
@@ -374,14 +384,15 @@ class Message:
|
|
|
374
384
|
size = p.size
|
|
375
385
|
content_blocks.append({"type": "file", "tag": f"<File ({size} bytes)>"})
|
|
376
386
|
elif isinstance(p, ToolCall):
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
387
|
+
tool_call_block = {
|
|
388
|
+
"type": "tool_call",
|
|
389
|
+
"id": p.id,
|
|
390
|
+
"name": p.name,
|
|
391
|
+
"arguments": _json_safe(p.arguments),
|
|
392
|
+
}
|
|
393
|
+
if p.thought_signature is not None:
|
|
394
|
+
tool_call_block["thought_signature"] = p.thought_signature
|
|
395
|
+
content_blocks.append(tool_call_block)
|
|
385
396
|
elif isinstance(p, ToolResult):
|
|
386
397
|
content_blocks.append(
|
|
387
398
|
{
|
|
@@ -391,7 +402,10 @@ class Message:
|
|
|
391
402
|
}
|
|
392
403
|
)
|
|
393
404
|
elif isinstance(p, Thinking):
|
|
394
|
-
|
|
405
|
+
thinking_block = {"type": "thinking", "content": p.content}
|
|
406
|
+
if p.thought_signature is not None:
|
|
407
|
+
thinking_block["thought_signature"] = p.thought_signature
|
|
408
|
+
content_blocks.append(thinking_block)
|
|
395
409
|
|
|
396
410
|
return {"role": self.role, "content": content_blocks}
|
|
397
411
|
|
|
@@ -415,14 +429,24 @@ class Message:
|
|
|
415
429
|
parts.append(Text(p["tag"]))
|
|
416
430
|
elif p["type"] == "tool_call":
|
|
417
431
|
parts.append(
|
|
418
|
-
ToolCall(
|
|
432
|
+
ToolCall(
|
|
433
|
+
id=p["id"],
|
|
434
|
+
name=p["name"],
|
|
435
|
+
arguments=p["arguments"],
|
|
436
|
+
thought_signature=p.get("thought_signature"),
|
|
437
|
+
)
|
|
419
438
|
)
|
|
420
439
|
elif p["type"] == "tool_result":
|
|
421
440
|
parts.append(
|
|
422
441
|
ToolResult(tool_call_id=p["tool_call_id"], result=p["result"])
|
|
423
442
|
)
|
|
424
443
|
elif p["type"] == "thinking":
|
|
425
|
-
parts.append(
|
|
444
|
+
parts.append(
|
|
445
|
+
Thinking(
|
|
446
|
+
content=p["content"],
|
|
447
|
+
thought_signature=p.get("thought_signature"),
|
|
448
|
+
)
|
|
449
|
+
)
|
|
426
450
|
else:
|
|
427
451
|
raise ValueError(f"Unknown part type {p['type']!r}")
|
|
428
452
|
|
|
@@ -1546,14 +1570,15 @@ class Conversation:
|
|
|
1546
1570
|
{"type": "file", "tag": f"<File ({size} bytes)>"}
|
|
1547
1571
|
)
|
|
1548
1572
|
elif isinstance(p, ToolCall):
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1573
|
+
tool_call_block = {
|
|
1574
|
+
"type": "tool_call",
|
|
1575
|
+
"id": p.id,
|
|
1576
|
+
"name": p.name,
|
|
1577
|
+
"arguments": p.arguments,
|
|
1578
|
+
}
|
|
1579
|
+
if p.thought_signature is not None:
|
|
1580
|
+
tool_call_block["thought_signature"] = p.thought_signature
|
|
1581
|
+
content_blocks.append(tool_call_block)
|
|
1557
1582
|
elif isinstance(p, ToolResult):
|
|
1558
1583
|
content_blocks.append(
|
|
1559
1584
|
{
|
|
@@ -1565,7 +1590,10 @@ class Conversation:
|
|
|
1565
1590
|
}
|
|
1566
1591
|
)
|
|
1567
1592
|
elif isinstance(p, Thinking):
|
|
1568
|
-
|
|
1593
|
+
thinking_block = {"type": "thinking", "content": p.content}
|
|
1594
|
+
if p.thought_signature is not None:
|
|
1595
|
+
thinking_block["thought_signature"] = p.thought_signature
|
|
1596
|
+
content_blocks.append(thinking_block)
|
|
1569
1597
|
serialized.append({"role": msg.role, "content": content_blocks})
|
|
1570
1598
|
|
|
1571
1599
|
return {"messages": serialized}
|
|
@@ -1590,14 +1618,24 @@ class Conversation:
|
|
|
1590
1618
|
parts.append(Text(p["tag"]))
|
|
1591
1619
|
elif p["type"] == "tool_call":
|
|
1592
1620
|
parts.append(
|
|
1593
|
-
ToolCall(
|
|
1621
|
+
ToolCall(
|
|
1622
|
+
id=p["id"],
|
|
1623
|
+
name=p["name"],
|
|
1624
|
+
arguments=p["arguments"],
|
|
1625
|
+
thought_signature=p.get("thought_signature"),
|
|
1626
|
+
)
|
|
1594
1627
|
)
|
|
1595
1628
|
elif p["type"] == "tool_result":
|
|
1596
1629
|
parts.append(
|
|
1597
1630
|
ToolResult(tool_call_id=p["tool_call_id"], result=p["result"])
|
|
1598
1631
|
)
|
|
1599
1632
|
elif p["type"] == "thinking":
|
|
1600
|
-
parts.append(
|
|
1633
|
+
parts.append(
|
|
1634
|
+
Thinking(
|
|
1635
|
+
content=p["content"],
|
|
1636
|
+
thought_signature=p.get("thought_signature"),
|
|
1637
|
+
)
|
|
1638
|
+
)
|
|
1601
1639
|
else:
|
|
1602
1640
|
raise ValueError(f"Unknown part type {p['type']!r}")
|
|
1603
1641
|
|
lm_deluge/warnings.py
CHANGED
|
@@ -9,6 +9,10 @@ WARNINGS: dict[str, str] = {
|
|
|
9
9
|
"WARN_LOGPROBS_UNSUPPORTED": "Ignoring logprobs param for non-logprobs model: {model_name}",
|
|
10
10
|
"WARN_MINIMAL_TO_LOW": "'minimal' reasoning effort only allowed for gpt-5 models. Setting to 'low' for {model_name}.",
|
|
11
11
|
"WARN_MINIMAL_TO_NONE": "GPT-5.1 models don't support 'minimal' reasoning effort. Converting to 'none' for {model_name}.",
|
|
12
|
+
"WARN_MEDIA_RESOLUTION_UNSUPPORTED": "media_resolution parameter is only supported for Gemini 3 models, ignoring for {model_name}.",
|
|
13
|
+
"WARN_GEMINI3_MISSING_SIGNATURE": "Gemini 3 thought signature missing in {part_type}, injecting dummy signature 'context_engineering_is_the_way_to_go' to avoid API error.",
|
|
14
|
+
"WARN_GEMINI3_NO_REASONING": "Gemini 3 requires reasoning (thinkingConfig). Setting thinkingConfig to low.",
|
|
15
|
+
"WARN_THINKING_BUDGET_AND_REASONING_EFFORT": "`reasoning_effort` and `thinking_budget` both provided. `thinking_budget` will take priority.",
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lm_deluge
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.80
|
|
4
4
|
Summary: Python utility for using LLM API models.
|
|
5
5
|
Author-email: Benjamin Anderson <ben@trytaylor.ai>
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -52,7 +52,7 @@ Dynamic: license-file
|
|
|
52
52
|
pip install lm-deluge
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
The package relies on environment variables for API keys. Typical variables include `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `COHERE_API_KEY`, `META_API_KEY`, and `
|
|
55
|
+
The package relies on environment variables for API keys. Typical variables include `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `COHERE_API_KEY`, `META_API_KEY`, and `GEMINI_API_KEY`. `LLMClient` will automatically load the `.env` file when imported; we recommend using that to set the environment variables. For Bedrock, you'll need to set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.
|
|
56
56
|
|
|
57
57
|
## Quickstart
|
|
58
58
|
|
|
@@ -61,9 +61,9 @@ The package relies on environment variables for API keys. Typical variables incl
|
|
|
61
61
|
```python
|
|
62
62
|
from lm_deluge import LLMClient
|
|
63
63
|
|
|
64
|
-
client = LLMClient("gpt-
|
|
64
|
+
client = LLMClient("gpt-4.1-mini")
|
|
65
65
|
resps = client.process_prompts_sync(["Hello, world!"])
|
|
66
|
-
print(
|
|
66
|
+
print(resps[0].completion)
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
## Spraying Across Models
|
|
@@ -74,13 +74,13 @@ To distribute your requests across models, just provide a list of more than one
|
|
|
74
74
|
from lm_deluge import LLMClient
|
|
75
75
|
|
|
76
76
|
client = LLMClient(
|
|
77
|
-
["gpt-
|
|
77
|
+
["gpt-4.1-mini", "claude-4.5-haiku"],
|
|
78
78
|
max_requests_per_minute=10_000
|
|
79
79
|
)
|
|
80
80
|
resps = client.process_prompts_sync(
|
|
81
81
|
["Hello, ChatGPT!", "Hello, Claude!"]
|
|
82
82
|
)
|
|
83
|
-
print(
|
|
83
|
+
print(resps[0].completion)
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
## Configuration
|
|
@@ -181,7 +181,7 @@ def get_weather(city: str) -> str:
|
|
|
181
181
|
return f"The weather in {city} is sunny and 72°F"
|
|
182
182
|
|
|
183
183
|
tool = Tool.from_function(get_weather)
|
|
184
|
-
client = LLMClient("claude-
|
|
184
|
+
client = LLMClient("claude-4.5-haiku")
|
|
185
185
|
resps = client.process_prompts_sync(
|
|
186
186
|
["What's the weather in Paris?"],
|
|
187
187
|
tools=[tool]
|
|
@@ -255,7 +255,7 @@ conv = (
|
|
|
255
255
|
)
|
|
256
256
|
|
|
257
257
|
# Use prompt caching to cache system message and tools
|
|
258
|
-
client = LLMClient("claude-
|
|
258
|
+
client = LLMClient("claude-4.5-sonnet")
|
|
259
259
|
resps = client.process_prompts_sync(
|
|
260
260
|
[conv],
|
|
261
261
|
cache="system_and_tools" # Cache system message and any tools
|
|
@@ -2,27 +2,27 @@ lm_deluge/__init__.py,sha256=zF5lAitfgJ8A28IXJ5BE9OUCqGOqSnGOWn3ZIlizNyY,822
|
|
|
2
2
|
lm_deluge/batches.py,sha256=Km6QM5_7BlF2qEyo4WPlhkaZkpzrLqf50AaveHXQOoY,25127
|
|
3
3
|
lm_deluge/cache.py,sha256=xO2AIYvP3tUpTMKQjwQQYfGRJSRi6e7sMlRhLjsS-u4,4873
|
|
4
4
|
lm_deluge/cli.py,sha256=Ilww5gOw3J5v0NReq_Ra4hhxU4BCIJBl1oTGxJZKedc,12065
|
|
5
|
-
lm_deluge/client.py,sha256=
|
|
6
|
-
lm_deluge/config.py,sha256=
|
|
5
|
+
lm_deluge/client.py,sha256=VqCuFXM_ylO4v-lev85HMPFRHeU69tZo70favz-I2Uk,44791
|
|
6
|
+
lm_deluge/config.py,sha256=C-_rVwAFL5sivLfKSkaa2ANMqqxKbyDCW86KfQB_Lck,1357
|
|
7
7
|
lm_deluge/embed.py,sha256=CO-TOlC5kOTAM8lcnicoG4u4K664vCBwHF1vHa-nAGg,13382
|
|
8
8
|
lm_deluge/errors.py,sha256=oHjt7YnxWbh-eXMScIzov4NvpJMo0-2r5J6Wh5DQ1tk,209
|
|
9
9
|
lm_deluge/file.py,sha256=PTmlJQ-IaYcYUFun9V0bJ1NPVP84edJrR0hvCMWFylY,19697
|
|
10
10
|
lm_deluge/image.py,sha256=5AMXmn2x47yXeYNfMSMAOWcnlrOxxOel-4L8QCJwU70,8928
|
|
11
11
|
lm_deluge/mock_openai.py,sha256=-u4kxSzwoxDt_2fLh5LaiqETnu0Jg_VDL7TWAAYHGNw,21762
|
|
12
|
-
lm_deluge/prompt.py,sha256=
|
|
12
|
+
lm_deluge/prompt.py,sha256=JRjLckFQ14r5wfWcYCjFOTGADTz4klwMthctx0GwrtU,65808
|
|
13
13
|
lm_deluge/request_context.py,sha256=eM_cCXZsrVb5FF3VQl6u1dZeZrWv00wW42Cr_Fjs5oA,2752
|
|
14
14
|
lm_deluge/rerank.py,sha256=-NBAJdHz9OB-SWWJnHzkFmeVO4wR6lFV7Vw-SxG7aVo,11457
|
|
15
15
|
lm_deluge/tool.py,sha256=ipgNy4OpfH3CA9OPQq5zfn1xO8H08GMvDynB8ZPQ5mA,30617
|
|
16
16
|
lm_deluge/tracker.py,sha256=aeS9GUJpgOSQRVXAnGDvlMO8qYpSxpTNLYj2hrMg0m8,14757
|
|
17
17
|
lm_deluge/usage.py,sha256=xz9tAw2hqaJvv9aAVhnQ6N1Arn7fS8Shb28VwCW26wI,5136
|
|
18
|
-
lm_deluge/warnings.py,sha256=
|
|
18
|
+
lm_deluge/warnings.py,sha256=12RseSa9mYAFkbY783FQTP0x9RapRBErIQt4o7hzVnM,2321
|
|
19
19
|
lm_deluge/api_requests/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
20
|
-
lm_deluge/api_requests/anthropic.py,sha256=
|
|
20
|
+
lm_deluge/api_requests/anthropic.py,sha256=ytNeADgGeflmlm5gVQ0cJ5bgchJ_EZvKJIIt7Imxf2A,11338
|
|
21
21
|
lm_deluge/api_requests/base.py,sha256=mXEM85mcU_5LD-ugELpCl28tv-tpHKcaxerTIVLQZVo,10436
|
|
22
22
|
lm_deluge/api_requests/bedrock.py,sha256=mY1xTvgfCLyqLlfFFmu_baKgkVq1Df1_MJXeN_G1jWQ,15597
|
|
23
23
|
lm_deluge/api_requests/chat_reasoning.py,sha256=sJvstvKFqsSBUjYcwxzGt2_FH4cEp3Z6gKcBPyPjGwk,236
|
|
24
24
|
lm_deluge/api_requests/common.py,sha256=BZ3vRO5TB669_UsNKugkkuFSzoLHOYJIKt4nV4sf4vc,422
|
|
25
|
-
lm_deluge/api_requests/gemini.py,sha256=
|
|
25
|
+
lm_deluge/api_requests/gemini.py,sha256=FjYKisAjD6rW2fA6WyXnnRn3oqJBXMod1_8HtGWIyEU,11099
|
|
26
26
|
lm_deluge/api_requests/mistral.py,sha256=8JZP2CDf1XZfaPcTk0WS4q-VfYYj58ptpoH8LD3MQG4,4528
|
|
27
27
|
lm_deluge/api_requests/openai.py,sha256=E0oakhcb2T5Swfn6ATMjRZKuLyRrx4Zj5SREo1JILfc,28841
|
|
28
28
|
lm_deluge/api_requests/response.py,sha256=vG194gAH5p7ulpNy4qy5Pryfb1p3ZV21-YGoj__ru3E,7436
|
|
@@ -43,18 +43,19 @@ lm_deluge/llm_tools/extract.py,sha256=p61JW8yv5gQxPp4P8Hkm90ERgfD_Ek5IABzjIIlX-M
|
|
|
43
43
|
lm_deluge/llm_tools/filesystem.py,sha256=Uy0lQ2Ecx5Cvqv0Sr3r_PEw8gBGZ21VAov5dg2knKfk,27942
|
|
44
44
|
lm_deluge/llm_tools/locate.py,sha256=lYNbKTmy9dTvj0lEQkOQ7yrxyqsgYzjD0C_byJKI_4w,6271
|
|
45
45
|
lm_deluge/llm_tools/ocr.py,sha256=7fDlvs6uUOvbxMasvGGNJx5Fj6biM6z3lijKZaGN26k,23
|
|
46
|
+
lm_deluge/llm_tools/sandbox.py,sha256=7bc4r0ApY4WfdzNrfKfO4Omoz9rR1rr86qp-OwtqlyY,18399
|
|
46
47
|
lm_deluge/llm_tools/score.py,sha256=9oGA3-k2U5buHQXkXaEI9M4Wb5yysNhTLsPbGeghAlQ,2580
|
|
47
48
|
lm_deluge/llm_tools/subagents.py,sha256=srJ7On7YR0Y8WuNvf5TJl_7IUfEtG3zlxZeLgmn_-NI,8484
|
|
48
49
|
lm_deluge/llm_tools/todos.py,sha256=doKJZWLZlh4J_k6HkdwonWHfZTZaxEI9_XHAoNFnfQo,14906
|
|
49
50
|
lm_deluge/llm_tools/translate.py,sha256=iXyYvQZ8bC44FWhBk4qpdqjKM1WFF7Shq-H2PxhPgg4,1452
|
|
50
51
|
lm_deluge/models/__init__.py,sha256=54H24K_eADbfdEH9aNORrNEXvDLZCQ4TEekeLiWljSE,4619
|
|
51
|
-
lm_deluge/models/anthropic.py,sha256=
|
|
52
|
+
lm_deluge/models/anthropic.py,sha256=X92EYIapos-8LXnIYiypPJcFhI0tqmXja_w8e9H4CF8,6781
|
|
52
53
|
lm_deluge/models/bedrock.py,sha256=g1PbfceSRH2lWST3ja0mUlF3oTq4e4T-si6RMe7qXgg,4888
|
|
53
54
|
lm_deluge/models/cerebras.py,sha256=u2FMXJF6xMr0euDRKLKMo_NVTOcvSrrEpehbHr8sSeE,2050
|
|
54
55
|
lm_deluge/models/cohere.py,sha256=iXjYtM6jy_YL73Op8OfNsrMNopwae9y-Sw-4vF9cEBw,3406
|
|
55
56
|
lm_deluge/models/deepseek.py,sha256=6_jDEprNNYis5I5MDQNloRes9h1P6pMYHXxOd2UZMgg,941
|
|
56
57
|
lm_deluge/models/fireworks.py,sha256=yvt2Ggzye4aUqCqY74ta67Vu7FrQaLFjdFtN4P7D-dc,638
|
|
57
|
-
lm_deluge/models/google.py,sha256=
|
|
58
|
+
lm_deluge/models/google.py,sha256=fARfBMHDwhPJ48SGcYg3sHQDQ0Mm0yPQ-9s6iVYne8M,6011
|
|
58
59
|
lm_deluge/models/grok.py,sha256=TDzr8yfTaHbdJhwMA-Du6L-efaKFJhjTQViuVElCCHI,2566
|
|
59
60
|
lm_deluge/models/groq.py,sha256=Mi5WE1xOBGoZlymD0UN6kzhH_NOmfJYU4N2l-TO0Z8Q,2552
|
|
60
61
|
lm_deluge/models/kimi.py,sha256=1voigLdNO2CxpWv0KDpQPP3Wolx5WrqgAlYL9ObJFuQ,1117
|
|
@@ -62,7 +63,7 @@ lm_deluge/models/meta.py,sha256=BBgnscL1gMcIdPbRqrlDl_q9YAYGSrkw9JkAIabXtLs,1883
|
|
|
62
63
|
lm_deluge/models/minimax.py,sha256=rwW9gNotAYfDVtMlqmSYegN6GoZM_9DSNNZU2yPOmaU,275
|
|
63
64
|
lm_deluge/models/mistral.py,sha256=x67o5gckBGmPcIGdVbS26XZAYFKBYM4tsxEAahGp8bk,4323
|
|
64
65
|
lm_deluge/models/openai.py,sha256=t6fcXo0YXgPQ6YiftZJP8gPw8FOBqoVapSavMVmtaOw,12411
|
|
65
|
-
lm_deluge/models/openrouter.py,sha256=
|
|
66
|
+
lm_deluge/models/openrouter.py,sha256=aT3AGBaZ0ShY2-ncGHbc8UEm00l78GqkXy9Pq67SITQ,2469
|
|
66
67
|
lm_deluge/models/together.py,sha256=AjKhPsazqBgqyLwHkNQW07COM1n_oSrYQRp2BFVvn9o,4381
|
|
67
68
|
lm_deluge/presets/cerebras.py,sha256=MDkqj15qQRrj8wxSCDNNe_Cs7h1WN1UjV6lTmSY1olQ,479
|
|
68
69
|
lm_deluge/presets/meta.py,sha256=QrreLAVgYS6VIC_NQth1vgGAYuxY38jFQQZSe6ot7C8,364
|
|
@@ -73,8 +74,8 @@ lm_deluge/util/schema.py,sha256=q6uwhA4s1lM2dHT1Kwc46E7OY1VecMOtTEI0PTFn6tA,1320
|
|
|
73
74
|
lm_deluge/util/spatial.py,sha256=BsF_UKhE-x0xBirc-bV1xSKZRTUhsOBdGqsMKme20C8,4099
|
|
74
75
|
lm_deluge/util/validation.py,sha256=hz5dDb3ebvZrZhnaWxOxbNSVMI6nmaOODBkk0htAUhs,1575
|
|
75
76
|
lm_deluge/util/xml.py,sha256=Ft4zajoYBJR3HHCt2oHwGfymGLdvp_gegVmJ-Wqk4Ck,10547
|
|
76
|
-
lm_deluge-0.0.
|
|
77
|
-
lm_deluge-0.0.
|
|
78
|
-
lm_deluge-0.0.
|
|
79
|
-
lm_deluge-0.0.
|
|
80
|
-
lm_deluge-0.0.
|
|
77
|
+
lm_deluge-0.0.80.dist-info/licenses/LICENSE,sha256=uNNXGXPCw2TC7CUs7SEBkA-Mz6QBQFWUUEWDMgEs1dU,1058
|
|
78
|
+
lm_deluge-0.0.80.dist-info/METADATA,sha256=LJ2nPTs9WzdiP3kU5KPKUdOy_SuuiHRJCz9PINHEvZk,13705
|
|
79
|
+
lm_deluge-0.0.80.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
80
|
+
lm_deluge-0.0.80.dist-info/top_level.txt,sha256=hqU-TJX93yBwpgkDtYcXyLr3t7TLSCCZ_reytJjwBaE,10
|
|
81
|
+
lm_deluge-0.0.80.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|