lm-deluge 0.0.73__tar.gz → 0.0.75__tar.gz
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-0.0.73/src/lm_deluge.egg-info → lm_deluge-0.0.75}/PKG-INFO +1 -1
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/pyproject.toml +1 -1
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/api_requests/anthropic.py +35 -1
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/api_requests/bedrock.py +7 -4
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/api_requests/openai.py +70 -6
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/client.py +14 -1
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/config.py +2 -1
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/anthropic.py +2 -2
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/prompt.py +12 -2
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/request_context.py +6 -1
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/tool.py +60 -15
- lm_deluge-0.0.75/src/lm_deluge/util/schema.py +412 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75/src/lm_deluge.egg-info}/PKG-INFO +1 -1
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge.egg-info/SOURCES.txt +1 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/LICENSE +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/README.md +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/setup.cfg +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/__init__.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/api_requests/__init__.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/api_requests/base.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/api_requests/chat_reasoning.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/api_requests/common.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/api_requests/deprecated/bedrock.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/api_requests/deprecated/cohere.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/api_requests/deprecated/deepseek.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/api_requests/deprecated/mistral.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/api_requests/deprecated/vertex.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/api_requests/gemini.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/api_requests/mistral.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/api_requests/response.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/batches.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/built_in_tools/anthropic/__init__.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/built_in_tools/anthropic/bash.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/built_in_tools/anthropic/computer_use.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/built_in_tools/anthropic/editor.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/built_in_tools/base.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/built_in_tools/openai.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/cache.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/cli.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/embed.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/errors.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/file.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/image.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/llm_tools/__init__.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/llm_tools/classify.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/llm_tools/extract.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/llm_tools/locate.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/llm_tools/ocr.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/llm_tools/score.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/llm_tools/translate.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/mock_openai.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/__init__.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/bedrock.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/cerebras.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/cohere.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/deepseek.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/fireworks.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/google.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/grok.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/groq.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/kimi.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/meta.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/minimax.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/mistral.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/openai.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/openrouter.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/models/together.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/presets/cerebras.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/presets/meta.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/rerank.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/tracker.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/usage.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/util/harmony.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/util/json.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/util/logprobs.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/util/spatial.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/util/validation.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/util/xml.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/warnings.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge.egg-info/dependency_links.txt +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge.egg-info/requires.txt +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge.egg-info/top_level.txt +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/tests/test_builtin_tools.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/tests/test_file_upload.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/tests/test_mock_openai.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/tests/test_native_mcp_server.py +0 -0
- {lm_deluge-0.0.73 → lm_deluge-0.0.75}/tests/test_openrouter_generic.py +0 -0
|
@@ -12,6 +12,10 @@ from lm_deluge.prompt import (
|
|
|
12
12
|
from lm_deluge.request_context import RequestContext
|
|
13
13
|
from lm_deluge.tool import MCPServer, Tool
|
|
14
14
|
from lm_deluge.usage import Usage
|
|
15
|
+
from lm_deluge.util.schema import (
|
|
16
|
+
prepare_output_schema,
|
|
17
|
+
transform_schema_for_anthropic,
|
|
18
|
+
)
|
|
15
19
|
|
|
16
20
|
from ..models import APIModel
|
|
17
21
|
from .base import APIRequestBase, APIResponse
|
|
@@ -84,12 +88,42 @@ def _build_anthropic_request(
|
|
|
84
88
|
if "temperature" in request_json and "top_p" in request_json:
|
|
85
89
|
request_json.pop("top_p")
|
|
86
90
|
|
|
91
|
+
# Handle structured outputs (output_format)
|
|
92
|
+
if context.output_schema:
|
|
93
|
+
if model.supports_json:
|
|
94
|
+
base_schema = prepare_output_schema(context.output_schema)
|
|
95
|
+
|
|
96
|
+
# Apply Anthropic-specific transformations (move unsupported constraints to description)
|
|
97
|
+
transformed_schema = transform_schema_for_anthropic(base_schema)
|
|
98
|
+
|
|
99
|
+
_add_beta(base_headers, "structured-outputs-2025-11-13")
|
|
100
|
+
request_json["output_format"] = {
|
|
101
|
+
"type": "json_schema",
|
|
102
|
+
"schema": transformed_schema,
|
|
103
|
+
}
|
|
104
|
+
else:
|
|
105
|
+
print(
|
|
106
|
+
f"WARNING: Model {model.name} does not support structured outputs. Ignoring output_schema."
|
|
107
|
+
)
|
|
108
|
+
elif sampling_params.json_mode:
|
|
109
|
+
# Anthropic doesn't support basic json_mode without a schema
|
|
110
|
+
print(
|
|
111
|
+
"WARNING: Anthropic does not support basic json_mode without a schema. "
|
|
112
|
+
"Use output_schema parameter for structured JSON outputs."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Add beta header for strict tools when enabled
|
|
116
|
+
if tools and sampling_params.strict_tools and model.supports_json:
|
|
117
|
+
_add_beta(base_headers, "structured-outputs-2025-11-13")
|
|
118
|
+
|
|
87
119
|
if tools:
|
|
88
120
|
mcp_servers = []
|
|
89
121
|
tool_definitions = []
|
|
90
122
|
for tool in tools:
|
|
91
123
|
if isinstance(tool, Tool):
|
|
92
|
-
|
|
124
|
+
# Only use strict mode if model supports structured outputs
|
|
125
|
+
use_strict = sampling_params.strict_tools and model.supports_json
|
|
126
|
+
tool_definitions.append(tool.dump_for("anthropic", strict=use_strict))
|
|
93
127
|
elif isinstance(tool, dict) and "url" in tool:
|
|
94
128
|
_add_beta(base_headers, "mcp-client-2025-04-04")
|
|
95
129
|
mcp_servers.append(tool)
|
|
@@ -106,7 +106,8 @@ async def _build_anthropic_bedrock_request(
|
|
|
106
106
|
tool_definitions = []
|
|
107
107
|
for tool in tools:
|
|
108
108
|
if isinstance(tool, Tool):
|
|
109
|
-
|
|
109
|
+
# Bedrock doesn't have the strict-mode betas Anthropic exposes yet
|
|
110
|
+
tool_definitions.append(tool.dump_for("anthropic", strict=False))
|
|
110
111
|
elif isinstance(tool, dict):
|
|
111
112
|
tool_definitions.append(tool)
|
|
112
113
|
# add betas if needed
|
|
@@ -124,7 +125,9 @@ async def _build_anthropic_bedrock_request(
|
|
|
124
125
|
# Convert to individual tools locally (like OpenAI does)
|
|
125
126
|
individual_tools = await tool.to_tools()
|
|
126
127
|
for individual_tool in individual_tools:
|
|
127
|
-
tool_definitions.append(
|
|
128
|
+
tool_definitions.append(
|
|
129
|
+
individual_tool.dump_for("anthropic", strict=False)
|
|
130
|
+
)
|
|
128
131
|
|
|
129
132
|
# Add cache control to last tool if tools_only caching is specified
|
|
130
133
|
if cache_pattern == "tools_only" and tool_definitions:
|
|
@@ -194,11 +197,11 @@ async def _build_openai_bedrock_request(
|
|
|
194
197
|
request_tools = []
|
|
195
198
|
for tool in tools:
|
|
196
199
|
if isinstance(tool, Tool):
|
|
197
|
-
request_tools.append(tool.dump_for("openai-completions"))
|
|
200
|
+
request_tools.append(tool.dump_for("openai-completions", strict=False))
|
|
198
201
|
elif isinstance(tool, MCPServer):
|
|
199
202
|
as_tools = await tool.to_tools()
|
|
200
203
|
request_tools.extend(
|
|
201
|
-
[t.dump_for("openai-completions") for t in as_tools]
|
|
204
|
+
[t.dump_for("openai-completions", strict=False) for t in as_tools]
|
|
202
205
|
)
|
|
203
206
|
request_json["tools"] = request_tools
|
|
204
207
|
|
|
@@ -9,6 +9,10 @@ from aiohttp import ClientResponse
|
|
|
9
9
|
from lm_deluge.request_context import RequestContext
|
|
10
10
|
from lm_deluge.tool import MCPServer, Tool
|
|
11
11
|
from lm_deluge.warnings import maybe_warn
|
|
12
|
+
from lm_deluge.util.schema import (
|
|
13
|
+
prepare_output_schema,
|
|
14
|
+
transform_schema_for_openai,
|
|
15
|
+
)
|
|
12
16
|
|
|
13
17
|
from ..config import SamplingParams
|
|
14
18
|
from ..models import APIModel
|
|
@@ -83,17 +87,48 @@ async def _build_oa_chat_request(
|
|
|
83
87
|
request_json["logprobs"] = True
|
|
84
88
|
if sampling_params.top_logprobs is not None:
|
|
85
89
|
request_json["top_logprobs"] = sampling_params.top_logprobs
|
|
86
|
-
|
|
90
|
+
|
|
91
|
+
# Handle structured outputs (output_schema takes precedence over json_mode)
|
|
92
|
+
if context.output_schema:
|
|
93
|
+
if model.supports_json:
|
|
94
|
+
base_schema = prepare_output_schema(context.output_schema)
|
|
95
|
+
|
|
96
|
+
# Apply OpenAI-specific transformations (currently passthrough with copy)
|
|
97
|
+
transformed_schema = transform_schema_for_openai(base_schema)
|
|
98
|
+
|
|
99
|
+
request_json["response_format"] = {
|
|
100
|
+
"type": "json_schema",
|
|
101
|
+
"json_schema": {
|
|
102
|
+
"name": "response",
|
|
103
|
+
"schema": transformed_schema,
|
|
104
|
+
"strict": True,
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
else:
|
|
108
|
+
print(
|
|
109
|
+
f"WARNING: Model {model.name} does not support structured outputs. Ignoring output_schema."
|
|
110
|
+
)
|
|
111
|
+
elif sampling_params.json_mode and model.supports_json:
|
|
87
112
|
request_json["response_format"] = {"type": "json_object"}
|
|
113
|
+
|
|
88
114
|
if tools:
|
|
89
115
|
request_tools = []
|
|
90
116
|
for tool in tools:
|
|
91
117
|
if isinstance(tool, Tool):
|
|
92
|
-
request_tools.append(
|
|
118
|
+
request_tools.append(
|
|
119
|
+
tool.dump_for(
|
|
120
|
+
"openai-completions", strict=sampling_params.strict_tools
|
|
121
|
+
)
|
|
122
|
+
)
|
|
93
123
|
elif isinstance(tool, MCPServer):
|
|
94
124
|
as_tools = await tool.to_tools()
|
|
95
125
|
request_tools.extend(
|
|
96
|
-
[
|
|
126
|
+
[
|
|
127
|
+
t.dump_for(
|
|
128
|
+
"openai-completions", strict=sampling_params.strict_tools
|
|
129
|
+
)
|
|
130
|
+
for t in as_tools
|
|
131
|
+
]
|
|
97
132
|
)
|
|
98
133
|
request_json["tools"] = request_tools
|
|
99
134
|
return request_json
|
|
@@ -297,7 +332,27 @@ async def _build_oa_responses_request(
|
|
|
297
332
|
if sampling_params.reasoning_effort:
|
|
298
333
|
maybe_warn("WARN_REASONING_UNSUPPORTED", model_name=context.model_name)
|
|
299
334
|
|
|
300
|
-
|
|
335
|
+
# Handle structured outputs (output_schema takes precedence over json_mode)
|
|
336
|
+
if context.output_schema:
|
|
337
|
+
if model.supports_json:
|
|
338
|
+
base_schema = prepare_output_schema(context.output_schema)
|
|
339
|
+
|
|
340
|
+
# Apply OpenAI-specific transformations (currently passthrough with copy)
|
|
341
|
+
transformed_schema = transform_schema_for_openai(base_schema)
|
|
342
|
+
|
|
343
|
+
request_json["text"] = {
|
|
344
|
+
"format": {
|
|
345
|
+
"type": "json_schema",
|
|
346
|
+
"name": "response",
|
|
347
|
+
"schema": transformed_schema,
|
|
348
|
+
"strict": True,
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
else:
|
|
352
|
+
print(
|
|
353
|
+
f"WARNING: Model {model.name} does not support structured outputs. Ignoring output_schema."
|
|
354
|
+
)
|
|
355
|
+
elif sampling_params.json_mode and model.supports_json:
|
|
301
356
|
request_json["text"] = {"format": {"type": "json_object"}}
|
|
302
357
|
|
|
303
358
|
# Handle tools
|
|
@@ -305,7 +360,9 @@ async def _build_oa_responses_request(
|
|
|
305
360
|
# Add regular function tools
|
|
306
361
|
for tool in tools or []:
|
|
307
362
|
if isinstance(tool, Tool):
|
|
308
|
-
request_tools.append(
|
|
363
|
+
request_tools.append(
|
|
364
|
+
tool.dump_for("openai-responses", strict=sampling_params.strict_tools)
|
|
365
|
+
)
|
|
309
366
|
elif isinstance(tool, dict):
|
|
310
367
|
# if computer use, make sure model supports it
|
|
311
368
|
if tool["type"] == "computer_use_preview":
|
|
@@ -317,7 +374,14 @@ async def _build_oa_responses_request(
|
|
|
317
374
|
elif isinstance(tool, MCPServer):
|
|
318
375
|
if context.force_local_mcp:
|
|
319
376
|
as_tools = await tool.to_tools()
|
|
320
|
-
request_tools.extend(
|
|
377
|
+
request_tools.extend(
|
|
378
|
+
[
|
|
379
|
+
t.dump_for(
|
|
380
|
+
"openai-responses", strict=sampling_params.strict_tools
|
|
381
|
+
)
|
|
382
|
+
for t in as_tools
|
|
383
|
+
]
|
|
384
|
+
)
|
|
321
385
|
else:
|
|
322
386
|
request_tools.append(tool.for_openai_responses())
|
|
323
387
|
|
|
@@ -561,6 +561,7 @@ class _LLMClient(BaseModel):
|
|
|
561
561
|
return_completions_only: Literal[True],
|
|
562
562
|
show_progress: bool = ...,
|
|
563
563
|
tools: list[Tool | dict | MCPServer] | None = ...,
|
|
564
|
+
output_schema: type[BaseModel] | dict | None = ...,
|
|
564
565
|
cache: CachePattern | None = ...,
|
|
565
566
|
service_tier: Literal["auto", "default", "flex", "priority"] | None = ...,
|
|
566
567
|
) -> list[str | None]: ...
|
|
@@ -573,6 +574,7 @@ class _LLMClient(BaseModel):
|
|
|
573
574
|
return_completions_only: Literal[False] = ...,
|
|
574
575
|
show_progress: bool = ...,
|
|
575
576
|
tools: list[Tool | dict | MCPServer] | None = ...,
|
|
577
|
+
output_schema: type[BaseModel] | dict | None = ...,
|
|
576
578
|
cache: CachePattern | None = ...,
|
|
577
579
|
service_tier: Literal["auto", "default", "flex", "priority"] | None = ...,
|
|
578
580
|
) -> list[APIResponse]: ...
|
|
@@ -584,6 +586,7 @@ class _LLMClient(BaseModel):
|
|
|
584
586
|
return_completions_only: bool = False,
|
|
585
587
|
show_progress: bool = True,
|
|
586
588
|
tools: list[Tool | dict | MCPServer] | None = None,
|
|
589
|
+
output_schema: type[BaseModel] | dict | None = None,
|
|
587
590
|
cache: CachePattern | None = None,
|
|
588
591
|
service_tier: Literal["auto", "default", "flex", "priority"] | None = None,
|
|
589
592
|
) -> list[APIResponse] | list[str | None] | dict[str, int]:
|
|
@@ -612,6 +615,7 @@ class _LLMClient(BaseModel):
|
|
|
612
615
|
task_id = self.start_nowait(
|
|
613
616
|
prompt,
|
|
614
617
|
tools=tools,
|
|
618
|
+
output_schema=output_schema,
|
|
615
619
|
cache=cache,
|
|
616
620
|
service_tier=service_tier,
|
|
617
621
|
)
|
|
@@ -657,6 +661,7 @@ class _LLMClient(BaseModel):
|
|
|
657
661
|
return_completions_only: bool = False,
|
|
658
662
|
show_progress=True,
|
|
659
663
|
tools: list[Tool | dict | MCPServer] | None = None,
|
|
664
|
+
output_schema: type[BaseModel] | dict | None = None,
|
|
660
665
|
cache: CachePattern | None = None,
|
|
661
666
|
):
|
|
662
667
|
return asyncio.run(
|
|
@@ -665,6 +670,7 @@ class _LLMClient(BaseModel):
|
|
|
665
670
|
return_completions_only=return_completions_only,
|
|
666
671
|
show_progress=show_progress,
|
|
667
672
|
tools=tools,
|
|
673
|
+
output_schema=output_schema,
|
|
668
674
|
cache=cache,
|
|
669
675
|
)
|
|
670
676
|
)
|
|
@@ -688,6 +694,7 @@ class _LLMClient(BaseModel):
|
|
|
688
694
|
prompt: Prompt,
|
|
689
695
|
*,
|
|
690
696
|
tools: list[Tool | dict | MCPServer] | None = None,
|
|
697
|
+
output_schema: type[BaseModel] | dict | None = None,
|
|
691
698
|
cache: CachePattern | None = None,
|
|
692
699
|
service_tier: Literal["auto", "default", "flex", "priority"] | None = None,
|
|
693
700
|
) -> int:
|
|
@@ -706,6 +713,7 @@ class _LLMClient(BaseModel):
|
|
|
706
713
|
request_timeout=self.request_timeout,
|
|
707
714
|
status_tracker=tracker,
|
|
708
715
|
tools=tools,
|
|
716
|
+
output_schema=output_schema,
|
|
709
717
|
cache=cache,
|
|
710
718
|
use_responses_api=self.use_responses_api,
|
|
711
719
|
background=self.background,
|
|
@@ -723,11 +731,16 @@ class _LLMClient(BaseModel):
|
|
|
723
731
|
prompt: Prompt,
|
|
724
732
|
*,
|
|
725
733
|
tools: list[Tool | dict | MCPServer] | None = None,
|
|
734
|
+
output_schema: type[BaseModel] | dict | None = None,
|
|
726
735
|
cache: CachePattern | None = None,
|
|
727
736
|
service_tier: Literal["auto", "default", "flex", "priority"] | None = None,
|
|
728
737
|
) -> APIResponse:
|
|
729
738
|
task_id = self.start_nowait(
|
|
730
|
-
prompt,
|
|
739
|
+
prompt,
|
|
740
|
+
tools=tools,
|
|
741
|
+
output_schema=output_schema,
|
|
742
|
+
cache=cache,
|
|
743
|
+
service_tier=service_tier,
|
|
731
744
|
)
|
|
732
745
|
return await self.wait_for(task_id)
|
|
733
746
|
|
|
@@ -7,10 +7,11 @@ class SamplingParams(BaseModel):
|
|
|
7
7
|
temperature: float = 0.0
|
|
8
8
|
top_p: float = 1.0
|
|
9
9
|
json_mode: bool = False
|
|
10
|
-
max_new_tokens: int =
|
|
10
|
+
max_new_tokens: int = 2_048
|
|
11
11
|
reasoning_effort: Literal["low", "medium", "high", "minimal", "none", None] = None
|
|
12
12
|
logprobs: bool = False
|
|
13
13
|
top_logprobs: int | None = None
|
|
14
|
+
strict_tools: bool = True
|
|
14
15
|
|
|
15
16
|
def to_vllm(self):
|
|
16
17
|
try:
|
|
@@ -27,7 +27,7 @@ ANTHROPIC_MODELS = {
|
|
|
27
27
|
"name": "claude-sonnet-4-5-20250929",
|
|
28
28
|
"api_base": "https://api.anthropic.com/v1",
|
|
29
29
|
"api_key_env_var": "ANTHROPIC_API_KEY",
|
|
30
|
-
"supports_json":
|
|
30
|
+
"supports_json": True,
|
|
31
31
|
"api_spec": "anthropic",
|
|
32
32
|
"input_cost": 3.0,
|
|
33
33
|
"cached_input_cost": 0.30,
|
|
@@ -39,7 +39,7 @@ ANTHROPIC_MODELS = {
|
|
|
39
39
|
"name": "claude-opus-4-1-20250805",
|
|
40
40
|
"api_base": "https://api.anthropic.com/v1",
|
|
41
41
|
"api_key_env_var": "ANTHROPIC_API_KEY",
|
|
42
|
-
"supports_json":
|
|
42
|
+
"supports_json": True,
|
|
43
43
|
"api_spec": "anthropic",
|
|
44
44
|
"input_cost": 15.0,
|
|
45
45
|
"cached_input_cost": 1.50,
|
|
@@ -1195,14 +1195,24 @@ class Conversation:
|
|
|
1195
1195
|
|
|
1196
1196
|
@classmethod
|
|
1197
1197
|
def from_unknown(
|
|
1198
|
-
cls, messages: list[dict], *, system: str | list[dict] | None = None
|
|
1198
|
+
cls, messages: list[dict] | dict, *, system: str | list[dict] | None = None
|
|
1199
1199
|
) -> tuple["Conversation", str]:
|
|
1200
1200
|
"""Attempt to convert provider-formatted messages without knowing the provider.
|
|
1201
1201
|
|
|
1202
1202
|
Returns the parsed conversation together with the provider label that succeeded
|
|
1203
|
-
("openai" or "
|
|
1203
|
+
("openai", "anthropic", or "log").
|
|
1204
1204
|
"""
|
|
1205
1205
|
|
|
1206
|
+
# Check if input is in log format (output from to_log())
|
|
1207
|
+
if isinstance(messages, dict) and "messages" in messages:
|
|
1208
|
+
return cls.from_log(messages), "log"
|
|
1209
|
+
|
|
1210
|
+
# Ensure messages is a list for provider detection
|
|
1211
|
+
if not isinstance(messages, list):
|
|
1212
|
+
raise ValueError(
|
|
1213
|
+
"messages must be a list of dicts or a dict with 'messages' key"
|
|
1214
|
+
)
|
|
1215
|
+
|
|
1206
1216
|
def _detect_provider() -> str:
|
|
1207
1217
|
has_openai_markers = False
|
|
1208
1218
|
has_anthropic_markers = False
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
2
|
from functools import cached_property
|
|
3
|
-
from typing import Any, Callable
|
|
3
|
+
from typing import Any, Callable, TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from .config import SamplingParams
|
|
6
6
|
from .prompt import CachePattern, Conversation
|
|
7
7
|
from .tracker import StatusTracker
|
|
8
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
@dataclass
|
|
11
14
|
class RequestContext:
|
|
@@ -32,6 +35,7 @@ class RequestContext:
|
|
|
32
35
|
|
|
33
36
|
# Optional features
|
|
34
37
|
tools: list | None = None
|
|
38
|
+
output_schema: "type[BaseModel] | dict | None" = None
|
|
35
39
|
cache: CachePattern | None = None
|
|
36
40
|
use_responses_api: bool = False
|
|
37
41
|
background: bool = False
|
|
@@ -66,6 +70,7 @@ class RequestContext:
|
|
|
66
70
|
"results_arr": self.results_arr,
|
|
67
71
|
"callback": self.callback,
|
|
68
72
|
"tools": self.tools,
|
|
73
|
+
"output_schema": self.output_schema,
|
|
69
74
|
"cache": self.cache,
|
|
70
75
|
"use_responses_api": self.use_responses_api,
|
|
71
76
|
"background": self.background,
|
|
@@ -713,17 +713,40 @@ class Tool(BaseModel):
|
|
|
713
713
|
"""just an alias for the above"""
|
|
714
714
|
return self.for_openai_completions(strict=strict, **kwargs)
|
|
715
715
|
|
|
716
|
-
def for_openai_responses(self, **kwargs) -> dict[str, Any]:
|
|
716
|
+
def for_openai_responses(self, *, strict: bool = True, **kwargs) -> dict[str, Any]:
|
|
717
717
|
if self.is_built_in:
|
|
718
718
|
return {"type": self.type, **self.built_in_args, **kwargs}
|
|
719
|
-
return {
|
|
720
|
-
"type": "function",
|
|
721
|
-
"name": self.name,
|
|
722
|
-
"description": self.description,
|
|
723
|
-
"parameters": self._json_schema(include_additional_properties=True),
|
|
724
|
-
}
|
|
725
719
|
|
|
726
|
-
|
|
720
|
+
# Check if schema is compatible with strict mode
|
|
721
|
+
if strict and not self._is_strict_mode_compatible():
|
|
722
|
+
strict = False
|
|
723
|
+
|
|
724
|
+
if strict:
|
|
725
|
+
# For strict mode, remove defaults and make all parameters required
|
|
726
|
+
schema = self._json_schema(
|
|
727
|
+
include_additional_properties=True, remove_defaults=True
|
|
728
|
+
)
|
|
729
|
+
schema["required"] = list(
|
|
730
|
+
(self.parameters or {}).keys()
|
|
731
|
+
) # All parameters required in strict mode
|
|
732
|
+
|
|
733
|
+
return {
|
|
734
|
+
"type": "function",
|
|
735
|
+
"name": self.name,
|
|
736
|
+
"description": self.description,
|
|
737
|
+
"parameters": schema,
|
|
738
|
+
"strict": True,
|
|
739
|
+
}
|
|
740
|
+
else:
|
|
741
|
+
# For non-strict mode, use the original required list
|
|
742
|
+
return {
|
|
743
|
+
"type": "function",
|
|
744
|
+
"name": self.name,
|
|
745
|
+
"description": self.description,
|
|
746
|
+
"parameters": self._json_schema(include_additional_properties=True),
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
def for_anthropic(self, *, strict: bool = True, **kwargs) -> dict[str, Any]:
|
|
727
750
|
# built-in tools have "name", "type", maybe metadata
|
|
728
751
|
if self.is_built_in:
|
|
729
752
|
return {
|
|
@@ -732,11 +755,33 @@ class Tool(BaseModel):
|
|
|
732
755
|
**self.built_in_args,
|
|
733
756
|
**kwargs,
|
|
734
757
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
758
|
+
|
|
759
|
+
# Check if schema is compatible with strict mode
|
|
760
|
+
if strict and not self._is_strict_mode_compatible():
|
|
761
|
+
strict = False
|
|
762
|
+
|
|
763
|
+
if strict:
|
|
764
|
+
# For strict mode, remove defaults and make all parameters required
|
|
765
|
+
schema = self._json_schema(
|
|
766
|
+
include_additional_properties=True, remove_defaults=True
|
|
767
|
+
)
|
|
768
|
+
schema["required"] = list(
|
|
769
|
+
(self.parameters or {}).keys()
|
|
770
|
+
) # All parameters required in strict mode
|
|
771
|
+
|
|
772
|
+
return {
|
|
773
|
+
"name": self.name,
|
|
774
|
+
"description": self.description,
|
|
775
|
+
"input_schema": schema,
|
|
776
|
+
"strict": True,
|
|
777
|
+
}
|
|
778
|
+
else:
|
|
779
|
+
# For non-strict mode, use the original required list
|
|
780
|
+
return {
|
|
781
|
+
"name": self.name,
|
|
782
|
+
"description": self.description,
|
|
783
|
+
"input_schema": self._json_schema(),
|
|
784
|
+
}
|
|
740
785
|
|
|
741
786
|
def for_google(self) -> dict[str, Any]:
|
|
742
787
|
"""
|
|
@@ -759,11 +804,11 @@ class Tool(BaseModel):
|
|
|
759
804
|
**kw,
|
|
760
805
|
) -> dict[str, Any]:
|
|
761
806
|
if provider == "openai-responses":
|
|
762
|
-
return self.for_openai_responses()
|
|
807
|
+
return self.for_openai_responses(**kw)
|
|
763
808
|
if provider == "openai-completions":
|
|
764
809
|
return self.for_openai_completions(**kw)
|
|
765
810
|
if provider == "anthropic":
|
|
766
|
-
return self.for_anthropic()
|
|
811
|
+
return self.for_anthropic(**kw)
|
|
767
812
|
if provider == "google":
|
|
768
813
|
return self.for_google()
|
|
769
814
|
raise ValueError(provider)
|
|
@@ -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
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lm_deluge-0.0.73 → lm_deluge-0.0.75}/src/lm_deluge/built_in_tools/anthropic/computer_use.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|