lm-deluge 0.0.59__tar.gz → 0.0.60__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.

Potentially problematic release.


This version of lm-deluge might be problematic. Click here for more details.

Files changed (79) hide show
  1. {lm_deluge-0.0.59/src/lm_deluge.egg-info → lm_deluge-0.0.60}/PKG-INFO +1 -1
  2. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/pyproject.toml +1 -1
  3. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/api_requests/bedrock.py +3 -4
  4. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/api_requests/gemini.py +7 -6
  5. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/api_requests/mistral.py +8 -9
  6. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/api_requests/openai.py +16 -13
  7. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/client.py +130 -2
  8. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/models/openai.py +28 -0
  9. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/prompt.py +70 -14
  10. lm_deluge-0.0.60/src/lm_deluge/warnings.py +46 -0
  11. {lm_deluge-0.0.59 → lm_deluge-0.0.60/src/lm_deluge.egg-info}/PKG-INFO +1 -1
  12. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge.egg-info/SOURCES.txt +1 -0
  13. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/LICENSE +0 -0
  14. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/README.md +0 -0
  15. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/setup.cfg +0 -0
  16. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/__init__.py +0 -0
  17. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/api_requests/__init__.py +0 -0
  18. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/api_requests/anthropic.py +0 -0
  19. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/api_requests/base.py +0 -0
  20. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/api_requests/common.py +0 -0
  21. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/api_requests/deprecated/bedrock.py +0 -0
  22. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/api_requests/deprecated/cohere.py +0 -0
  23. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/api_requests/deprecated/deepseek.py +0 -0
  24. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/api_requests/deprecated/mistral.py +0 -0
  25. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/api_requests/deprecated/vertex.py +0 -0
  26. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/api_requests/response.py +0 -0
  27. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/batches.py +0 -0
  28. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/built_in_tools/anthropic/__init__.py +0 -0
  29. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/built_in_tools/anthropic/bash.py +0 -0
  30. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/built_in_tools/anthropic/computer_use.py +0 -0
  31. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/built_in_tools/anthropic/editor.py +0 -0
  32. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/built_in_tools/base.py +0 -0
  33. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/built_in_tools/openai.py +0 -0
  34. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/cache.py +0 -0
  35. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/cli.py +0 -0
  36. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/config.py +0 -0
  37. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/embed.py +0 -0
  38. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/errors.py +0 -0
  39. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/file.py +0 -0
  40. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/image.py +0 -0
  41. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/llm_tools/__init__.py +0 -0
  42. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/llm_tools/classify.py +0 -0
  43. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/llm_tools/extract.py +0 -0
  44. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/llm_tools/locate.py +0 -0
  45. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/llm_tools/ocr.py +0 -0
  46. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/llm_tools/score.py +0 -0
  47. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/llm_tools/translate.py +0 -0
  48. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/models/__init__.py +0 -0
  49. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/models/anthropic.py +0 -0
  50. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/models/bedrock.py +0 -0
  51. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/models/cerebras.py +0 -0
  52. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/models/cohere.py +0 -0
  53. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/models/deepseek.py +0 -0
  54. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/models/fireworks.py +0 -0
  55. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/models/google.py +0 -0
  56. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/models/grok.py +0 -0
  57. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/models/groq.py +0 -0
  58. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/models/meta.py +0 -0
  59. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/models/mistral.py +0 -0
  60. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/models/openrouter.py +0 -0
  61. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/models/together.py +0 -0
  62. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/presets/cerebras.py +0 -0
  63. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/presets/meta.py +0 -0
  64. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/request_context.py +0 -0
  65. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/rerank.py +0 -0
  66. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/tool.py +0 -0
  67. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/tracker.py +0 -0
  68. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/usage.py +0 -0
  69. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/util/harmony.py +0 -0
  70. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/util/json.py +0 -0
  71. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/util/logprobs.py +0 -0
  72. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/util/spatial.py +0 -0
  73. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/util/validation.py +0 -0
  74. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge/util/xml.py +0 -0
  75. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge.egg-info/dependency_links.txt +0 -0
  76. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge.egg-info/requires.txt +0 -0
  77. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/src/lm_deluge.egg-info/top_level.txt +0 -0
  78. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/tests/test_builtin_tools.py +0 -0
  79. {lm_deluge-0.0.59 → lm_deluge-0.0.60}/tests/test_native_mcp_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lm_deluge
