lm-deluge 0.0.56__py3-none-any.whl → 0.0.69__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/__init__.py +12 -1
- lm_deluge/api_requests/anthropic.py +12 -1
- lm_deluge/api_requests/base.py +87 -5
- lm_deluge/api_requests/bedrock.py +3 -4
- lm_deluge/api_requests/chat_reasoning.py +4 -0
- lm_deluge/api_requests/gemini.py +7 -6
- lm_deluge/api_requests/mistral.py +8 -9
- lm_deluge/api_requests/openai.py +179 -124
- lm_deluge/batches.py +25 -9
- lm_deluge/client.py +280 -67
- lm_deluge/config.py +1 -1
- lm_deluge/file.py +382 -13
- lm_deluge/mock_openai.py +482 -0
- lm_deluge/models/__init__.py +12 -8
- lm_deluge/models/anthropic.py +12 -20
- lm_deluge/models/bedrock.py +0 -14
- lm_deluge/models/cohere.py +0 -16
- lm_deluge/models/google.py +0 -20
- lm_deluge/models/grok.py +48 -4
- lm_deluge/models/groq.py +2 -2
- lm_deluge/models/kimi.py +34 -0
- lm_deluge/models/meta.py +0 -8
- lm_deluge/models/minimax.py +10 -0
- lm_deluge/models/openai.py +28 -34
- lm_deluge/models/openrouter.py +64 -1
- lm_deluge/models/together.py +0 -16
- lm_deluge/prompt.py +138 -29
- lm_deluge/request_context.py +9 -11
- lm_deluge/tool.py +395 -19
- lm_deluge/tracker.py +11 -5
- lm_deluge/warnings.py +46 -0
- {lm_deluge-0.0.56.dist-info → lm_deluge-0.0.69.dist-info}/METADATA +3 -1
- {lm_deluge-0.0.56.dist-info → lm_deluge-0.0.69.dist-info}/RECORD +36 -33
- lm_deluge/agent.py +0 -0
- lm_deluge/gemini_limits.py +0 -65
- {lm_deluge-0.0.56.dist-info → lm_deluge-0.0.69.dist-info}/WHEEL +0 -0
- {lm_deluge-0.0.56.dist-info → lm_deluge-0.0.69.dist-info}/licenses/LICENSE +0 -0
- {lm_deluge-0.0.56.dist-info → lm_deluge-0.0.69.dist-info}/top_level.txt +0 -0
lm_deluge/tool.py
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import inspect
|
|
3
3
|
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
Callable,
|
|
7
|
+
Coroutine,
|
|
8
|
+
Literal,
|
|
9
|
+
Type,
|
|
10
|
+
TypedDict,
|
|
11
|
+
get_args,
|
|
12
|
+
get_origin,
|
|
13
|
+
get_type_hints,
|
|
14
|
+
)
|
|
5
15
|
|
|
6
16
|
from fastmcp import Client # pip install fastmcp >= 2.0
|
|
7
17
|
from mcp.types import Tool as MCPTool
|
|
@@ -11,6 +21,196 @@ from lm_deluge.image import Image
|
|
|
11
21
|
from lm_deluge.prompt import Text, ToolResultPart
|
|
12
22
|
|
|
13
23
|
|
|
24
|
+
def _python_type_to_json_schema_enhanced(python_type: Any) -> dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Convert Python type annotations to JSON Schema.
|
|
27
|
+
Handles: primitives, Optional, Literal, list[T], dict[str, T], Union.
|
|
28
|
+
"""
|
|
29
|
+
# Get origin and args for generic types
|
|
30
|
+
origin = get_origin(python_type)
|
|
31
|
+
args = get_args(python_type)
|
|
32
|
+
|
|
33
|
+
# Handle Optional[T] or T | None
|
|
34
|
+
if origin is type(None) or python_type is type(None):
|
|
35
|
+
return {"type": "null"}
|
|
36
|
+
|
|
37
|
+
# Handle Union types (including Optional)
|
|
38
|
+
if origin is Literal:
|
|
39
|
+
# Literal["a", "b"] -> enum
|
|
40
|
+
return {"type": "string", "enum": list(args)}
|
|
41
|
+
|
|
42
|
+
# Handle list[T]
|
|
43
|
+
if origin is list:
|
|
44
|
+
if args:
|
|
45
|
+
items_schema = _python_type_to_json_schema_enhanced(args[0])
|
|
46
|
+
return {"type": "array", "items": items_schema}
|
|
47
|
+
return {"type": "array"}
|
|
48
|
+
|
|
49
|
+
# Handle dict[str, T]
|
|
50
|
+
if origin is dict:
|
|
51
|
+
if len(args) >= 2:
|
|
52
|
+
# For dict[str, T], we can set additionalProperties
|
|
53
|
+
value_schema = _python_type_to_json_schema_enhanced(args[1])
|
|
54
|
+
return {"type": "object", "additionalProperties": value_schema}
|
|
55
|
+
return {"type": "object"}
|
|
56
|
+
|
|
57
|
+
# Handle basic types
|
|
58
|
+
if python_type is int:
|
|
59
|
+
return {"type": "integer"}
|
|
60
|
+
elif python_type is float:
|
|
61
|
+
return {"type": "number"}
|
|
62
|
+
elif python_type is str:
|
|
63
|
+
return {"type": "string"}
|
|
64
|
+
elif python_type is bool:
|
|
65
|
+
return {"type": "boolean"}
|
|
66
|
+
elif python_type is list:
|
|
67
|
+
return {"type": "array"}
|
|
68
|
+
elif python_type is dict:
|
|
69
|
+
return {"type": "object"}
|
|
70
|
+
else:
|
|
71
|
+
# Default to string for unknown types
|
|
72
|
+
return {"type": "string"}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ToolParams:
|
|
76
|
+
"""
|
|
77
|
+
Helper class for constructing tool parameters more easily.
|
|
78
|
+
|
|
79
|
+
Usage:
|
|
80
|
+
# Simple constructor with Python types
|
|
81
|
+
params = ToolParams({"city": str, "age": int})
|
|
82
|
+
|
|
83
|
+
# With extras (description, enum, etc)
|
|
84
|
+
params = ToolParams({
|
|
85
|
+
"operation": (str, {"enum": ["add", "sub"], "description": "Math operation"}),
|
|
86
|
+
"value": (int, {"description": "The value"})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
# From Pydantic model
|
|
90
|
+
params = ToolParams.from_pydantic(MyModel)
|
|
91
|
+
|
|
92
|
+
# From TypedDict
|
|
93
|
+
params = ToolParams.from_typed_dict(MyTypedDict)
|
|
94
|
+
|
|
95
|
+
# From existing JSON Schema
|
|
96
|
+
params = ToolParams.from_json_schema(schema_dict, required=["field1"])
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, spec: dict[str, Any]):
|
|
100
|
+
"""
|
|
101
|
+
Create ToolParams from a dict mapping parameter names to types or (type, extras) tuples.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
spec: Dict where values can be:
|
|
105
|
+
- A Python type (str, int, list[str], etc.)
|
|
106
|
+
- A tuple of (type, extras_dict) for additional JSON Schema properties
|
|
107
|
+
- An already-formed JSON Schema dict (passed through as-is)
|
|
108
|
+
"""
|
|
109
|
+
self.parameters: dict[str, Any] = {}
|
|
110
|
+
self.required: list[str] = []
|
|
111
|
+
|
|
112
|
+
for param_name, param_spec in spec.items():
|
|
113
|
+
# If it's a tuple, extract (type, extras)
|
|
114
|
+
if isinstance(param_spec, tuple):
|
|
115
|
+
param_type, extras = param_spec
|
|
116
|
+
schema = _python_type_to_json_schema_enhanced(param_type)
|
|
117
|
+
schema.update(extras)
|
|
118
|
+
self.parameters[param_name] = schema
|
|
119
|
+
# Mark as required unless explicitly marked as optional
|
|
120
|
+
if extras.get("optional") is not True:
|
|
121
|
+
self.required.append(param_name)
|
|
122
|
+
# If it's already a dict with "type" key, use as-is
|
|
123
|
+
elif isinstance(param_spec, dict) and "type" in param_spec:
|
|
124
|
+
self.parameters[param_name] = param_spec
|
|
125
|
+
# Assume required unless marked optional
|
|
126
|
+
if param_spec.get("optional") is not True:
|
|
127
|
+
self.required.append(param_name)
|
|
128
|
+
# Otherwise treat as a Python type
|
|
129
|
+
else:
|
|
130
|
+
self.parameters[param_name] = _python_type_to_json_schema_enhanced(
|
|
131
|
+
param_spec
|
|
132
|
+
)
|
|
133
|
+
self.required.append(param_name)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def from_pydantic(cls, model: Type[BaseModel]) -> "ToolParams":
|
|
137
|
+
"""
|
|
138
|
+
Create ToolParams from a Pydantic model.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
model: A Pydantic BaseModel class
|
|
142
|
+
"""
|
|
143
|
+
# Get the JSON schema from Pydantic
|
|
144
|
+
schema = model.model_json_schema()
|
|
145
|
+
properties = schema.get("properties", {})
|
|
146
|
+
required = schema.get("required", [])
|
|
147
|
+
|
|
148
|
+
return cls.from_json_schema(properties, required)
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def from_typed_dict(cls, typed_dict: Type) -> "ToolParams":
|
|
152
|
+
"""
|
|
153
|
+
Create ToolParams from a TypedDict.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
typed_dict: A TypedDict class
|
|
157
|
+
"""
|
|
158
|
+
hints = get_type_hints(typed_dict)
|
|
159
|
+
|
|
160
|
+
# TypedDict doesn't have a built-in way to mark optional fields,
|
|
161
|
+
# but we can check for Optional in the type hints
|
|
162
|
+
params = {}
|
|
163
|
+
required = []
|
|
164
|
+
|
|
165
|
+
for field_name, field_type in hints.items():
|
|
166
|
+
# Check if it's Optional (Union with None)
|
|
167
|
+
origin = get_origin(field_type)
|
|
168
|
+
# args = get_args(field_type)
|
|
169
|
+
|
|
170
|
+
is_optional = False
|
|
171
|
+
actual_type = field_type
|
|
172
|
+
|
|
173
|
+
# Check for Union types (including Optional[T] which is Union[T, None])
|
|
174
|
+
if origin is type(None):
|
|
175
|
+
is_optional = True
|
|
176
|
+
actual_type = type(None)
|
|
177
|
+
|
|
178
|
+
# For now, treat all TypedDict fields as required unless they're explicitly Optional
|
|
179
|
+
schema = _python_type_to_json_schema_enhanced(actual_type)
|
|
180
|
+
params[field_name] = schema
|
|
181
|
+
|
|
182
|
+
if not is_optional:
|
|
183
|
+
required.append(field_name)
|
|
184
|
+
|
|
185
|
+
instance = cls.__new__(cls)
|
|
186
|
+
instance.parameters = params
|
|
187
|
+
instance.required = required
|
|
188
|
+
return instance
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def from_json_schema(
|
|
192
|
+
cls, properties: dict[str, Any], required: list[str] | None = None
|
|
193
|
+
) -> "ToolParams":
|
|
194
|
+
"""
|
|
195
|
+
Create ToolParams from an existing JSON Schema properties dict.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
properties: The "properties" section of a JSON Schema
|
|
199
|
+
required: List of required field names
|
|
200
|
+
"""
|
|
201
|
+
instance = cls.__new__(cls)
|
|
202
|
+
instance.parameters = properties
|
|
203
|
+
instance.required = required or []
|
|
204
|
+
return instance
|
|
205
|
+
|
|
206
|
+
def to_dict(self) -> dict[str, Any]:
|
|
207
|
+
"""
|
|
208
|
+
Convert to a dict with 'parameters' and 'required' keys.
|
|
209
|
+
Useful for unpacking into Tool constructor.
|
|
210
|
+
"""
|
|
211
|
+
return {"parameters": self.parameters, "required": self.required}
|
|
212
|
+
|
|
213
|
+
|
|
14
214
|
async def _load_all_mcp_tools(client: Client) -> list["Tool"]:
|
|
15
215
|
metas: list[MCPTool] = await client.list_tools()
|
|
16
216
|
|
|
@@ -39,6 +239,9 @@ async def _load_all_mcp_tools(client: Client) -> list["Tool"]:
|
|
|
39
239
|
|
|
40
240
|
tools: list[Tool] = []
|
|
41
241
|
for m in metas:
|
|
242
|
+
# Extract definitions from the schema (could be $defs or definitions)
|
|
243
|
+
definitions = m.inputSchema.get("$defs") or m.inputSchema.get("definitions")
|
|
244
|
+
|
|
42
245
|
tools.append(
|
|
43
246
|
Tool(
|
|
44
247
|
name=m.name,
|
|
@@ -46,6 +249,7 @@ async def _load_all_mcp_tools(client: Client) -> list["Tool"]:
|
|
|
46
249
|
parameters=m.inputSchema.get("properties", {}),
|
|
47
250
|
required=m.inputSchema.get("required", []),
|
|
48
251
|
additionalProperties=m.inputSchema.get("additionalProperties"),
|
|
252
|
+
definitions=definitions,
|
|
49
253
|
run=make_runner(m.name),
|
|
50
254
|
)
|
|
51
255
|
)
|
|
@@ -68,6 +272,8 @@ class Tool(BaseModel):
|
|
|
68
272
|
is_built_in: bool = False
|
|
69
273
|
type: str | None = None
|
|
70
274
|
built_in_args: dict[str, Any] = Field(default_factory=dict)
|
|
275
|
+
# JSON Schema definitions (for $ref support)
|
|
276
|
+
definitions: dict[str, Any] | None = None
|
|
71
277
|
|
|
72
278
|
@field_validator("name")
|
|
73
279
|
@classmethod
|
|
@@ -79,6 +285,24 @@ class Tool(BaseModel):
|
|
|
79
285
|
)
|
|
80
286
|
return v
|
|
81
287
|
|
|
288
|
+
@field_validator("parameters", mode="before")
|
|
289
|
+
@classmethod
|
|
290
|
+
def validate_parameters(cls, v: Any) -> dict[str, Any] | None:
|
|
291
|
+
"""Accept ToolParams objects and convert to dict for backwards compatibility."""
|
|
292
|
+
if isinstance(v, ToolParams):
|
|
293
|
+
return v.parameters
|
|
294
|
+
return v
|
|
295
|
+
|
|
296
|
+
def model_post_init(self, __context: Any) -> None:
|
|
297
|
+
"""
|
|
298
|
+
After validation, if parameters came from ToolParams, also update required list.
|
|
299
|
+
This is called by Pydantic after __init__.
|
|
300
|
+
"""
|
|
301
|
+
# This is a bit tricky - we need to capture the required list from ToolParams
|
|
302
|
+
# Since Pydantic has already converted it in the validator, we can't access it here
|
|
303
|
+
# Instead, we'll handle this differently in the convenience constructors
|
|
304
|
+
pass
|
|
305
|
+
|
|
82
306
|
def _is_async(self) -> bool:
|
|
83
307
|
return inspect.iscoroutinefunction(self.run)
|
|
84
308
|
|
|
@@ -143,7 +367,7 @@ class Tool(BaseModel):
|
|
|
143
367
|
param_type = type_hints.get(param_name, str)
|
|
144
368
|
|
|
145
369
|
# Convert Python types to JSON Schema types
|
|
146
|
-
json_type =
|
|
370
|
+
json_type = _python_type_to_json_schema_enhanced(param_type)
|
|
147
371
|
|
|
148
372
|
parameters[param_name] = json_type
|
|
149
373
|
|
|
@@ -209,6 +433,119 @@ class Tool(BaseModel):
|
|
|
209
433
|
return t
|
|
210
434
|
raise ValueError(f"Tool '{tool_name}' not found on that server")
|
|
211
435
|
|
|
436
|
+
@classmethod
|
|
437
|
+
def from_params(
|
|
438
|
+
cls,
|
|
439
|
+
name: str,
|
|
440
|
+
params: ToolParams,
|
|
441
|
+
*,
|
|
442
|
+
description: str | None = None,
|
|
443
|
+
run: Callable | None = None,
|
|
444
|
+
**kwargs,
|
|
445
|
+
) -> "Tool":
|
|
446
|
+
"""
|
|
447
|
+
Create a Tool from a ToolParams object.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
name: Tool name
|
|
451
|
+
params: ToolParams object defining the parameter schema
|
|
452
|
+
description: Optional description
|
|
453
|
+
run: Optional callable to execute the tool
|
|
454
|
+
**kwargs: Additional Tool arguments
|
|
455
|
+
|
|
456
|
+
Example:
|
|
457
|
+
params = ToolParams({"city": str, "age": int})
|
|
458
|
+
tool = Tool.from_params("get_user", params, run=my_function)
|
|
459
|
+
"""
|
|
460
|
+
return cls(
|
|
461
|
+
name=name,
|
|
462
|
+
description=description,
|
|
463
|
+
parameters=params.parameters,
|
|
464
|
+
required=params.required,
|
|
465
|
+
run=run,
|
|
466
|
+
**kwargs,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
@classmethod
|
|
470
|
+
def from_pydantic(
|
|
471
|
+
cls,
|
|
472
|
+
name: str,
|
|
473
|
+
model: Type[BaseModel],
|
|
474
|
+
*,
|
|
475
|
+
description: str | None = None,
|
|
476
|
+
run: Callable | None = None,
|
|
477
|
+
**kwargs,
|
|
478
|
+
) -> "Tool":
|
|
479
|
+
"""
|
|
480
|
+
Create a Tool from a Pydantic model.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
name: Tool name
|
|
484
|
+
model: Pydantic BaseModel class
|
|
485
|
+
description: Optional description (defaults to model docstring)
|
|
486
|
+
run: Optional callable to execute the tool
|
|
487
|
+
**kwargs: Additional Tool arguments
|
|
488
|
+
|
|
489
|
+
Example:
|
|
490
|
+
class UserQuery(BaseModel):
|
|
491
|
+
city: str
|
|
492
|
+
age: int
|
|
493
|
+
|
|
494
|
+
tool = Tool.from_pydantic("get_user", UserQuery, run=my_function)
|
|
495
|
+
"""
|
|
496
|
+
params = ToolParams.from_pydantic(model)
|
|
497
|
+
|
|
498
|
+
# Use model docstring as default description if not provided
|
|
499
|
+
if description is None and model.__doc__:
|
|
500
|
+
description = model.__doc__.strip()
|
|
501
|
+
|
|
502
|
+
return cls(
|
|
503
|
+
name=name,
|
|
504
|
+
description=description,
|
|
505
|
+
parameters=params.parameters,
|
|
506
|
+
required=params.required,
|
|
507
|
+
run=run,
|
|
508
|
+
**kwargs,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
@classmethod
|
|
512
|
+
def from_typed_dict(
|
|
513
|
+
cls,
|
|
514
|
+
name: str,
|
|
515
|
+
typed_dict: Type,
|
|
516
|
+
*,
|
|
517
|
+
description: str | None = None,
|
|
518
|
+
run: Callable | None = None,
|
|
519
|
+
**kwargs,
|
|
520
|
+
) -> "Tool":
|
|
521
|
+
"""
|
|
522
|
+
Create a Tool from a TypedDict.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
name: Tool name
|
|
526
|
+
typed_dict: TypedDict class
|
|
527
|
+
description: Optional description
|
|
528
|
+
run: Optional callable to execute the tool
|
|
529
|
+
**kwargs: Additional Tool arguments
|
|
530
|
+
|
|
531
|
+
Example:
|
|
532
|
+
class UserQuery(TypedDict):
|
|
533
|
+
city: str
|
|
534
|
+
age: int
|
|
535
|
+
|
|
536
|
+
tool = Tool.from_typed_dict("get_user", UserQuery, run=my_function)
|
|
537
|
+
"""
|
|
538
|
+
params = ToolParams.from_typed_dict(typed_dict)
|
|
539
|
+
|
|
540
|
+
return cls(
|
|
541
|
+
name=name,
|
|
542
|
+
description=description,
|
|
543
|
+
parameters=params.parameters,
|
|
544
|
+
required=params.required,
|
|
545
|
+
run=run,
|
|
546
|
+
**kwargs,
|
|
547
|
+
)
|
|
548
|
+
|
|
212
549
|
@staticmethod
|
|
213
550
|
def _tool_from_meta(meta: dict[str, Any], runner) -> "Tool":
|
|
214
551
|
props = meta["inputSchema"].get("properties", {})
|
|
@@ -225,22 +562,39 @@ class Tool(BaseModel):
|
|
|
225
562
|
|
|
226
563
|
@staticmethod
|
|
227
564
|
def _python_type_to_json_schema(python_type) -> dict[str, Any]:
|
|
228
|
-
"""
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
565
|
+
"""
|
|
566
|
+
Convert Python type to JSON Schema type definition.
|
|
567
|
+
Now delegates to enhanced version for better type support.
|
|
568
|
+
"""
|
|
569
|
+
return _python_type_to_json_schema_enhanced(python_type)
|
|
570
|
+
|
|
571
|
+
def _is_strict_mode_compatible(self) -> bool:
|
|
572
|
+
"""
|
|
573
|
+
Check if this tool's schema is compatible with OpenAI strict mode.
|
|
574
|
+
Strict mode requires all objects to have defined properties.
|
|
575
|
+
"""
|
|
576
|
+
|
|
577
|
+
def has_undefined_objects(schema: dict | list | Any) -> bool:
|
|
578
|
+
"""Recursively check for objects without defined properties."""
|
|
579
|
+
if isinstance(schema, dict):
|
|
580
|
+
# Check if this is an object type without properties
|
|
581
|
+
if schema.get("type") == "object":
|
|
582
|
+
# If additionalProperties is True or properties is missing/empty
|
|
583
|
+
if schema.get("additionalProperties") is True:
|
|
584
|
+
return True
|
|
585
|
+
if "properties" not in schema or not schema["properties"]:
|
|
586
|
+
return True
|
|
587
|
+
# Recursively check nested schemas
|
|
588
|
+
for value in schema.values():
|
|
589
|
+
if has_undefined_objects(value):
|
|
590
|
+
return True
|
|
591
|
+
elif isinstance(schema, list):
|
|
592
|
+
for item in schema:
|
|
593
|
+
if has_undefined_objects(item):
|
|
594
|
+
return True
|
|
595
|
+
return False
|
|
596
|
+
|
|
597
|
+
return not has_undefined_objects(self.parameters or {})
|
|
244
598
|
|
|
245
599
|
def _json_schema(
|
|
246
600
|
self, include_additional_properties=False, remove_defaults=False
|
|
@@ -248,7 +602,8 @@ class Tool(BaseModel):
|
|
|
248
602
|
def _add_additional_properties_recursive(
|
|
249
603
|
schema: dict | list | Any, remove_defaults: bool = False
|
|
250
604
|
) -> dict | list | Any:
|
|
251
|
-
"""Recursively add additionalProperties: false to all object-type schemas.
|
|
605
|
+
"""Recursively add additionalProperties: false to all object-type schemas.
|
|
606
|
+
In strict mode (when remove_defaults=True), also makes all properties required."""
|
|
252
607
|
if isinstance(schema, dict):
|
|
253
608
|
# Copy the dictionary to avoid modifying the original
|
|
254
609
|
new_schema = schema.copy()
|
|
@@ -264,6 +619,10 @@ class Tool(BaseModel):
|
|
|
264
619
|
if new_schema.get("type") == "object":
|
|
265
620
|
new_schema["additionalProperties"] = False
|
|
266
621
|
|
|
622
|
+
# In strict mode, all properties must be required
|
|
623
|
+
if remove_defaults and "properties" in new_schema:
|
|
624
|
+
new_schema["required"] = list(new_schema["properties"].keys())
|
|
625
|
+
|
|
267
626
|
# Remove default values if requested (for strict mode)
|
|
268
627
|
if remove_defaults and "default" in new_schema:
|
|
269
628
|
del new_schema["default"]
|
|
@@ -294,6 +653,14 @@ class Tool(BaseModel):
|
|
|
294
653
|
else:
|
|
295
654
|
processed_parameters = self.parameters
|
|
296
655
|
|
|
656
|
+
# Process definitions too
|
|
657
|
+
if self.definitions and include_additional_properties:
|
|
658
|
+
processed_definitions = _add_additional_properties_recursive(
|
|
659
|
+
self.definitions, remove_defaults
|
|
660
|
+
)
|
|
661
|
+
else:
|
|
662
|
+
processed_definitions = self.definitions
|
|
663
|
+
|
|
297
664
|
res = {
|
|
298
665
|
"type": "object",
|
|
299
666
|
"properties": processed_parameters,
|
|
@@ -303,6 +670,10 @@ class Tool(BaseModel):
|
|
|
303
670
|
if include_additional_properties:
|
|
304
671
|
res["additionalProperties"] = False
|
|
305
672
|
|
|
673
|
+
# Include definitions if present (for $ref support)
|
|
674
|
+
if processed_definitions:
|
|
675
|
+
res["$defs"] = processed_definitions
|
|
676
|
+
|
|
306
677
|
return res
|
|
307
678
|
|
|
308
679
|
# ---------- dumpers ----------
|
|
@@ -311,6 +682,11 @@ class Tool(BaseModel):
|
|
|
311
682
|
) -> dict[str, Any]:
|
|
312
683
|
if self.is_built_in:
|
|
313
684
|
return {"type": self.type, **self.built_in_args, **kwargs}
|
|
685
|
+
|
|
686
|
+
# Check if schema is compatible with strict mode
|
|
687
|
+
if strict and not self._is_strict_mode_compatible():
|
|
688
|
+
strict = False
|
|
689
|
+
|
|
314
690
|
if strict:
|
|
315
691
|
# For strict mode, remove defaults and make all parameters required
|
|
316
692
|
schema = self._json_schema(
|
lm_deluge/tracker.py
CHANGED
|
@@ -171,20 +171,24 @@ class StatusTracker:
|
|
|
171
171
|
)
|
|
172
172
|
|
|
173
173
|
# Display cumulative usage stats if available
|
|
174
|
-
if
|
|
174
|
+
if (
|
|
175
|
+
self.total_cost > 0
|
|
176
|
+
or self.total_input_tokens > 0
|
|
177
|
+
or self.total_output_tokens > 0
|
|
178
|
+
):
|
|
175
179
|
usage_parts = []
|
|
176
180
|
if self.total_cost > 0:
|
|
177
|
-
usage_parts.append(f"Cost: ${self.total_cost:.4f}")
|
|
181
|
+
usage_parts.append(f"💰 Cost: ${self.total_cost:.4f}")
|
|
178
182
|
if self.total_input_tokens > 0 or self.total_output_tokens > 0:
|
|
179
183
|
usage_parts.append(
|
|
180
|
-
f"Tokens: {self.total_input_tokens:,} in / {self.total_output_tokens:,} out"
|
|
184
|
+
f"🔡 Tokens: {self.total_input_tokens:,} in / {self.total_output_tokens:,} out"
|
|
181
185
|
)
|
|
182
186
|
if self.total_cache_read_tokens > 0:
|
|
183
187
|
usage_parts.append(f"Cache: {self.total_cache_read_tokens:,} read")
|
|
184
188
|
if self.total_cache_write_tokens > 0:
|
|
185
189
|
usage_parts.append(f"{self.total_cache_write_tokens:,} write")
|
|
186
190
|
|
|
187
|
-
print("
|
|
191
|
+
print(" ", " • ".join(usage_parts))
|
|
188
192
|
|
|
189
193
|
@property
|
|
190
194
|
def pbar(self) -> tqdm | None:
|
|
@@ -288,7 +292,9 @@ class StatusTracker:
|
|
|
288
292
|
usage_text = f" [gold3]Usage:[/gold3] {' • '.join(usage_parts)}"
|
|
289
293
|
|
|
290
294
|
if usage_text:
|
|
291
|
-
display = Group(
|
|
295
|
+
display = Group(
|
|
296
|
+
self._rich_progress, in_progress, capacity_text, usage_text
|
|
297
|
+
)
|
|
292
298
|
else:
|
|
293
299
|
display = Group(self._rich_progress, in_progress, capacity_text)
|
|
294
300
|
live.update(display)
|
lm_deluge/warnings.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import os
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
WARNINGS: dict[str, str] = {
|
|
6
|
+
"WARN_JSON_MODE_UNSUPPORTED": "JSON mode requested for {model_name} but response_format parameter not supported.",
|
|
7
|
+
"WARN_REASONING_UNSUPPORTED": "Ignoring reasoning_effort param for non-reasoning model: {model_name}.",
|
|
8
|
+
"WARN_CACHING_UNSUPPORTED": "Cache parameter '{cache_param}' is not supported, ignoring for {model_name}.",
|
|
9
|
+
"WARN_LOGPROBS_UNSUPPORTED": "Ignoring logprobs param for non-logprobs model: {model_name}",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def maybe_warn(warning: str, **kwargs):
|
|
14
|
+
if os.getenv(warning):
|
|
15
|
+
pass
|
|
16
|
+
else:
|
|
17
|
+
warnings.warn(WARNINGS[warning].format(**kwargs))
|
|
18
|
+
os.environ[warning] = "1"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def deprecated(replacement: str):
|
|
22
|
+
"""Decorator to mark methods as deprecated and suggest replacement.
|
|
23
|
+
|
|
24
|
+
Only shows the warning once per method to avoid spam.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
replacement: The name of the replacement method to suggest
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def decorator(func):
|
|
31
|
+
warning_key = f"DEPRECATED_{func.__module__}_{func.__qualname__}"
|
|
32
|
+
|
|
33
|
+
@functools.wraps(func)
|
|
34
|
+
def wrapper(*args, **kwargs):
|
|
35
|
+
if not os.getenv(warning_key):
|
|
36
|
+
warnings.warn(
|
|
37
|
+
f"{func.__name__} is deprecated, use {replacement} instead",
|
|
38
|
+
DeprecationWarning,
|
|
39
|
+
stacklevel=2,
|
|
40
|
+
)
|
|
41
|
+
os.environ[warning_key] = "1"
|
|
42
|
+
return func(*args, **kwargs)
|
|
43
|
+
|
|
44
|
+
return wrapper
|
|
45
|
+
|
|
46
|
+
return decorator
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lm_deluge
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.69
|
|
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
|
|
@@ -23,6 +23,8 @@ Requires-Dist: pdf2image
|
|
|
23
23
|
Requires-Dist: pillow
|
|
24
24
|
Requires-Dist: fastmcp>=2.4
|
|
25
25
|
Requires-Dist: rich
|
|
26
|
+
Provides-Extra: openai
|
|
27
|
+
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
26
28
|
Dynamic: license-file
|
|
27
29
|
|
|
28
30
|
# lm-deluge
|