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.
@@ -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 and sampling_params.reasoning_effort:
67
- # translate reasoning effort of low, medium, high to budget tokens
68
- budget = {"minimal": 256, "low": 1024, "medium": 4096, "high": 16384}.get(
69
- sampling_params.reasoning_effort
70
- )
71
- request_json["thinking"] = {
72
- "type": "enabled",
73
- "budget_tokens": budget,
74
- }
75
- if "top_p" in request_json:
76
- request_json["top_p"] = max(request_json["top_p"], 0.95)
77
- request_json["temperature"] = 1.0
78
- request_json["max_tokens"] += budget
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
- if "temperature" in request_json and "top_p" in request_json:
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:
@@ -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
- if model.reasoning_model:
41
- thinking_config: dict[str, Any] | None = None
42
- effort = sampling_params.reasoning_effort
43
- if effort is None or effort == "none":
44
- budget = 128 if "2.5-pro" in model.id else 0
45
- # Explicitly disable thoughts when no effort is requested
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
- thinking_config = {"includeThoughts": True}
49
- if effort in {"minimal", "low", "medium", "high"} and "flash" in model.id:
50
- budget = {"minimal": 256, "low": 1024, "medium": 4096, "high": 16384}[
51
- effort
52
- ]
53
- thinking_config["thinkingBudget"] = budget
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
- parts.append(Thinking(part["thought"]))
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.75
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.75),
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.75,
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.75,
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.75,
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 = 0.0
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
+ ]
@@ -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",
@@ -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
  }
@@ -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
- return {"functionCall": {"name": self.name, "args": self.arguments}}
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
- return {"text": f"[Thinking: {self.content}]"}
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
- content_blocks.append(
378
- {
379
- "type": "tool_call",
380
- "id": p.id,
381
- "name": p.name,
382
- "arguments": _json_safe(p.arguments),
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
- content_blocks.append({"type": "thinking", "content": p.content})
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(id=p["id"], name=p["name"], arguments=p["arguments"])
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(Thinking(content=p["content"]))
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
- content_blocks.append(
1550
- {
1551
- "type": "tool_call",
1552
- "id": p.id,
1553
- "name": p.name,
1554
- "arguments": p.arguments,
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
- content_blocks.append({"type": "thinking", "content": p.content})
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(id=p["id"], name=p["name"], arguments=p["arguments"])
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(Thinking(content=p["content"]))
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.78
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 `GOOGLE_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`.
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-4o-mini")
64
+ client = LLMClient("gpt-4.1-mini")
65
65
  resps = client.process_prompts_sync(["Hello, world!"])
66
- print(resp[0].completion)
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-4o-mini", "claude-3-haiku"],
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(resp[0].completion)
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-3-haiku")
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-3-5-sonnet")
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=RoxIeuM-LjaKl9fPXfs7rf2NyOuUNBLbTjVPkcIRfYA,44776
6
- lm_deluge/config.py,sha256=d7lS6i5-J23nM-UPjJ_gFcsmfgI8aPVwqw_IGGb1x3I,975
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=_6xkMuEKJEizRNbjQ5gbZesoE_vtwQe2wJHdm-E6vP0,64002
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=xXXYXEfaaSVr__16BKOEEWLdfZi1L-2ylzTrXTRyO18,1748
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=OvkciXTHyrG1cFyC1vv6nYyCFTqtMgt1r15Q-pbHiUQ,10411
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=4uD7fQl0yWyAvYkPNi3oO1InBnvYfo5_QR6k-va-2GI,7838
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=sFkS-g0OWgRnVoFMKxWkSUt0qy2LVrcO5KtbYAG26iY,6283
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=Hr2MolQoaeY85pKCGO7k7OH_1nQJdrwMgrJbfz5bI8w,5387
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=O-Po4tmHjAqFIVU96TUL0QnK01R4e2yDN7Z4sYJ-CuE,2120
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.78.dist-info/licenses/LICENSE,sha256=uNNXGXPCw2TC7CUs7SEBkA-Mz6QBQFWUUEWDMgEs1dU,1058
77
- lm_deluge-0.0.78.dist-info/METADATA,sha256=VaCsZgSsbc2yr0Wm8Q5Gw_9UKMRm-KkKkVQ7rS_p5-M,13697
78
- lm_deluge-0.0.78.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
79
- lm_deluge-0.0.78.dist-info/top_level.txt,sha256=hqU-TJX93yBwpgkDtYcXyLr3t7TLSCCZ_reytJjwBaE,10
80
- lm_deluge-0.0.78.dist-info/RECORD,,
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,,