3
- Version: 0.0.59
3
+ Version: 0.0.60
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
@@ -3,7 +3,7 @@ requires = ["setuptools", "wheel"]
3
3
 
4
4
  [project]
5
5
  name = "lm_deluge"
6
- version = "0.0.59"
6
+ version = "0.0.60"
7
7
  authors = [{ name = "Benjamin Anderson", email = "ben@trytaylor.ai" }]
8
8
  description = "Python utility for using LLM API models."
9
9
  readme = "README.md"
@@ -1,10 +1,11 @@
1
1
  import asyncio
2
2
  import json
3
3
  import os
4
- import warnings
5
4
 
6
5
  from aiohttp import ClientResponse
7
6
 
7
+ from lm_deluge.warnings import maybe_warn
8
+
8
9
  try:
9
10
  from requests_aws4auth import AWS4Auth
10
11
  except ImportError:
@@ -187,9 +188,7 @@ async def _build_openai_bedrock_request(
187
188
  # Note: GPT-OSS on Bedrock doesn't support response_format parameter
188
189
  # Even though the model supports JSON, we can't use the response_format parameter
189
190
  if sampling_params.json_mode and model.supports_json:
190
- warnings.warn(
191
- f"JSON mode requested for {model.name} but response_format parameter not supported on Bedrock"
192
- )
191
+ maybe_warn("WARN_JSON_MODE_UNSUPPORTED", model_name=model.name)
193
192
 
194
193
  if tools:
195
194
  request_tools = []
@@ -1,11 +1,12 @@
1
1
  import json
2
2
  import os
3
- import warnings
4
3
  from typing import Any
4
+
5
5
  from aiohttp import ClientResponse
6
6
 
7
7
  from lm_deluge.request_context import RequestContext
8
8
  from lm_deluge.tool import Tool
9
+ from lm_deluge.warnings import maybe_warn
9
10
 
10
11
  from ..config import SamplingParams
11
12
  from ..models import APIModel
@@ -54,9 +55,7 @@ async def _build_gemini_request(
54
55
 
55
56
  else:
56
57
  if sampling_params.reasoning_effort:
57
- warnings.warn(
58
- f"Ignoring reasoning_effort param for non-reasoning model: {model.name}"
59
- )
58
+ maybe_warn("WARN_REASONING_UNSUPPORTED", model_name=model.name)
60
59
 
61
60
  # Add tools if provided
62
61
  if tools:
@@ -76,8 +75,10 @@ class GeminiRequest(APIRequestBase):
76
75
 
77
76
  # Warn if cache is specified for Gemini model
78
77
  if self.context.cache is not None:
79
- warnings.warn(
80
- f"Cache parameter '{self.context.cache}' is not supported for Gemini models, ignoring for {self.context.model_name}"
78
+ maybe_warn(
79
+ "WARN_CACHING_UNSUPPORTED",
80
+ model_name=self.context.model_name,
81
+ cache_param=self.context.cache,
81
82
  )
82
83
 
83
84
  self.model = APIModel.from_registry(self.context.model_name)
@@ -1,9 +1,10 @@
1
1
  import json
2
2
  import os
3
- import warnings
4
3
 
5
4
  from aiohttp import ClientResponse
6
5
 
6
+ from lm_deluge.warnings import maybe_warn
7
+
7
8
  from ..models import APIModel
8
9
  from ..prompt import Message
9
10
  from ..request_context import RequestContext
@@ -17,8 +18,10 @@ class MistralRequest(APIRequestBase):
17
18
 
18
19
  # Warn if cache is specified for non-Anthropic model
19
20
  if self.context.cache is not None:
20
- warnings.warn(
21
- f"Cache parameter '{self.context.cache}' is only supported for Anthropic models, ignoring for {self.context.model_name}"
21
+ maybe_warn(
22
+ "WARN_CACHING_UNSUPPORTED",
23
+ model_name=self.context.model_name,
24
+ cache_param=self.context.cache,
22
25
  )
23
26
  self.model = APIModel.from_registry(self.context.model_name)
24
27
 
@@ -38,13 +41,9 @@ class MistralRequest(APIRequestBase):
38
41
  "max_tokens": self.context.sampling_params.max_new_tokens,
39
42
  }
40
43
  if self.context.sampling_params.reasoning_effort:
41
- warnings.warn(
42
- f"Ignoring reasoning_effort param for non-reasoning model: {self.context.model_name}"
43
- )
44
+ maybe_warn("WARN_REASONING_UNSUPPORTED", model_name=self.context.model_name)
44
45
  if self.context.sampling_params.logprobs:
45
- warnings.warn(
46
- f"Ignoring logprobs param for non-logprobs model: {self.context.model_name}"
47
- )
46
+ maybe_warn("WARN_LOGPROBS_UNSUPPORTED", model_name=self.context.model_name)
48
47
  if self.context.sampling_params.json_mode and self.model.supports_json:
49
48
  self.request_json["response_format"] = {"type": "json_object"}
50
49
 
@@ -1,7 +1,6 @@
1
1
  import json
2
2
  import os
3
3
  import traceback as tb
4
- import warnings
5
4
  from types import SimpleNamespace
6
5
 
7
6
  import aiohttp
@@ -9,6 +8,7 @@ from aiohttp import ClientResponse
9
8
 
10
9
  from lm_deluge.request_context import RequestContext
11
10
  from lm_deluge.tool import MCPServer, Tool
11
+ from lm_deluge.warnings import maybe_warn
12
12
 
13
13
  from ..config import SamplingParams
14
14
  from ..models import APIModel
@@ -75,9 +75,8 @@ async def _build_oa_chat_request(
75
75
  request_json["reasoning_effort"] = effort
76
76
  else:
77
77
  if sampling_params.reasoning_effort:
78
- warnings.warn(
79
- f"Ignoring reasoning_effort param for non-reasoning model: {model.name}"
80
- )
78
+ maybe_warn("WARN_REASONING_UNSUPPORTED", model_name=context.model_name)
79
+
81
80
  if sampling_params.logprobs:
82
81
  request_json["logprobs"] = True
83
82
  if sampling_params.top_logprobs is not None:
@@ -105,8 +104,10 @@ class OpenAIRequest(APIRequestBase):
105
104
 
106
105
  # Warn if cache is specified for non-Anthropic model
107
106
  if self.context.cache is not None:
108
- warnings.warn(
109
- f"Cache parameter '{self.context.cache}' is only supported for Anthropic models, ignoring for {self.context.model_name}"
107
+ maybe_warn(
108
+ "WARN_CACHING_UNSUPPORTED",
109
+ model_name=self.context.model_name,
110
+ cache_param=self.context.cache,
110
111
  )
111
112
  self.model = APIModel.from_registry(self.context.model_name)
112
113
 
@@ -283,9 +284,7 @@ async def _build_oa_responses_request(
283
284
  }
284
285
  else:
285
286
  if sampling_params.reasoning_effort:
286
- warnings.warn(
287
- f"Ignoring reasoning_effort for non-reasoning model: {model.id}"
288
- )
287
+ maybe_warn("WARN_REASONING_UNSUPPORTED", model_name=context.model_name)
289
288
 
290
289
  if sampling_params.json_mode and model.supports_json:
291
290
  request_json["text"] = {"format": {"type": "json_object"}}
@@ -322,8 +321,10 @@ class OpenAIResponsesRequest(APIRequestBase):
322
321
  super().__init__(context)
323
322
  # Warn if cache is specified for non-Anthropic model
324
323
  if self.context.cache is not None:
325
- warnings.warn(
326
- f"Cache parameter '{self.context.cache}' is only supported for Anthropic models, ignoring for {self.context.model_name}"
324
+ maybe_warn(
325
+ "WARN_CACHING_UNSUPPORTED",
326
+ model_name=self.context.model_name,
327
+ cache_param=self.context.cache,
327
328
  )
328
329
  self.model = APIModel.from_registry(self.context.model_name)
329
330
 
@@ -526,8 +527,10 @@ async def stream_chat(
526
527
  extra_headers: dict[str, str] | None = None,
527
528
  ):
528
529
  if cache is not None:
529
- warnings.warn(
530
- f"Cache parameter '{cache}' is only supported for Anthropic models, ignoring for {model_name}"
530
+ maybe_warn(
531
+ "WARN_CACHING_UNSUPPORTED",
532
+ model_name=model_name,
533
+ cache_param=cache,
531
534
  )
532
535
 
533
536
  model = APIModel.from_registry(model_name)
@@ -117,13 +117,120 @@ class _LLMClient(BaseModel):
117
117
 
118
118
  # NEW! Builder methods
119
119
  def with_model(self, model: str):
120
- self.model_names = [model]
120
+ self._update_models([model])
121
121
  return self
122
122
 
123
123
  def with_models(self, models: list[str]):
124
- self.model_names = models
124
+ self._update_models(models)
125
125
  return self
126
126
 
127
+ def _update_models(self, models: list[str]) -> None:
128
+ normalized, per_model_efforts = self._normalize_model_names(models)
129
+ if self.reasoning_effort is None:
130
+ unique_efforts = {eff for eff in per_model_efforts if eff is not None}
131
+ if len(normalized) == 1 and per_model_efforts[0] is not None:
132
+ self.reasoning_effort = per_model_efforts[0]
133
+ elif (
134
+ len(unique_efforts) == 1
135
+ and len(unique_efforts) != 0
136
+ and None not in per_model_efforts
137
+ ):
138
+ self.reasoning_effort = next(iter(unique_efforts)) # type: ignore
139
+ self.model_names = normalized
140
+ self._align_sampling_params(per_model_efforts)
141
+ self._reset_model_weights()
142
+
143
+ def _normalize_model_names(
144
+ self, models: list[str]
145
+ ) -> tuple[list[str], list[Literal["low", "medium", "high"] | None]]:
146
+ reasoning_effort_suffixes: dict[str, Literal["low", "medium", "high"]] = {
147
+ "-low": "low",
148
+ "-medium": "medium",
149
+ "-high": "high",
150
+ }
151
+ normalized: list[str] = []
152
+ efforts: list[Literal["low", "medium", "high"] | None] = []
153
+
154
+ for name in models:
155
+ base_name = name
156
+ effort: Literal["low", "medium", "high"] | None = None
157
+ for suffix, candidate in reasoning_effort_suffixes.items():
158
+ if name.endswith(suffix) and len(name) > len(suffix):
159
+ base_name = name[: -len(suffix)]
160
+ effort = candidate
161
+ break
162
+ normalized.append(base_name)
163
+ efforts.append(effort)
164
+
165
+ return normalized, efforts
166
+
167
+ def _align_sampling_params(
168
+ self, per_model_efforts: list[Literal["low", "medium", "high"] | None]
169
+ ) -> None:
170
+ if len(per_model_efforts) < len(self.model_names):
171
+ per_model_efforts = per_model_efforts + [None] * (
172
+ len(self.model_names) - len(per_model_efforts)
173
+ )
174
+
175
+ if not self.model_names:
176
+ self.sampling_params = []
177
+ return
178
+
179
+ if not self.sampling_params:
180
+ self.sampling_params = []
181
+
182
+ if len(self.sampling_params) == 0:
183
+ for _ in self.model_names:
184
+ self.sampling_params.append(
185
+ SamplingParams(
186
+ temperature=self.temperature,
187
+ top_p=self.top_p,
188
+ json_mode=self.json_mode,
189
+ max_new_tokens=self.max_new_tokens,
190
+ reasoning_effort=self.reasoning_effort,
191
+ logprobs=self.logprobs,
192
+ top_logprobs=self.top_logprobs,
193
+ )
194
+ )
195
+ elif len(self.sampling_params) == 1 and len(self.model_names) > 1:
196
+ base_param = self.sampling_params[0]
197
+ self.sampling_params = [
198
+ base_param.model_copy(deep=True) for _ in self.model_names
199
+ ]
200
+ elif len(self.sampling_params) != len(self.model_names):
201
+ base_param = self.sampling_params[0]
202
+ self.sampling_params = [
203
+ base_param.model_copy(deep=True) for _ in self.model_names
204
+ ]
205
+
206
+ if self.reasoning_effort is not None:
207
+ for sp in self.sampling_params:
208
+ sp.reasoning_effort = self.reasoning_effort
209
+ else:
210
+ for sp, effort in zip(self.sampling_params, per_model_efforts):
211
+ if effort is not None:
212
+ sp.reasoning_effort = effort
213
+
214
+ def _reset_model_weights(self) -> None:
215
+ if not self.model_names:
216
+ self.model_weights = []
217
+ return
218
+
219
+ if isinstance(self.model_weights, list):
220
+ if len(self.model_weights) == len(self.model_names) and any(
221
+ self.model_weights
222
+ ):
223
+ total = sum(self.model_weights)
224
+ if total == 0:
225
+ self.model_weights = [
226
+ 1 / len(self.model_names) for _ in self.model_names
227
+ ]
228
+ else:
229
+ self.model_weights = [w / total for w in self.model_weights]
230
+ return
231
+ # Fallback to uniform distribution
232
+ self.model_weights = [1 / len(self.model_names) for _ in self.model_names]
233
+
127
234
  def with_limits(
128
235
  self,
129
236
  max_requests_per_minute: int | None = None,
@@ -150,8 +257,29 @@ class _LLMClient(BaseModel):
150
257
  @model_validator(mode="before")
151
258
  @classmethod
152
259
  def fix_lists(cls, data) -> "_LLMClient":
260
+ # Parse reasoning effort from model name suffixes (e.g., "gpt-5-high")
261
+ # Only applies when a single model string is provided
153
262
  if isinstance(data.get("model_names"), str):
263
+ model_name = data["model_names"]
264
+ reasoning_effort_suffixes = {
265
+ "-low": "low",
266
+ "-medium": "medium",
267
+ "-high": "high",
268
+ }
269
+
270
+ for suffix, effort in reasoning_effort_suffixes.items():
271
+ if model_name.endswith(suffix):
272
+ # Extract base model name by removing suffix
273
+ base_model = model_name[: -len(suffix)]
274
+ data["model_names"] = base_model
275
+
276
+ # Set reasoning_effort if not already explicitly set
277
+ if data.get("reasoning_effort") is None:
278
+ data["reasoning_effort"] = effort
279
+ break
280
+
154
281
  data["model_names"] = [data["model_names"]]
282
+
155
283
  if not isinstance(data.get("sampling_params", []), list):
156
284
  data["sampling_params"] = [data["sampling_params"]]
157
285
  if "sampling_params" not in data or len(data.get("sampling_params", [])) == 0:
@@ -10,6 +10,20 @@ OPENAI_MODELS = {
10
10
  # ░███
11
11
  # █████
12
12
  # ░░░░░
13
+ "gpt-5-codex": {
14
+ "id": "gpt-5-codex",
15
+ "name": "gpt-5-codex",
16
+ "api_base": "https://api.openai.com/v1",
17
+ "api_key_env_var": "OPENAI_API_KEY",
18
+ "supports_json": False,
19
+ "supports_logprobs": True,
20
+ "supports_responses": True,
21
+ "api_spec": "openai",
22
+ "input_cost": 1.25,
23
+ "cached_input_cost": 0.125,
24
+ "output_cost": 10.0,
25
+ "reasoning_model": True,
26
+ },
13
27
  "gpt-5": {
14
28
  "id": "gpt-5",
15
29
  "name": "gpt-5",
@@ -79,6 +93,20 @@ OPENAI_MODELS = {
79
93
  "output_cost": 12.0,
80
94
  "reasoning_model": False,
81
95
  },
96
+ "codex-mini-latest": {
97
+ "id": "codex-mini-latest",
98
+ "name": "codex-mini-latest",
99
+ "api_base": "https://api.openai.com/v1",
100
+ "api_key_env_var": "OPENAI_API_KEY",
101
+ "supports_json": True,
102
+ "supports_logprobs": False,
103
+ "supports_responses": True,
104
+ "api_spec": "openai",
105
+ "input_cost": 1.5,
106
+ "cached_input_cost": 0.375,
107
+ "output_cost": 6.0,
108
+ "reasoning_model": True,
109
+ },
82
110
  "o3": {
83
111
  "id": "o3",
84
112
  "name": "o3-2025-04-16",
@@ -9,6 +9,7 @@ import xxhash
9
9
 
10
10
  from lm_deluge.file import File
11
11
  from lm_deluge.image import Image, MediaType
12
+ from lm_deluge.warnings import deprecated
12
13
 
13
14
  CachePattern = Literal[
14
15
  "tools_only",
@@ -415,12 +416,17 @@ class Message:
415
416
 
416
417
  return cls(role, parts)
417
418
 
418
- def add_text(self, content: str) -> "Message":
419
+ def with_text(self, content: str) -> "Message":
419
420
  """Append a text block and return self for chaining."""
420
421
  self.parts.append(Text(content))
421
422
  return self
422
423
 
423
- def add_image(
424
+ @deprecated("with_text")
425
+ def add_text(self, content: str) -> "Message":
426
+ """Append a text block and return self for chaining."""
427
+ return self.with_text(content)
428
+
429
+ def with_image(
424
430
  self,
425
431
  data: bytes | str | Path | io.BytesIO | Image,
426
432
  *,
@@ -446,7 +452,27 @@ class Message:
446
452
  self.parts.append(img)
447
453
  return self
448
454
 
449
- def add_file(
455
+ @deprecated("with_image")
456
+ def add_image(
457
+ self,
458
+ data: bytes | str | Path | io.BytesIO | Image,
459
+ *,
460
+ media_type: MediaType | None = None,
461
+ detail: Literal["low", "high", "auto"] = "auto",
462
+ max_size: int | None = None,
463
+ ) -> "Message":
464
+ """
465
+ Append an image block and return self for chaining.
466
+
467
+ If max_size is provided, the image will be resized so that its longer
468
+ dimension equals max_size, but only if the longer dimension is currently
469
+ larger than max_size.
470
+ """
471
+ return self.with_image(
472
+ data=data, media_type=media_type, detail=detail, max_size=max_size
473
+ )
474
+
475
+ def with_file(
450
476
  self,
451
477
  data: bytes | str | Path | io.BytesIO,
452
478
  *,
@@ -460,11 +486,29 @@ class Message:
460
486
  self.parts.append(file)
461
487
  return self
462
488
 
463
- def add_tool_call(self, id: str, name: str, arguments: dict) -> "Message":
489
+ @deprecated("with_file")
490
+ def add_file(
491
+ self,
492
+ data: bytes | str | Path | io.BytesIO,
493
+ *,
494
+ media_type: str | None = None,
495
+ filename: str | None = None,
496
+ ) -> "Message":
497
+ """
498
+ Append a file block and return self for chaining.
499
+ """
500
+ return self.with_file(data, media_type=media_type, filename=filename)
501
+
502
+ def with_tool_call(self, id: str, name: str, arguments: dict) -> "Message":
464
503
  """Append a tool call block and return self for chaining."""
465
504
  self.parts.append(ToolCall(id=id, name=name, arguments=arguments))
466
505
  return self
467
506
 
507
+ @deprecated("with_tool_call")
508
+ def add_tool_call(self, id: str, name: str, arguments: dict) -> "Message":
509
+ """Append a tool call block and return self for chaining."""
510
+ return self.with_tool_call(id, name, arguments)
511
+
468
512
  def with_tool_result(
469
513
  self, tool_call_id: str, result: str | list[ToolResultPart]
470
514
  ) -> "Message":
@@ -472,11 +516,23 @@ class Message:
472
516
  self.parts.append(ToolResult(tool_call_id=tool_call_id, result=result))
473
517
  return self
474
518
 
475
- def add_thinking(self, content: str) -> "Message":
519
+ @deprecated("with_tool_result")
520
+ def add_tool_result(
521
+ self, tool_call_id: str, result: str | list[ToolResultPart]
522
+ ) -> "Message":
523
+ """Append a tool result block and return self for chaining."""
524
+ return self.with_tool_result(tool_call_id, result)
525
+
526
+ def with_thinking(self, content: str) -> "Message":
476
527
  """Append a thinking block and return self for chaining."""
477
528
  self.parts.append(Thinking(content=content))
478
529
  return self
479
530
 
531
+ @deprecated("with_thinking")
532
+ def add_thinking(self, content: str) -> "Message":
533
+ """Append a thinking block and return self for chaining."""
534
+ return self.with_thinking(content)
535
+
480
536
  # -------- convenient constructors --------
481
537
  @classmethod
482
538
  def user(
@@ -488,25 +544,25 @@ class Message:
488
544
  ) -> "Message":
489
545
  res = cls("user", [])
490
546
  if text is not None:
491
- res.add_text(text)
547
+ res.with_text(text)
492
548
  if image is not None:
493
- res.add_image(image)
549
+ res.with_image(image)
494
550
  if file is not None:
495
- res.add_file(file)
551
+ res.with_file(file)
496
552
  return res
497
553
 
498
554
  @classmethod
499
555
  def system(cls, text: str | None = None) -> "Message":
500
556
  res = cls("system", [])
501
557
  if text is not None:
502
- res.add_text(text)
558
+ res.with_text(text)
503
559
  return res
504
560
 
505
561
  @classmethod
506
562
  def ai(cls, text: str | None = None) -> "Message":
507
563
  res = cls("assistant", [])
508
564
  if text is not None:
509
- res.add_text(text)
565
+ res.with_text(text)
510
566
  return res
511
567
 
512
568
  # ──── provider-specific constructors ───
@@ -698,9 +754,9 @@ class Conversation:
698
754
  ) -> "Conversation":
699
755
  msg = Message.user(text)
700
756
  if image is not None:
701
- msg.add_image(image)
757
+ msg.with_image(image)
702
758
  if file is not None:
703
- msg.add_file(file)
759
+ msg.with_file(file)
704
760
  return cls([msg])
705
761
 
706
762
  @classmethod
@@ -1211,11 +1267,11 @@ class Conversation:
1211
1267
  for i, tool_result in enumerate(m.tool_results):
1212
1268
  images = tool_result.get_images()
1213
1269
  if len(images) > 0:
1214
- user_msg.add_text(
1270
+ user_msg.with_text(
1215
1271
  f"[Images for Tool Call {tool_result.tool_call_id}]"
1216
1272
  )
1217
1273
  for img in images:
1218
- user_msg.add_image(img)
1274
+ user_msg.with_image(img)
1219
1275
 
1220
1276
  else:
1221
1277
  result.append(m.oa_chat())
@@ -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.59
3
+ Version: 0.0.60
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
@@ -17,6 +17,7 @@ src/lm_deluge/rerank.py
17
17
  src/lm_deluge/tool.py
18
18
  src/lm_deluge/tracker.py
19
19
  src/lm_deluge/usage.py
20
+ src/lm_deluge/warnings.py
20
21
  src/lm_deluge.egg-info/PKG-INFO
21
22
  src/lm_deluge.egg-info/SOURCES.txt
22
23
  src/lm_deluge.egg-info/dependency_links.txt
File without changes
File without changes
File without changes