lm-deluge 0.0.67__py3-none-any.whl → 0.0.90__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of lm-deluge might be problematic. Click here for more details.
- lm_deluge/__init__.py +1 -2
- lm_deluge/api_requests/anthropic.py +117 -22
- lm_deluge/api_requests/base.py +84 -11
- lm_deluge/api_requests/bedrock.py +30 -6
- lm_deluge/api_requests/chat_reasoning.py +4 -0
- lm_deluge/api_requests/gemini.py +166 -20
- lm_deluge/api_requests/openai.py +145 -25
- lm_deluge/batches.py +15 -45
- lm_deluge/client.py +309 -50
- lm_deluge/config.py +15 -3
- lm_deluge/models/__init__.py +14 -1
- lm_deluge/models/anthropic.py +29 -14
- lm_deluge/models/arcee.py +16 -0
- lm_deluge/models/deepseek.py +36 -4
- lm_deluge/models/google.py +42 -0
- lm_deluge/models/grok.py +24 -0
- lm_deluge/models/kimi.py +36 -0
- lm_deluge/models/minimax.py +18 -0
- lm_deluge/models/openai.py +100 -0
- lm_deluge/models/openrouter.py +133 -7
- lm_deluge/models/together.py +11 -0
- lm_deluge/models/zai.py +50 -0
- lm_deluge/pipelines/gepa/__init__.py +95 -0
- lm_deluge/pipelines/gepa/core.py +354 -0
- lm_deluge/pipelines/gepa/docs/samples.py +705 -0
- lm_deluge/pipelines/gepa/examples/01_synthetic_keywords.py +140 -0
- lm_deluge/pipelines/gepa/examples/02_gsm8k_math.py +261 -0
- lm_deluge/pipelines/gepa/examples/03_hotpotqa_multihop.py +300 -0
- lm_deluge/pipelines/gepa/examples/04_batch_classification.py +271 -0
- lm_deluge/pipelines/gepa/examples/simple_qa.py +129 -0
- lm_deluge/pipelines/gepa/optimizer.py +435 -0
- lm_deluge/pipelines/gepa/proposer.py +235 -0
- lm_deluge/pipelines/gepa/util.py +165 -0
- lm_deluge/{llm_tools → pipelines}/score.py +2 -2
- lm_deluge/{llm_tools → pipelines}/translate.py +5 -3
- lm_deluge/prompt.py +537 -88
- lm_deluge/request_context.py +7 -2
- lm_deluge/server/__init__.py +24 -0
- lm_deluge/server/__main__.py +144 -0
- lm_deluge/server/adapters.py +369 -0
- lm_deluge/server/app.py +388 -0
- lm_deluge/server/auth.py +71 -0
- lm_deluge/server/model_policy.py +215 -0
- lm_deluge/server/models_anthropic.py +172 -0
- lm_deluge/server/models_openai.py +175 -0
- lm_deluge/tool/__init__.py +1130 -0
- lm_deluge/tool/builtin/anthropic/__init__.py +300 -0
- lm_deluge/tool/builtin/anthropic/bash.py +0 -0
- lm_deluge/tool/builtin/anthropic/computer_use.py +0 -0
- lm_deluge/tool/builtin/gemini.py +59 -0
- lm_deluge/tool/builtin/openai.py +74 -0
- lm_deluge/tool/cua/__init__.py +173 -0
- lm_deluge/tool/cua/actions.py +148 -0
- lm_deluge/tool/cua/base.py +27 -0
- lm_deluge/tool/cua/batch.py +215 -0
- lm_deluge/tool/cua/converters.py +466 -0
- lm_deluge/tool/cua/kernel.py +702 -0
- lm_deluge/tool/cua/trycua.py +989 -0
- lm_deluge/tool/prefab/__init__.py +45 -0
- lm_deluge/tool/prefab/batch_tool.py +156 -0
- lm_deluge/tool/prefab/docs.py +1119 -0
- lm_deluge/tool/prefab/email.py +294 -0
- lm_deluge/tool/prefab/filesystem.py +1711 -0
- lm_deluge/tool/prefab/full_text_search/__init__.py +285 -0
- lm_deluge/tool/prefab/full_text_search/tantivy_index.py +396 -0
- lm_deluge/tool/prefab/memory.py +458 -0
- lm_deluge/tool/prefab/otc/__init__.py +165 -0
- lm_deluge/tool/prefab/otc/executor.py +281 -0
- lm_deluge/tool/prefab/otc/parse.py +188 -0
- lm_deluge/tool/prefab/random.py +212 -0
- lm_deluge/tool/prefab/rlm/__init__.py +296 -0
- lm_deluge/tool/prefab/rlm/executor.py +349 -0
- lm_deluge/tool/prefab/rlm/parse.py +144 -0
- lm_deluge/tool/prefab/sandbox/__init__.py +19 -0
- lm_deluge/tool/prefab/sandbox/daytona_sandbox.py +483 -0
- lm_deluge/tool/prefab/sandbox/docker_sandbox.py +609 -0
- lm_deluge/tool/prefab/sandbox/fargate_sandbox.py +546 -0
- lm_deluge/tool/prefab/sandbox/modal_sandbox.py +469 -0
- lm_deluge/tool/prefab/sandbox/seatbelt_sandbox.py +827 -0
- lm_deluge/tool/prefab/sheets.py +385 -0
- lm_deluge/tool/prefab/skills.py +0 -0
- lm_deluge/tool/prefab/subagents.py +233 -0
- lm_deluge/tool/prefab/todos.py +342 -0
- lm_deluge/tool/prefab/tool_search.py +169 -0
- lm_deluge/tool/prefab/web_search.py +199 -0
- lm_deluge/tracker.py +16 -13
- lm_deluge/util/schema.py +412 -0
- lm_deluge/warnings.py +8 -0
- {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.90.dist-info}/METADATA +23 -9
- lm_deluge-0.0.90.dist-info/RECORD +132 -0
- lm_deluge/built_in_tools/anthropic/__init__.py +0 -128
- lm_deluge/built_in_tools/openai.py +0 -28
- lm_deluge/presets/cerebras.py +0 -17
- lm_deluge/presets/meta.py +0 -13
- lm_deluge/tool.py +0 -849
- lm_deluge-0.0.67.dist-info/RECORD +0 -72
- lm_deluge/{llm_tools → pipelines}/__init__.py +1 -1
- /lm_deluge/{llm_tools → pipelines}/classify.py +0 -0
- /lm_deluge/{llm_tools → pipelines}/extract.py +0 -0
- /lm_deluge/{llm_tools → pipelines}/locate.py +0 -0
- /lm_deluge/{llm_tools → pipelines}/ocr.py +0 -0
- /lm_deluge/{built_in_tools/anthropic/bash.py → skills/anthropic.py} +0 -0
- /lm_deluge/{built_in_tools/anthropic/computer_use.py → skills/compat.py} +0 -0
- /lm_deluge/{built_in_tools → tool/builtin}/anthropic/editor.py +0 -0
- /lm_deluge/{built_in_tools → tool/builtin}/base.py +0 -0
- {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.90.dist-info}/WHEEL +0 -0
- {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.90.dist-info}/licenses/LICENSE +0 -0
- {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.90.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Web search prefab tool using Exa API."""
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from aiohttp import ClientSession, ClientTimeout
|
|
9
|
+
|
|
10
|
+
from .. import Tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AbstractWebSearchManager(abc.ABC):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
search_tool_name: str = "web_search",
|
|
17
|
+
fetch_tool_name: str = "web_fetch",
|
|
18
|
+
timeout: int = 30,
|
|
19
|
+
):
|
|
20
|
+
self.search_tool_name = search_tool_name
|
|
21
|
+
self.fetch_tool_name = fetch_tool_name
|
|
22
|
+
self.timeout = ClientTimeout(total=timeout)
|
|
23
|
+
self._tools: list[Tool] | None = None
|
|
24
|
+
|
|
25
|
+
@abc.abstractmethod
|
|
26
|
+
async def _search(self, query: str, limit: int) -> list[dict]:
|
|
27
|
+
"""Search the web and get results with content."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@abc.abstractmethod
|
|
31
|
+
async def _fetch(self, url: str) -> str:
|
|
32
|
+
"""Get the contents of a specific URL as markdown."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def get_tools(self) -> list[Tool]:
|
|
36
|
+
"""Return the web search tools."""
|
|
37
|
+
if self._tools is not None:
|
|
38
|
+
return self._tools
|
|
39
|
+
|
|
40
|
+
self._tools = [
|
|
41
|
+
Tool.from_function(self._search, name=self.search_tool_name),
|
|
42
|
+
Tool.from_function(self._fetch, name=self.fetch_tool_name),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
return self._tools
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ExaWebSearchManager(AbstractWebSearchManager):
|
|
49
|
+
"""
|
|
50
|
+
Simple web search tools using the Exa API.
|
|
51
|
+
|
|
52
|
+
Provides two tools:
|
|
53
|
+
- search: Search the web and get results with content
|
|
54
|
+
- fetch: Get the contents of a specific URL as markdown
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
search_tool_name: Name for the search tool (default: "web_search")
|
|
58
|
+
fetch_tool_name: Name for the fetch tool (default: "web_fetch")
|
|
59
|
+
timeout: Request timeout in seconds (default: 30)
|
|
60
|
+
|
|
61
|
+
Environment variables:
|
|
62
|
+
EXA_API_KEY: Your Exa API key (required)
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
```python
|
|
66
|
+
manager = ExaWebSearchManager()
|
|
67
|
+
tools = manager.get_tools()
|
|
68
|
+
```
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
BASE_URL = "https://api.exa.ai"
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
*,
|
|
76
|
+
search_tool_name: str = "web_search",
|
|
77
|
+
fetch_tool_name: str = "web_fetch",
|
|
78
|
+
timeout: int = 30,
|
|
79
|
+
max_contents_chars: int = 20_000,
|
|
80
|
+
):
|
|
81
|
+
super().__init__(
|
|
82
|
+
search_tool_name=search_tool_name,
|
|
83
|
+
fetch_tool_name=fetch_tool_name,
|
|
84
|
+
timeout=timeout,
|
|
85
|
+
)
|
|
86
|
+
self.max_contents_chars = max_contents_chars
|
|
87
|
+
|
|
88
|
+
async def _search( # type: ignore
|
|
89
|
+
self,
|
|
90
|
+
query: str,
|
|
91
|
+
limit: int = 5,
|
|
92
|
+
search_type: Literal["auto", "deep"] = "auto",
|
|
93
|
+
) -> str:
|
|
94
|
+
"""Search the web and return results with content."""
|
|
95
|
+
try:
|
|
96
|
+
key = os.getenv("EXA_API_KEY")
|
|
97
|
+
if not key:
|
|
98
|
+
raise ValueError("EXA_API_KEY environment variable not set")
|
|
99
|
+
data = {
|
|
100
|
+
"query": query,
|
|
101
|
+
"numResults": limit,
|
|
102
|
+
"type": search_type,
|
|
103
|
+
"contents": {"text": True},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
headers = {
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
"x-api-key": key,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async with ClientSession() as session:
|
|
112
|
+
async with session.post(
|
|
113
|
+
f"{self.BASE_URL}/search",
|
|
114
|
+
headers=headers,
|
|
115
|
+
json=data,
|
|
116
|
+
timeout=self.timeout,
|
|
117
|
+
) as response:
|
|
118
|
+
if response.status != 200:
|
|
119
|
+
error_text = await response.text()
|
|
120
|
+
return json.dumps(
|
|
121
|
+
{
|
|
122
|
+
"status": "error",
|
|
123
|
+
"error": f"API error: {response.status} - {error_text}",
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
result = await response.json()
|
|
127
|
+
|
|
128
|
+
results = []
|
|
129
|
+
for item in result.get("results", []):
|
|
130
|
+
results.append(
|
|
131
|
+
{
|
|
132
|
+
"title": item.get("title", ""),
|
|
133
|
+
"url": item.get("url", ""),
|
|
134
|
+
"text": item.get("text", ""),
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return json.dumps({"status": "success", "results": results}, indent=2)
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
return json.dumps({"status": "error", "error": str(e)})
|
|
142
|
+
|
|
143
|
+
async def _fetch(self, url: str) -> str:
|
|
144
|
+
"""Fetch the contents of a URL as markdown."""
|
|
145
|
+
try:
|
|
146
|
+
key = os.getenv("EXA_API_KEY")
|
|
147
|
+
if not key:
|
|
148
|
+
raise ValueError("EXA_API_KEY environment variable not set")
|
|
149
|
+
data = {
|
|
150
|
+
"urls": [url],
|
|
151
|
+
"text": {
|
|
152
|
+
"maxCharacters": self.max_contents_chars,
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
headers = {
|
|
157
|
+
"Content-Type": "application/json",
|
|
158
|
+
"x-api-key": key,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async with ClientSession() as session:
|
|
162
|
+
async with session.post(
|
|
163
|
+
f"{self.BASE_URL}/contents",
|
|
164
|
+
headers=headers,
|
|
165
|
+
json=data,
|
|
166
|
+
timeout=self.timeout,
|
|
167
|
+
) as response:
|
|
168
|
+
if response.status != 200:
|
|
169
|
+
error_text = await response.text()
|
|
170
|
+
return json.dumps(
|
|
171
|
+
{
|
|
172
|
+
"status": "error",
|
|
173
|
+
"error": f"API error: {response.status} - {error_text}",
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
result = await response.json()
|
|
177
|
+
|
|
178
|
+
results = result.get("results", [])
|
|
179
|
+
if not results:
|
|
180
|
+
return json.dumps(
|
|
181
|
+
{"status": "error", "error": "No content found for URL"}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
item = results[0]
|
|
185
|
+
return json.dumps(
|
|
186
|
+
{
|
|
187
|
+
"status": "success",
|
|
188
|
+
"title": item.get("title", ""),
|
|
189
|
+
"url": item.get("url", url),
|
|
190
|
+
"text": item.get("text", ""),
|
|
191
|
+
},
|
|
192
|
+
indent=2,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
except Exception as e:
|
|
196
|
+
return json.dumps({"status": "error", "error": str(e)})
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
__all__ = ["ExaWebSearchManager", "AbstractWebSearchManager"]
|
lm_deluge/tracker.py
CHANGED
|
@@ -157,19 +157,7 @@ class StatusTracker:
|
|
|
157
157
|
if response.usage.cache_write_tokens:
|
|
158
158
|
self.total_cache_write_tokens += response.usage.cache_write_tokens
|
|
159
159
|
|
|
160
|
-
def
|
|
161
|
-
# Close progress bar before printing final status
|
|
162
|
-
self.close_progress_bar()
|
|
163
|
-
|
|
164
|
-
if self.num_tasks_failed > 0:
|
|
165
|
-
print(
|
|
166
|
-
f"{self.num_tasks_failed} / {self.num_tasks_started} requests failed."
|
|
167
|
-
)
|
|
168
|
-
if self.num_rate_limit_errors > 0:
|
|
169
|
-
print(
|
|
170
|
-
f"{self.num_rate_limit_errors} rate limit errors received. Consider running at a lower rate."
|
|
171
|
-
)
|
|
172
|
-
|
|
160
|
+
def log_usage(self):
|
|
173
161
|
# Display cumulative usage stats if available
|
|
174
162
|
if (
|
|
175
163
|
self.total_cost > 0
|
|
@@ -190,6 +178,21 @@ class StatusTracker:
|
|
|
190
178
|
|
|
191
179
|
print(" ", " • ".join(usage_parts))
|
|
192
180
|
|
|
181
|
+
def log_final_status(self):
|
|
182
|
+
# Close progress bar before printing final status
|
|
183
|
+
self.close_progress_bar()
|
|
184
|
+
|
|
185
|
+
if self.num_tasks_failed > 0:
|
|
186
|
+
print(
|
|
187
|
+
f"{self.num_tasks_failed} / {self.num_tasks_started} requests failed."
|
|
188
|
+
)
|
|
189
|
+
if self.num_rate_limit_errors > 0:
|
|
190
|
+
print(
|
|
191
|
+
f"{self.num_rate_limit_errors} rate limit errors received. Consider running at a lower rate."
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
self.log_usage()
|
|
195
|
+
|
|
193
196
|
@property
|
|
194
197
|
def pbar(self) -> tqdm | None:
|
|
195
198
|
"""Backward compatibility property to access progress bar."""
|
lm_deluge/util/schema.py
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""Schema transformation utilities for structured outputs.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for transforming Pydantic models and JSON schemas
|
|
4
|
+
to be compatible with provider-specific structured output requirements (OpenAI, Anthropic).
|
|
5
|
+
|
|
6
|
+
Key functions:
|
|
7
|
+
- to_strict_json_schema: Convert Pydantic model to strict JSON schema
|
|
8
|
+
- transform_schema_for_openai: Apply OpenAI-specific transformations
|
|
9
|
+
- transform_schema_for_anthropic: Apply Anthropic-specific transformations
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import copy
|
|
15
|
+
import inspect
|
|
16
|
+
from typing import Any, TypeGuard, TYPE_CHECKING, Type
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import pydantic
|
|
23
|
+
from pydantic import BaseModel as _BaseModel
|
|
24
|
+
except ImportError:
|
|
25
|
+
pydantic = None
|
|
26
|
+
_BaseModel = None # type: ignore
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_pydantic_model(obj: Any) -> bool:
|
|
30
|
+
"""Check if an object is a Pydantic model class."""
|
|
31
|
+
if pydantic is None or _BaseModel is None:
|
|
32
|
+
return False
|
|
33
|
+
return inspect.isclass(obj) and issubclass(obj, _BaseModel)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_dict(obj: object) -> TypeGuard[dict[str, object]]:
|
|
37
|
+
"""Type guard for dictionaries."""
|
|
38
|
+
return isinstance(obj, dict)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def has_more_than_n_keys(obj: dict[str, object], n: int) -> bool:
|
|
42
|
+
"""Check if a dictionary has more than n keys."""
|
|
43
|
+
i = 0
|
|
44
|
+
for _ in obj.keys():
|
|
45
|
+
i += 1
|
|
46
|
+
if i > n:
|
|
47
|
+
return True
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def resolve_ref(*, root: dict[str, object], ref: str) -> object:
|
|
52
|
+
"""Resolve a JSON Schema $ref pointer.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
root: The root schema object
|
|
56
|
+
ref: The $ref string (e.g., "#/$defs/MyType")
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The resolved schema object
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: If the $ref format is invalid or cannot be resolved
|
|
63
|
+
"""
|
|
64
|
+
if not ref.startswith("#/"):
|
|
65
|
+
raise ValueError(f"Unexpected $ref format {ref!r}; Does not start with #/")
|
|
66
|
+
|
|
67
|
+
path = ref[2:].split("/")
|
|
68
|
+
resolved = root
|
|
69
|
+
for key in path:
|
|
70
|
+
value = resolved[key]
|
|
71
|
+
if not is_dict(value):
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Encountered non-dictionary entry while resolving {ref} - {resolved}"
|
|
74
|
+
)
|
|
75
|
+
resolved = value
|
|
76
|
+
|
|
77
|
+
return resolved
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def to_strict_json_schema(model: Type["BaseModel"]) -> dict[str, Any]:
|
|
81
|
+
"""Convert a Pydantic model to a strict JSON schema.
|
|
82
|
+
|
|
83
|
+
This function extracts the JSON schema from a Pydantic model and ensures
|
|
84
|
+
it conforms to the strict mode requirements for structured outputs.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
model: A Pydantic BaseModel class
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A JSON schema dict that conforms to strict mode requirements
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
TypeError: If the model is not a Pydantic BaseModel
|
|
94
|
+
ImportError: If pydantic is not installed
|
|
95
|
+
"""
|
|
96
|
+
if pydantic is None or _BaseModel is None:
|
|
97
|
+
raise ImportError(
|
|
98
|
+
"pydantic is required for Pydantic model support. "
|
|
99
|
+
"Install it with: pip install pydantic"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if not is_pydantic_model(model):
|
|
103
|
+
raise TypeError(
|
|
104
|
+
f"Expected a Pydantic BaseModel class, got {type(model).__name__}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
schema = model.model_json_schema()
|
|
108
|
+
return _ensure_strict_json_schema(schema, path=(), root=schema)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def prepare_output_schema(
|
|
112
|
+
schema_obj: Type["BaseModel"] | dict[str, Any],
|
|
113
|
+
) -> dict[str, Any]:
|
|
114
|
+
"""Normalize a user-provided schema into strict JSON schema form.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
schema_obj: Either a Pydantic BaseModel subclass or a JSON schema dict.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
A strict JSON schema suitable for provider-specific transformation.
|
|
121
|
+
|
|
122
|
+
Notes:
|
|
123
|
+
Dict schemas are deep-copied before normalization so the caller's
|
|
124
|
+
original object is left untouched.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
if is_pydantic_model(schema_obj):
|
|
128
|
+
return to_strict_json_schema(schema_obj) # type: ignore[arg-type]
|
|
129
|
+
|
|
130
|
+
if is_dict(schema_obj):
|
|
131
|
+
schema_copy = copy.deepcopy(schema_obj)
|
|
132
|
+
return _ensure_strict_json_schema(
|
|
133
|
+
schema_copy,
|
|
134
|
+
path=(),
|
|
135
|
+
root=schema_copy,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
raise TypeError(
|
|
139
|
+
"output_schema must be a Pydantic BaseModel subclass or a JSON schema dict"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _ensure_strict_json_schema(
|
|
144
|
+
json_schema: object,
|
|
145
|
+
*,
|
|
146
|
+
path: tuple[str, ...],
|
|
147
|
+
root: dict[str, object],
|
|
148
|
+
) -> dict[str, Any]:
|
|
149
|
+
"""Recursively ensure a JSON schema conforms to strict mode requirements.
|
|
150
|
+
|
|
151
|
+
This function:
|
|
152
|
+
- Adds additionalProperties: false to all objects
|
|
153
|
+
- Makes all properties required
|
|
154
|
+
- Removes unsupported constraints and adds them to descriptions
|
|
155
|
+
- Expands $refs that are mixed with other properties
|
|
156
|
+
- Processes $defs, anyOf, allOf, etc.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
json_schema: The schema to transform
|
|
160
|
+
path: Current path in the schema (for error messages)
|
|
161
|
+
root: The root schema (for resolving $refs)
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
The transformed schema
|
|
165
|
+
"""
|
|
166
|
+
if not is_dict(json_schema):
|
|
167
|
+
raise TypeError(f"Expected {json_schema} to be a dictionary; path={path}")
|
|
168
|
+
|
|
169
|
+
# Process $defs recursively
|
|
170
|
+
defs = json_schema.get("$defs")
|
|
171
|
+
if is_dict(defs):
|
|
172
|
+
for def_name, def_schema in defs.items():
|
|
173
|
+
_ensure_strict_json_schema(
|
|
174
|
+
def_schema, path=(*path, "$defs", def_name), root=root
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Process definitions recursively
|
|
178
|
+
definitions = json_schema.get("definitions")
|
|
179
|
+
if is_dict(definitions):
|
|
180
|
+
for definition_name, definition_schema in definitions.items():
|
|
181
|
+
_ensure_strict_json_schema(
|
|
182
|
+
definition_schema,
|
|
183
|
+
path=(*path, "definitions", definition_name),
|
|
184
|
+
root=root,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
typ = json_schema.get("type")
|
|
188
|
+
|
|
189
|
+
# Object types - add additionalProperties: false and make all fields required
|
|
190
|
+
if typ == "object" and "additionalProperties" not in json_schema:
|
|
191
|
+
json_schema["additionalProperties"] = False
|
|
192
|
+
|
|
193
|
+
properties = json_schema.get("properties")
|
|
194
|
+
if is_dict(properties):
|
|
195
|
+
# Make all properties required
|
|
196
|
+
json_schema["required"] = list(properties.keys())
|
|
197
|
+
|
|
198
|
+
# Process each property recursively
|
|
199
|
+
json_schema["properties"] = {
|
|
200
|
+
key: _ensure_strict_json_schema(
|
|
201
|
+
prop_schema, path=(*path, "properties", key), root=root
|
|
202
|
+
)
|
|
203
|
+
for key, prop_schema in properties.items()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# Arrays - process items schema
|
|
207
|
+
items = json_schema.get("items")
|
|
208
|
+
if is_dict(items):
|
|
209
|
+
json_schema["items"] = _ensure_strict_json_schema(
|
|
210
|
+
items, path=(*path, "items"), root=root
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Unions - process each variant
|
|
214
|
+
any_of = json_schema.get("anyOf")
|
|
215
|
+
if isinstance(any_of, list):
|
|
216
|
+
json_schema["anyOf"] = [
|
|
217
|
+
_ensure_strict_json_schema(
|
|
218
|
+
variant, path=(*path, "anyOf", str(i)), root=root
|
|
219
|
+
)
|
|
220
|
+
for i, variant in enumerate(any_of)
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
# Intersections - process each entry
|
|
224
|
+
all_of = json_schema.get("allOf")
|
|
225
|
+
if isinstance(all_of, list):
|
|
226
|
+
if len(all_of) == 1:
|
|
227
|
+
# Flatten single-element allOf
|
|
228
|
+
json_schema.update(
|
|
229
|
+
_ensure_strict_json_schema(
|
|
230
|
+
all_of[0], path=(*path, "allOf", "0"), root=root
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
json_schema.pop("allOf")
|
|
234
|
+
else:
|
|
235
|
+
json_schema["allOf"] = [
|
|
236
|
+
_ensure_strict_json_schema(
|
|
237
|
+
entry, path=(*path, "allOf", str(i)), root=root
|
|
238
|
+
)
|
|
239
|
+
for i, entry in enumerate(all_of)
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
# Remove None defaults (redundant with nullable)
|
|
243
|
+
if "default" in json_schema and json_schema["default"] is None:
|
|
244
|
+
json_schema.pop("default")
|
|
245
|
+
|
|
246
|
+
# Expand $refs that are mixed with other properties
|
|
247
|
+
ref = json_schema.get("$ref")
|
|
248
|
+
if ref and has_more_than_n_keys(json_schema, 1):
|
|
249
|
+
if not isinstance(ref, str):
|
|
250
|
+
raise ValueError(f"Received non-string $ref - {ref}")
|
|
251
|
+
|
|
252
|
+
resolved = resolve_ref(root=root, ref=ref)
|
|
253
|
+
if not is_dict(resolved):
|
|
254
|
+
raise ValueError(
|
|
255
|
+
f"Expected `$ref: {ref}` to resolve to a dictionary but got {resolved}"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Properties from json_schema take priority over $ref
|
|
259
|
+
json_schema.update({**resolved, **json_schema})
|
|
260
|
+
json_schema.pop("$ref")
|
|
261
|
+
|
|
262
|
+
# Re-process the expanded schema
|
|
263
|
+
return _ensure_strict_json_schema(json_schema, path=path, root=root)
|
|
264
|
+
|
|
265
|
+
return json_schema
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _move_constraints_to_description(
|
|
269
|
+
json_schema: dict[str, Any],
|
|
270
|
+
constraint_keys: list[str],
|
|
271
|
+
) -> dict[str, Any]:
|
|
272
|
+
"""Move unsupported constraints to the description field.
|
|
273
|
+
|
|
274
|
+
This helps the model follow constraints even when they can't be enforced
|
|
275
|
+
by the grammar.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
json_schema: The schema to modify
|
|
279
|
+
constraint_keys: List of constraint keys to move to description
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
The modified schema
|
|
283
|
+
"""
|
|
284
|
+
constraints_found = {}
|
|
285
|
+
|
|
286
|
+
for key in constraint_keys:
|
|
287
|
+
if key in json_schema:
|
|
288
|
+
constraints_found[key] = json_schema.pop(key)
|
|
289
|
+
|
|
290
|
+
if constraints_found:
|
|
291
|
+
description = json_schema.get("description", "")
|
|
292
|
+
constraint_str = ", ".join(
|
|
293
|
+
f"{key}: {value}" for key, value in constraints_found.items()
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
if description:
|
|
297
|
+
json_schema["description"] = f"{description}\n\n{{{constraint_str}}}"
|
|
298
|
+
else:
|
|
299
|
+
json_schema["description"] = f"{{{constraint_str}}}"
|
|
300
|
+
|
|
301
|
+
return json_schema
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def transform_schema_for_openai(schema: dict[str, Any]) -> dict[str, Any]:
|
|
305
|
+
"""Return a deep copy of the schema for OpenAI requests.
|
|
306
|
+
|
|
307
|
+
OpenAI Structured Outputs currently support the standard constraints we
|
|
308
|
+
rely on (min/max length, numeric bounds, etc.), so we intentionally leave
|
|
309
|
+
the schema untouched apart from copying it to prevent downstream mutation.
|
|
310
|
+
"""
|
|
311
|
+
|
|
312
|
+
return copy.deepcopy(schema)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _transform_schema_recursive_anthropic(
|
|
316
|
+
json_schema: dict[str, Any],
|
|
317
|
+
root: dict[str, Any],
|
|
318
|
+
) -> dict[str, Any]:
|
|
319
|
+
"""Recursively strip unsupported constraints for Anthropic."""
|
|
320
|
+
if not is_dict(json_schema):
|
|
321
|
+
return json_schema
|
|
322
|
+
|
|
323
|
+
# Process $defs
|
|
324
|
+
if "$defs" in json_schema and is_dict(json_schema["$defs"]):
|
|
325
|
+
for def_name, def_schema in json_schema["$defs"].items():
|
|
326
|
+
if is_dict(def_schema):
|
|
327
|
+
_transform_schema_recursive_anthropic(def_schema, root)
|
|
328
|
+
|
|
329
|
+
# Process definitions
|
|
330
|
+
if "definitions" in json_schema and is_dict(json_schema["definitions"]):
|
|
331
|
+
for def_name, def_schema in json_schema["definitions"].items():
|
|
332
|
+
if is_dict(def_schema):
|
|
333
|
+
_transform_schema_recursive_anthropic(def_schema, root)
|
|
334
|
+
|
|
335
|
+
typ = json_schema.get("type")
|
|
336
|
+
|
|
337
|
+
# Handle unsupported constraints based on type
|
|
338
|
+
if typ == "string":
|
|
339
|
+
_move_constraints_to_description(
|
|
340
|
+
json_schema,
|
|
341
|
+
["minLength", "maxLength", "pattern"],
|
|
342
|
+
)
|
|
343
|
+
elif typ in ("number", "integer"):
|
|
344
|
+
_move_constraints_to_description(
|
|
345
|
+
json_schema,
|
|
346
|
+
[
|
|
347
|
+
"minimum",
|
|
348
|
+
"maximum",
|
|
349
|
+
"exclusiveMinimum",
|
|
350
|
+
"exclusiveMaximum",
|
|
351
|
+
"multipleOf",
|
|
352
|
+
],
|
|
353
|
+
)
|
|
354
|
+
elif typ == "array":
|
|
355
|
+
_move_constraints_to_description(
|
|
356
|
+
json_schema,
|
|
357
|
+
[
|
|
358
|
+
"minItems",
|
|
359
|
+
"maxItems",
|
|
360
|
+
],
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Recursively process nested schemas
|
|
364
|
+
if "properties" in json_schema and is_dict(json_schema["properties"]):
|
|
365
|
+
for prop_name, prop_schema in json_schema["properties"].items():
|
|
366
|
+
if is_dict(prop_schema):
|
|
367
|
+
_transform_schema_recursive_anthropic(prop_schema, root)
|
|
368
|
+
|
|
369
|
+
if "items" in json_schema and is_dict(json_schema["items"]):
|
|
370
|
+
_transform_schema_recursive_anthropic(json_schema["items"], root)
|
|
371
|
+
|
|
372
|
+
if "anyOf" in json_schema and isinstance(json_schema["anyOf"], list):
|
|
373
|
+
for variant in json_schema["anyOf"]:
|
|
374
|
+
if is_dict(variant):
|
|
375
|
+
_transform_schema_recursive_anthropic(variant, root)
|
|
376
|
+
|
|
377
|
+
if "allOf" in json_schema and isinstance(json_schema["allOf"], list):
|
|
378
|
+
for entry in json_schema["allOf"]:
|
|
379
|
+
if is_dict(entry):
|
|
380
|
+
_transform_schema_recursive_anthropic(entry, root)
|
|
381
|
+
|
|
382
|
+
return json_schema
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def transform_schema_for_anthropic(schema: dict[str, Any]) -> dict[str, Any]:
|
|
386
|
+
"""Transform a JSON schema for Anthropic's structured output requirements."""
|
|
387
|
+
|
|
388
|
+
schema_copy = copy.deepcopy(schema)
|
|
389
|
+
return _transform_schema_recursive_anthropic(schema_copy, schema_copy)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def get_json_schema(obj: Type["BaseModel"] | dict[str, Any]) -> dict[str, Any]:
|
|
393
|
+
"""Get JSON schema from a Pydantic model or dict.
|
|
394
|
+
|
|
395
|
+
This is a convenience function that handles both Pydantic models
|
|
396
|
+
and raw dictionaries.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
obj: Either a Pydantic BaseModel class or a dict
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
The JSON schema dict
|
|
403
|
+
"""
|
|
404
|
+
if is_pydantic_model(obj):
|
|
405
|
+
# Type narrowing: if is_pydantic_model returns True, obj must have model_json_schema
|
|
406
|
+
return obj.model_json_schema() # type: ignore
|
|
407
|
+
elif is_dict(obj):
|
|
408
|
+
return obj # type: ignore
|
|
409
|
+
else:
|
|
410
|
+
raise TypeError(
|
|
411
|
+
f"Expected Pydantic BaseModel or dict, got {type(obj).__name__}"
|
|
412
|
+
)
|
lm_deluge/warnings.py
CHANGED
|
@@ -7,6 +7,14 @@ WARNINGS: dict[str, str] = {
|
|
|
7
7
|
"WARN_REASONING_UNSUPPORTED": "Ignoring reasoning_effort param for non-reasoning model: {model_name}.",
|
|
8
8
|
"WARN_CACHING_UNSUPPORTED": "Cache parameter '{cache_param}' is not supported, ignoring for {model_name}.",
|
|
9
9
|
"WARN_LOGPROBS_UNSUPPORTED": "Ignoring logprobs param for non-logprobs model: {model_name}",
|
|
10
|
+
"WARN_MINIMAL_TO_LOW": "'minimal' reasoning effort only allowed for gpt-5 models. Setting to 'low' for {model_name}.",
|
|
11
|
+
"WARN_MINIMAL_TO_NONE": "GPT-5.1 models don't support 'minimal' reasoning effort. Converting to 'none' for {model_name}.",
|
|
12
|
+
"WARN_XHIGH_TO_HIGH": "'xhigh' reasoning effort only supported for gpt-5.2 and gpt-5.1-codex-max. Using 'high' for {model_name}.",
|
|
13
|
+
"WARN_MEDIA_RESOLUTION_UNSUPPORTED": "media_resolution parameter is only supported for Gemini 3 models, ignoring for {model_name}.",
|
|
14
|
+
"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.",
|
|
15
|
+
"WARN_GEMINI3_NO_REASONING": "Gemini 3 requires reasoning (thinkingConfig). Setting thinkingConfig to low.",
|
|
16
|
+
"WARN_THINKING_BUDGET_AND_REASONING_EFFORT": "`reasoning_effort` and `thinking_budget` both provided. `thinking_budget` will take priority.",
|
|
17
|
+
"WARN_KIMI_THINKING_NO_REASONING": "kimi-k2-thinking works best with thinking enabled. set thinking_budget > 0 or reasoning_effort to anything but none",
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
|