synth-ai 0.2.4.dev5__py3-none-any.whl → 0.2.4.dev7__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.
Files changed (229) hide show
  1. synth_ai/__init__.py +18 -9
  2. synth_ai/cli/__init__.py +10 -5
  3. synth_ai/cli/balance.py +22 -17
  4. synth_ai/cli/calc.py +2 -3
  5. synth_ai/cli/demo.py +3 -5
  6. synth_ai/cli/legacy_root_backup.py +58 -32
  7. synth_ai/cli/man.py +22 -19
  8. synth_ai/cli/recent.py +9 -8
  9. synth_ai/cli/root.py +58 -13
  10. synth_ai/cli/status.py +13 -6
  11. synth_ai/cli/traces.py +45 -21
  12. synth_ai/cli/watch.py +40 -37
  13. synth_ai/config/base_url.py +1 -3
  14. synth_ai/core/experiment.py +1 -2
  15. synth_ai/environments/__init__.py +2 -6
  16. synth_ai/environments/environment/artifacts/base.py +3 -1
  17. synth_ai/environments/environment/db/sqlite.py +1 -1
  18. synth_ai/environments/environment/registry.py +19 -20
  19. synth_ai/environments/environment/resources/sqlite.py +2 -3
  20. synth_ai/environments/environment/rewards/core.py +3 -2
  21. synth_ai/environments/environment/tools/__init__.py +6 -4
  22. synth_ai/environments/examples/crafter_classic/__init__.py +1 -1
  23. synth_ai/environments/examples/crafter_classic/engine.py +21 -17
  24. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +1 -0
  25. synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +2 -1
  26. synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +2 -1
  27. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +3 -2
  28. synth_ai/environments/examples/crafter_classic/environment.py +16 -15
  29. synth_ai/environments/examples/crafter_classic/taskset.py +2 -2
  30. synth_ai/environments/examples/crafter_classic/trace_hooks_v3.py +2 -3
  31. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +2 -1
  32. synth_ai/environments/examples/crafter_custom/crafter/__init__.py +2 -2
  33. synth_ai/environments/examples/crafter_custom/crafter/config.py +2 -2
  34. synth_ai/environments/examples/crafter_custom/crafter/env.py +1 -5
  35. synth_ai/environments/examples/crafter_custom/crafter/objects.py +1 -2
  36. synth_ai/environments/examples/crafter_custom/crafter/worldgen.py +1 -2
  37. synth_ai/environments/examples/crafter_custom/dataset_builder.py +5 -5
  38. synth_ai/environments/examples/crafter_custom/environment.py +13 -13
  39. synth_ai/environments/examples/crafter_custom/run_dataset.py +5 -5
  40. synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +2 -2
  41. synth_ai/environments/examples/enron/art_helpers/local_email_db.py +5 -4
  42. synth_ai/environments/examples/enron/art_helpers/types_enron.py +2 -1
  43. synth_ai/environments/examples/enron/engine.py +18 -14
  44. synth_ai/environments/examples/enron/environment.py +12 -11
  45. synth_ai/environments/examples/enron/taskset.py +7 -7
  46. synth_ai/environments/examples/minigrid/__init__.py +6 -6
  47. synth_ai/environments/examples/minigrid/engine.py +6 -6
  48. synth_ai/environments/examples/minigrid/environment.py +6 -6
  49. synth_ai/environments/examples/minigrid/puzzle_loader.py +3 -2
  50. synth_ai/environments/examples/minigrid/taskset.py +13 -13
  51. synth_ai/environments/examples/nethack/achievements.py +1 -1
  52. synth_ai/environments/examples/nethack/engine.py +8 -7
  53. synth_ai/environments/examples/nethack/environment.py +10 -9
  54. synth_ai/environments/examples/nethack/helpers/__init__.py +8 -9
  55. synth_ai/environments/examples/nethack/helpers/action_mapping.py +1 -1
  56. synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +2 -1
  57. synth_ai/environments/examples/nethack/helpers/observation_utils.py +1 -1
  58. synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +3 -4
  59. synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +6 -5
  60. synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +5 -5
  61. synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +7 -6
  62. synth_ai/environments/examples/nethack/taskset.py +5 -5
  63. synth_ai/environments/examples/red/engine.py +9 -8
  64. synth_ai/environments/examples/red/engine_helpers/reward_components.py +2 -1
  65. synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +7 -7
  66. synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +2 -1
  67. synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +2 -1
  68. synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +2 -1
  69. synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +2 -1
  70. synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +2 -1
  71. synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +2 -1
  72. synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +2 -1
  73. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +2 -1
  74. synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +2 -1
  75. synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +2 -1
  76. synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +2 -1
  77. synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +3 -2
  78. synth_ai/environments/examples/red/engine_helpers/state_extraction.py +2 -1
  79. synth_ai/environments/examples/red/environment.py +18 -15
  80. synth_ai/environments/examples/red/taskset.py +5 -3
  81. synth_ai/environments/examples/sokoban/engine.py +16 -13
  82. synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +3 -2
  83. synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +2 -1
  84. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +1 -1
  85. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +7 -5
  86. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +1 -1
  87. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +2 -1
  88. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +5 -4
  89. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +3 -2
  90. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +2 -1
  91. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +5 -4
  92. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +1 -1
  93. synth_ai/environments/examples/sokoban/environment.py +15 -14
  94. synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +5 -3
  95. synth_ai/environments/examples/sokoban/puzzle_loader.py +3 -2
  96. synth_ai/environments/examples/sokoban/taskset.py +13 -10
  97. synth_ai/environments/examples/tictactoe/engine.py +6 -6
  98. synth_ai/environments/examples/tictactoe/environment.py +8 -7
  99. synth_ai/environments/examples/tictactoe/taskset.py +6 -5
  100. synth_ai/environments/examples/verilog/engine.py +4 -3
  101. synth_ai/environments/examples/verilog/environment.py +11 -10
  102. synth_ai/environments/examples/verilog/taskset.py +14 -12
  103. synth_ai/environments/examples/wordle/__init__.py +29 -0
  104. synth_ai/environments/examples/wordle/engine.py +398 -0
  105. synth_ai/environments/examples/wordle/environment.py +159 -0
  106. synth_ai/environments/examples/wordle/helpers/generate_instances_wordfreq.py +75 -0
  107. synth_ai/environments/examples/wordle/taskset.py +230 -0
  108. synth_ai/environments/reproducibility/core.py +1 -1
  109. synth_ai/environments/reproducibility/tree.py +21 -21
  110. synth_ai/environments/service/app.py +11 -2
  111. synth_ai/environments/service/core_routes.py +137 -105
  112. synth_ai/environments/service/external_registry.py +1 -2
  113. synth_ai/environments/service/registry.py +1 -1
  114. synth_ai/environments/stateful/core.py +1 -2
  115. synth_ai/environments/stateful/engine.py +1 -1
  116. synth_ai/environments/tasks/api.py +4 -4
  117. synth_ai/environments/tasks/core.py +14 -12
  118. synth_ai/environments/tasks/filters.py +6 -4
  119. synth_ai/environments/tasks/utils.py +13 -11
  120. synth_ai/evals/base.py +2 -3
  121. synth_ai/experimental/synth_oss.py +4 -4
  122. synth_ai/learning/gateway.py +1 -3
  123. synth_ai/learning/prompts/banking77_injection_eval.py +168 -0
  124. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +213 -0
  125. synth_ai/learning/prompts/mipro.py +282 -1
  126. synth_ai/learning/prompts/random_search.py +246 -0
  127. synth_ai/learning/prompts/run_mipro_banking77.py +172 -0
  128. synth_ai/learning/prompts/run_random_search_banking77.py +324 -0
  129. synth_ai/lm/__init__.py +5 -5
  130. synth_ai/lm/caching/ephemeral.py +9 -9
  131. synth_ai/lm/caching/handler.py +20 -20
  132. synth_ai/lm/caching/persistent.py +10 -10
  133. synth_ai/lm/config.py +3 -3
  134. synth_ai/lm/constants.py +7 -7
  135. synth_ai/lm/core/all.py +17 -3
  136. synth_ai/lm/core/exceptions.py +0 -2
  137. synth_ai/lm/core/main.py +26 -41
  138. synth_ai/lm/core/main_v3.py +20 -10
  139. synth_ai/lm/core/vendor_clients.py +18 -17
  140. synth_ai/lm/injection.py +80 -0
  141. synth_ai/lm/overrides.py +206 -0
  142. synth_ai/lm/provider_support/__init__.py +1 -1
  143. synth_ai/lm/provider_support/anthropic.py +51 -24
  144. synth_ai/lm/provider_support/openai.py +51 -22
  145. synth_ai/lm/structured_outputs/handler.py +34 -32
  146. synth_ai/lm/structured_outputs/inject.py +24 -27
  147. synth_ai/lm/structured_outputs/rehabilitate.py +19 -15
  148. synth_ai/lm/tools/base.py +17 -16
  149. synth_ai/lm/unified_interface.py +17 -18
  150. synth_ai/lm/vendors/base.py +20 -18
  151. synth_ai/lm/vendors/core/anthropic_api.py +50 -25
  152. synth_ai/lm/vendors/core/gemini_api.py +31 -36
  153. synth_ai/lm/vendors/core/mistral_api.py +19 -19
  154. synth_ai/lm/vendors/core/openai_api.py +11 -10
  155. synth_ai/lm/vendors/openai_standard.py +144 -88
  156. synth_ai/lm/vendors/openai_standard_responses.py +74 -61
  157. synth_ai/lm/vendors/retries.py +9 -1
  158. synth_ai/lm/vendors/supported/custom_endpoint.py +26 -26
  159. synth_ai/lm/vendors/supported/deepseek.py +10 -10
  160. synth_ai/lm/vendors/supported/grok.py +8 -8
  161. synth_ai/lm/vendors/supported/ollama.py +2 -1
  162. synth_ai/lm/vendors/supported/openrouter.py +11 -9
  163. synth_ai/lm/vendors/synth_client.py +69 -63
  164. synth_ai/lm/warmup.py +8 -7
  165. synth_ai/tracing/__init__.py +22 -10
  166. synth_ai/tracing_v1/__init__.py +22 -20
  167. synth_ai/tracing_v3/__init__.py +7 -7
  168. synth_ai/tracing_v3/abstractions.py +56 -52
  169. synth_ai/tracing_v3/config.py +4 -2
  170. synth_ai/tracing_v3/db_config.py +6 -8
  171. synth_ai/tracing_v3/decorators.py +29 -30
  172. synth_ai/tracing_v3/examples/basic_usage.py +12 -12
  173. synth_ai/tracing_v3/hooks.py +21 -21
  174. synth_ai/tracing_v3/llm_call_record_helpers.py +85 -98
  175. synth_ai/tracing_v3/lm_call_record_abstractions.py +2 -4
  176. synth_ai/tracing_v3/migration_helper.py +3 -5
  177. synth_ai/tracing_v3/replica_sync.py +30 -32
  178. synth_ai/tracing_v3/session_tracer.py +35 -29
  179. synth_ai/tracing_v3/storage/__init__.py +1 -1
  180. synth_ai/tracing_v3/storage/base.py +8 -7
  181. synth_ai/tracing_v3/storage/config.py +4 -4
  182. synth_ai/tracing_v3/storage/factory.py +4 -4
  183. synth_ai/tracing_v3/storage/utils.py +9 -9
  184. synth_ai/tracing_v3/turso/__init__.py +3 -3
  185. synth_ai/tracing_v3/turso/daemon.py +9 -9
  186. synth_ai/tracing_v3/turso/manager.py +60 -48
  187. synth_ai/tracing_v3/turso/models.py +24 -19
  188. synth_ai/tracing_v3/utils.py +5 -5
  189. synth_ai/tui/__main__.py +1 -1
  190. synth_ai/tui/cli/query_experiments.py +2 -3
  191. synth_ai/tui/cli/query_experiments_v3.py +2 -3
  192. synth_ai/tui/dashboard.py +97 -86
  193. synth_ai/v0/tracing/abstractions.py +28 -28
  194. synth_ai/v0/tracing/base_client.py +9 -9
  195. synth_ai/v0/tracing/client_manager.py +7 -7
  196. synth_ai/v0/tracing/config.py +7 -7
  197. synth_ai/v0/tracing/context.py +6 -6
  198. synth_ai/v0/tracing/decorators.py +6 -5
  199. synth_ai/v0/tracing/events/manage.py +1 -1
  200. synth_ai/v0/tracing/events/store.py +5 -4
  201. synth_ai/v0/tracing/immediate_client.py +4 -5
  202. synth_ai/v0/tracing/local.py +3 -3
  203. synth_ai/v0/tracing/log_client_base.py +4 -5
  204. synth_ai/v0/tracing/retry_queue.py +5 -6
  205. synth_ai/v0/tracing/trackers.py +25 -25
  206. synth_ai/v0/tracing/upload.py +6 -0
  207. synth_ai/v0/tracing_v1/__init__.py +1 -1
  208. synth_ai/v0/tracing_v1/abstractions.py +28 -28
  209. synth_ai/v0/tracing_v1/base_client.py +9 -9
  210. synth_ai/v0/tracing_v1/client_manager.py +7 -7
  211. synth_ai/v0/tracing_v1/config.py +7 -7
  212. synth_ai/v0/tracing_v1/context.py +6 -6
  213. synth_ai/v0/tracing_v1/decorators.py +7 -6
  214. synth_ai/v0/tracing_v1/events/manage.py +1 -1
  215. synth_ai/v0/tracing_v1/events/store.py +5 -4
  216. synth_ai/v0/tracing_v1/immediate_client.py +4 -5
  217. synth_ai/v0/tracing_v1/local.py +3 -3
  218. synth_ai/v0/tracing_v1/log_client_base.py +4 -5
  219. synth_ai/v0/tracing_v1/retry_queue.py +5 -6
  220. synth_ai/v0/tracing_v1/trackers.py +25 -25
  221. synth_ai/v0/tracing_v1/upload.py +25 -24
  222. synth_ai/zyk/__init__.py +1 -0
  223. {synth_ai-0.2.4.dev5.dist-info → synth_ai-0.2.4.dev7.dist-info}/METADATA +2 -11
  224. synth_ai-0.2.4.dev7.dist-info/RECORD +299 -0
  225. synth_ai-0.2.4.dev5.dist-info/RECORD +0 -287
  226. {synth_ai-0.2.4.dev5.dist-info → synth_ai-0.2.4.dev7.dist-info}/WHEEL +0 -0
  227. {synth_ai-0.2.4.dev5.dist-info → synth_ai-0.2.4.dev7.dist-info}/entry_points.txt +0 -0
  228. {synth_ai-0.2.4.dev5.dist-info → synth_ai-0.2.4.dev7.dist-info}/licenses/LICENSE +0 -0
  229. {synth_ai-0.2.4.dev5.dist-info → synth_ai-0.2.4.dev7.dist-info}/top_level.txt +0 -0
synth_ai/lm/core/main.py CHANGED
@@ -1,8 +1,9 @@
1
- from typing import Any, Dict, List, Literal, Optional, Union
2
1
  import os
2
+ from typing import Any, Literal
3
3
 
4
4
  from pydantic import BaseModel, Field
5
5
 
6
+ from synth_ai.lm.config import reasoning_models
6
7
  from synth_ai.lm.core.exceptions import StructuredOutputCoercionFailureException
7
8
  from synth_ai.lm.core.vendor_clients import (
8
9
  anthropic_naming_regexes,
@@ -10,29 +11,28 @@ from synth_ai.lm.core.vendor_clients import (
10
11
  openai_naming_regexes,
11
12
  )
12
13
  from synth_ai.lm.structured_outputs.handler import StructuredOutputHandler
13
- from synth_ai.lm.vendors.base import VendorBase
14
14
  from synth_ai.lm.tools.base import BaseTool
15
- from synth_ai.lm.config import reasoning_models
15
+ from synth_ai.lm.vendors.base import VendorBase
16
16
 
17
17
 
18
18
  def build_messages(
19
19
  sys_msg: str,
20
20
  user_msg: str,
21
- images_bytes: List[bytes] = [],
22
- model_name: Optional[str] = None,
23
- ) -> List[Dict]:
21
+ images_bytes: list[bytes] = [],
22
+ model_name: str | None = None,
23
+ ) -> list[dict]:
24
24
  """
25
25
  Build a messages list for API calls, handling image formatting based on the model provider.
26
-
26
+
27
27
  Args:
28
28
  sys_msg: System message content
29
29
  user_msg: User message content
30
30
  images_bytes: List of base64-encoded image bytes
31
31
  model_name: Model name to determine proper image format (OpenAI vs Anthropic)
32
-
32
+
33
33
  Returns:
34
34
  List[Dict]: Formatted messages list ready for API calls
35
-
35
+
36
36
  Note:
37
37
  Different providers require different image formats:
38
38
  - OpenAI: Uses "image_url" with data URL format
@@ -102,7 +102,7 @@ class LM:
102
102
  # if str
103
103
  model_name: str
104
104
  client: VendorBase
105
- lm_config: Dict[str, Any]
105
+ lm_config: dict[str, Any]
106
106
  structured_output_handler: StructuredOutputHandler
107
107
 
108
108
  def __init__(
@@ -113,23 +113,8 @@ class LM:
113
113
  max_retries: Literal["None", "Few", "Many"] = "Few",
114
114
  structured_output_mode: Literal["stringified_json", "forced_json"] = "stringified_json",
115
115
  synth_logging: bool = True,
116
- provider: Optional[
117
- Union[
118
- Literal[
119
- "openai",
120
- "anthropic",
121
- "groq",
122
- "gemini",
123
- "deepseek",
124
- "grok",
125
- "mistral",
126
- "openrouter",
127
- "together",
128
- ],
129
- str,
130
- ]
131
- ] = None,
132
- enable_thinking: Optional[bool] = None,
116
+ provider: Literal["openai", "anthropic", "groq", "gemini", "deepseek", "grok", "mistral", "openrouter", "together"] | str | None = None,
117
+ enable_thinking: bool | None = None,
133
118
  ):
134
119
  # print("Structured output mode", structured_output_mode)
135
120
  # Check for environment variable if provider is not specified
@@ -170,13 +155,13 @@ class LM:
170
155
 
171
156
  def respond_sync(
172
157
  self,
173
- system_message: Optional[str] = None,
174
- user_message: Optional[str] = None,
175
- messages: Optional[List[Dict]] = None,
176
- images_as_bytes: List[bytes] = [],
177
- response_model: Optional[BaseModel] = None,
158
+ system_message: str | None = None,
159
+ user_message: str | None = None,
160
+ messages: list[dict] | None = None,
161
+ images_as_bytes: list[bytes] = [],
162
+ response_model: BaseModel | None = None,
178
163
  use_ephemeral_cache_only: bool = False,
179
- tools: Optional[List[BaseTool]] = None,
164
+ tools: list[BaseTool] | None = None,
180
165
  reasoning_effort: str = "low",
181
166
  ):
182
167
  assert (system_message is None) == (user_message is None), (
@@ -231,13 +216,13 @@ class LM:
231
216
 
232
217
  async def respond_async(
233
218
  self,
234
- system_message: Optional[str] = None,
235
- user_message: Optional[str] = None,
236
- messages: Optional[List[Dict]] = None,
237
- images_as_bytes: List[bytes] = [],
238
- response_model: Optional[BaseModel] = None,
219
+ system_message: str | None = None,
220
+ user_message: str | None = None,
221
+ messages: list[dict] | None = None,
222
+ images_as_bytes: list[bytes] = [],
223
+ response_model: BaseModel | None = None,
239
224
  use_ephemeral_cache_only: bool = False,
240
- tools: Optional[List[BaseTool]] = None,
225
+ tools: list[BaseTool] | None = None,
241
226
  reasoning_effort: str = "low",
242
227
  ):
243
228
  # "In respond_async")
@@ -300,8 +285,8 @@ if __name__ == "__main__":
300
285
 
301
286
  # Update json instructions to handle nested pydantic?
302
287
  class Thought(BaseModel):
303
- argument_keys: List[str] = Field(description="The keys of the arguments")
304
- argument_values: List[str] = Field(
288
+ argument_keys: list[str] = Field(description="The keys of the arguments")
289
+ argument_values: list[str] = Field(
305
290
  description="Stringified JSON for the values of the arguments"
306
291
  )
307
292
 
@@ -53,7 +53,9 @@ def build_messages(
53
53
  ],
54
54
  },
55
55
  ]
56
- elif len(images_bytes) > 0 and any(regex.match(model_name) for regex in anthropic_naming_regexes):
56
+ elif len(images_bytes) > 0 and any(
57
+ regex.match(model_name) for regex in anthropic_naming_regexes
58
+ ):
57
59
  return [
58
60
  {"role": "system", "content": sys_msg},
59
61
  {
@@ -163,7 +165,7 @@ class LM:
163
165
  self.system_id = system_id or f"lm_{self.vendor or 'unknown'}_{self.model or 'unknown'}"
164
166
  self.enable_v3_tracing = enable_v3_tracing
165
167
  self.additional_params = additional_params
166
-
168
+
167
169
  # Initialize vendor wrapper early, before any potential usage
168
170
  # (e.g., within StructuredOutputHandler initialization below)
169
171
  self._vendor_wrapper = None
@@ -221,11 +223,14 @@ class LM:
221
223
  """Determine if Responses API should be used."""
222
224
  if self.use_responses_api is not None:
223
225
  return self.use_responses_api
224
-
226
+
225
227
  # Auto-detect based on model
226
228
  responses_models = {
227
- "o4-mini", "o3", "o3-mini", # Supported Synth-hosted models
228
- "gpt-oss-120b", "gpt-oss-20b" # OSS models via Synth
229
+ "o4-mini",
230
+ "o3",
231
+ "o3-mini", # Supported Synth-hosted models
232
+ "gpt-oss-120b",
233
+ "gpt-oss-20b", # OSS models via Synth
229
234
  }
230
235
  return self.model in responses_models or (self.model and self.model in reasoning_models)
231
236
 
@@ -377,11 +382,15 @@ class LM:
377
382
  raise AttributeError(
378
383
  f"Vendor wrapper {type(vendor_wrapper).__name__} has no suitable response method"
379
384
  )
380
- if not hasattr(response, 'api_type'):
385
+ if not hasattr(response, "api_type"):
381
386
  response.api_type = "chat"
382
387
 
383
388
  # Update stored response ID if auto-storing
384
- if self.auto_store_responses and hasattr(response, 'response_id') and response.response_id:
389
+ if (
390
+ self.auto_store_responses
391
+ and hasattr(response, "response_id")
392
+ and response.response_id
393
+ ):
385
394
  self._last_response_id = response.response_id
386
395
 
387
396
  except Exception as e:
@@ -397,12 +406,13 @@ class LM:
397
406
  and hasattr(self.session_tracer, "current_session")
398
407
  ):
399
408
  latency_ms = int((time.time() - start_time) * 1000)
400
-
409
+
401
410
  # Create LLMCallRecord from the response
402
411
  from datetime import datetime
412
+
403
413
  started_at = datetime.utcnow()
404
414
  completed_at = datetime.utcnow()
405
-
415
+
406
416
  call_record = create_llm_call_record_from_response(
407
417
  response=response,
408
418
  model_name=self.model or self.vendor,
@@ -415,7 +425,7 @@ class LM:
415
425
  completed_at=completed_at,
416
426
  latency_ms=latency_ms,
417
427
  )
418
-
428
+
419
429
  # Compute aggregates from the call record
420
430
  aggregates = compute_aggregates_from_call_records([call_record])
421
431
 
@@ -6,43 +6,44 @@ based on model names or explicit provider specifications.
6
6
  """
7
7
 
8
8
  import re
9
- from typing import Any, List, Pattern, Optional, Dict
9
+ from re import Pattern
10
+ from typing import Any
10
11
 
11
12
  from synth_ai.lm.core.all import (
12
13
  AnthropicClient,
14
+ CustomEndpointClient,
13
15
  DeepSeekClient,
14
16
  GeminiClient,
15
- GroqClient,
16
17
  GrokClient,
18
+ GroqClient,
17
19
  # OpenAIClient,
18
20
  OpenAIStructuredOutputClient,
19
- TogetherClient,
20
- CustomEndpointClient,
21
21
  OpenRouterClient,
22
+ TogetherClient,
22
23
  )
23
24
 
24
25
  # Regular expressions to match model names to their respective providers
25
- openai_naming_regexes: List[Pattern] = [
26
+ openai_naming_regexes: list[Pattern] = [
26
27
  re.compile(r"^(ft:)?(o[1,3,4](-.*)?|gpt-.*)$"),
27
28
  ]
28
- openai_formatting_model_regexes: List[Pattern] = [
29
+ openai_formatting_model_regexes: list[Pattern] = [
29
30
  re.compile(r"^(ft:)?gpt-4o(-.*)?$"),
30
31
  ]
31
- anthropic_naming_regexes: List[Pattern] = [
32
+ anthropic_naming_regexes: list[Pattern] = [
32
33
  re.compile(r"^claude-.*$"),
33
34
  ]
34
- gemini_naming_regexes: List[Pattern] = [
35
+ gemini_naming_regexes: list[Pattern] = [
35
36
  re.compile(r"^gemini-.*$"),
36
37
  re.compile(r"^gemma[2-9].*$"),
37
38
  ]
38
- deepseek_naming_regexes: List[Pattern] = [
39
+ deepseek_naming_regexes: list[Pattern] = [
39
40
  re.compile(r"^deepseek-.*$"),
40
41
  ]
41
- together_naming_regexes: List[Pattern] = [
42
+ together_naming_regexes: list[Pattern] = [
42
43
  re.compile(r"^.*\/.*$"),
43
44
  ]
44
45
 
45
- groq_naming_regexes: List[Pattern] = [
46
+ groq_naming_regexes: list[Pattern] = [
46
47
  re.compile(r"^llama-3.3-70b-versatile$"),
47
48
  re.compile(r"^llama-3.1-8b-instant$"),
48
49
  re.compile(r"^qwen-2.5-32b$"),
@@ -60,7 +61,7 @@ groq_naming_regexes: List[Pattern] = [
60
61
  re.compile(r"^moonshotai/kimi-k2-instruct$"),
61
62
  ]
62
63
 
63
- grok_naming_regexes: List[Pattern] = [
64
+ grok_naming_regexes: list[Pattern] = [
64
65
  re.compile(r"^grok-3-beta$"),
65
66
  re.compile(r"^grok-3-mini-beta$"),
66
67
  re.compile(r"^grok-beta$"),
@@ -68,16 +69,16 @@ grok_naming_regexes: List[Pattern] = [
68
69
  ]
69
70
 
70
71
 
71
- openrouter_naming_regexes: List[Pattern] = [
72
+ openrouter_naming_regexes: list[Pattern] = [
72
73
  re.compile(r"^openrouter/.*$"), # openrouter/model-name pattern
73
74
  ]
74
75
 
75
- openrouter_naming_regexes: List[Pattern] = [
76
+ openrouter_naming_regexes: list[Pattern] = [
76
77
  re.compile(r"^openrouter/.*$"), # openrouter/model-name pattern
77
78
  ]
78
79
 
79
80
  # Custom endpoint patterns - check these before generic patterns
80
- custom_endpoint_naming_regexes: List[Pattern] = [
81
+ custom_endpoint_naming_regexes: list[Pattern] = [
81
82
  # Modal endpoints: org--app.modal.run
82
83
  re.compile(r"^[a-zA-Z0-9\-]+--[a-zA-Z0-9\-]+\.modal\.run$"),
83
84
  # Generic domain patterns for custom endpoints
@@ -86,7 +87,7 @@ custom_endpoint_naming_regexes: List[Pattern] = [
86
87
  ]
87
88
 
88
89
  # Provider mapping for explicit provider overrides
89
- PROVIDER_MAP: Dict[str, Any] = {
90
+ PROVIDER_MAP: dict[str, Any] = {
90
91
  "openai": OpenAIStructuredOutputClient,
91
92
  "anthropic": AnthropicClient,
92
93
  "groq": GroqClient,
@@ -104,7 +105,7 @@ def get_client(
104
105
  model_name: str,
105
106
  with_formatting: bool = False,
106
107
  synth_logging: bool = True,
107
- provider: Optional[str] = None,
108
+ provider: str | None = None,
108
109
  ) -> Any:
109
110
  """
110
111
  Get a vendor client for the specified model.
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ import contextvars
4
+ from contextlib import contextmanager
5
+ from typing import Any
6
+
7
+ Rule = dict[str, Any]
8
+
9
+ _rules_ctx: contextvars.ContextVar[list[Rule] | None] = contextvars.ContextVar(
10
+ "injection_rules", default=None
11
+ )
12
+
13
+
14
+ def set_injection_rules(rules: list[Rule]):
15
+ """Set prompt-injection rules for the current context and return a reset token.
16
+
17
+ Each rule must be a dict with at least keys: "find" and "replace" (strings).
18
+ Optional: "roles" as a list of role names to scope the replacement.
19
+ """
20
+ if not isinstance(rules, list) or not all(
21
+ isinstance(r, dict) and "find" in r and "replace" in r for r in rules
22
+ ):
23
+ raise ValueError("Injection rules must be a list of dicts with 'find' and 'replace'")
24
+ return _rules_ctx.set(rules)
25
+
26
+
27
+ def get_injection_rules() -> list[Rule] | None:
28
+ """Get the current context's injection rules, if any."""
29
+ return _rules_ctx.get()
30
+
31
+
32
+ def clear_injection_rules(token) -> None:
33
+ """Reset the injection rules to the previous value using the provided token."""
34
+ _rules_ctx.reset(token)
35
+
36
+
37
+ def apply_injection(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
38
+ """Apply ordered substring replacements to text parts of messages in place.
39
+
40
+ - Only modifies `str` content or list parts where `part["type"] == "text"`.
41
+ - Honors optional `roles` scoping in each rule.
42
+ - Returns the input list for convenience.
43
+ """
44
+ rules = get_injection_rules()
45
+ if not rules:
46
+ return messages
47
+
48
+ for m in messages:
49
+ role = m.get("role")
50
+ content = m.get("content")
51
+ if isinstance(content, str):
52
+ new_content = content
53
+ for r in rules:
54
+ allowed_roles = r.get("roles")
55
+ if allowed_roles is not None and role not in allowed_roles:
56
+ continue
57
+ new_content = new_content.replace(str(r["find"]), str(r["replace"]))
58
+ m["content"] = new_content
59
+ elif isinstance(content, list):
60
+ for part in content:
61
+ if part.get("type") == "text":
62
+ text = part.get("text", "")
63
+ new_text = text
64
+ for r in rules:
65
+ allowed_roles = r.get("roles")
66
+ if allowed_roles is not None and role not in allowed_roles:
67
+ continue
68
+ new_text = new_text.replace(str(r["find"]), str(r["replace"]))
69
+ part["text"] = new_text
70
+ return messages
71
+
72
+
73
+ @contextmanager
74
+ def injection_rules_ctx(rules: list[Rule]):
75
+ """Context manager to temporarily apply injection rules within the block."""
76
+ tok = set_injection_rules(rules)
77
+ try:
78
+ yield
79
+ finally:
80
+ clear_injection_rules(tok)
@@ -0,0 +1,206 @@
1
+ from __future__ import annotations
2
+
3
+ import contextvars
4
+ from contextlib import contextmanager
5
+ from typing import Any
6
+
7
+ from synth_ai.lm.injection import (
8
+ apply_injection as _apply_injection,
9
+ )
10
+ from synth_ai.lm.injection import (
11
+ clear_injection_rules,
12
+ set_injection_rules,
13
+ )
14
+
15
+ # Context to hold a list of override specs to evaluate per-call
16
+ # Each spec shape (minimal v1):
17
+ # {
18
+ # "match": {"contains": "atm", "role": "user" | "system" | None},
19
+ # "injection_rules": [{"find": str, "replace": str, "roles": Optional[List[str]]}],
20
+ # "params": { ... api params to override ... },
21
+ # "tools": { ... optional tools overrides ... },
22
+ # }
23
+ _override_specs_ctx: contextvars.ContextVar[list[dict[str, Any]] | None] = (
24
+ contextvars.ContextVar("override_specs", default=None)
25
+ )
26
+
27
+ # ContextVars actually applied for the specific call once matched
28
+ _param_overrides_ctx: contextvars.ContextVar[dict[str, Any] | None] = contextvars.ContextVar(
29
+ "param_overrides", default=None
30
+ )
31
+ _tool_overrides_ctx: contextvars.ContextVar[dict[str, Any] | None] = contextvars.ContextVar(
32
+ "tool_overrides", default=None
33
+ )
34
+ _current_override_label_ctx: contextvars.ContextVar[str | None] = contextvars.ContextVar(
35
+ "override_label", default=None
36
+ )
37
+
38
+
39
+ def set_override_specs(specs: list[dict[str, Any]]):
40
+ if not isinstance(specs, list):
41
+ raise ValueError("override specs must be a list of dicts")
42
+ return _override_specs_ctx.set(specs)
43
+
44
+
45
+ def get_override_specs() -> list[dict[str, Any]] | None:
46
+ return _override_specs_ctx.get()
47
+
48
+
49
+ def clear_override_specs(token) -> None:
50
+ _override_specs_ctx.reset(token)
51
+
52
+
53
+ def _matches(spec: dict[str, Any], messages: list[dict[str, Any]]) -> bool:
54
+ match = spec.get("match") or {}
55
+ contains = match.get("contains")
56
+ role = match.get("role") # optional
57
+ if not contains:
58
+ # no match criteria means always apply
59
+ return True
60
+ contains_l = str(contains).lower()
61
+ for m in messages:
62
+ if role and m.get("role") != role:
63
+ continue
64
+ c = m.get("content")
65
+ if isinstance(c, str) and contains_l in c.lower():
66
+ return True
67
+ if isinstance(c, list):
68
+ for part in c:
69
+ if part.get("type") == "text" and contains_l in str(part.get("text", "")).lower():
70
+ return True
71
+ return False
72
+
73
+
74
+ def resolve_override_for_messages(messages: list[dict[str, Any]]) -> dict[str, Any] | None:
75
+ specs = get_override_specs() or []
76
+ for spec in specs:
77
+ try:
78
+ if _matches(spec, messages):
79
+ return spec
80
+ except Exception:
81
+ # On matcher errors, skip spec
82
+ continue
83
+ return None
84
+
85
+
86
+ def apply_injection(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
87
+ # Delegate to injection.apply_injection
88
+ return _apply_injection(messages)
89
+
90
+
91
+ def apply_param_overrides(api_params: dict[str, Any]) -> dict[str, Any]:
92
+ ov = _param_overrides_ctx.get()
93
+ if not ov:
94
+ return api_params
95
+ # Shallow merge only known keys users provided
96
+ for k, v in ov.items():
97
+ api_params[k] = v
98
+ return api_params
99
+
100
+
101
+ def apply_tool_overrides(api_params: dict[str, Any]) -> dict[str, Any]:
102
+ """Apply tool overrides to OpenAI/Anthropic-like api_params in place.
103
+
104
+ Supports keys under spec["tools"]:
105
+ - set_tools: replace tools entirely
106
+ - add_tools: append tools
107
+ - remove_tools_by_name: remove by function name
108
+ - tool_choice: set tool_choice param
109
+ """
110
+ ov = _tool_overrides_ctx.get()
111
+ if not ov:
112
+ return api_params
113
+ tov = ov.get("tools") if isinstance(ov, dict) else None
114
+ if tov:
115
+ tools = api_params.get("tools")
116
+ if "set_tools" in tov:
117
+ tools = tov["set_tools"]
118
+ if "add_tools" in tov:
119
+ tools = (tools or []) + tov["add_tools"]
120
+ if "remove_tools_by_name" in tov and tools:
121
+ names = set(tov["remove_tools_by_name"]) # function names
122
+ new_tools = []
123
+ for t in tools:
124
+ try:
125
+ # OpenAI dict style
126
+ fn = t.get("function", {}).get("name") if isinstance(t, dict) else None
127
+ except Exception:
128
+ fn = None
129
+ # If BaseTool objects slipped through
130
+ if fn is None:
131
+ fn = getattr(t, "function_name", None)
132
+ if fn is None or fn not in names:
133
+ new_tools.append(t)
134
+ tools = new_tools
135
+ if tools is not None:
136
+ api_params["tools"] = tools
137
+ if "tool_choice" in tov:
138
+ api_params["tool_choice"] = tov["tool_choice"]
139
+ return api_params
140
+
141
+
142
+ @contextmanager
143
+ def use_overrides_for_messages(messages: list[dict[str, Any]]):
144
+ """Resolve an override spec against messages and apply its contexts within the scope.
145
+
146
+ - Sets injection rules and param overrides if present on the matched spec.
147
+ - Yields, then resets ContextVars to previous values.
148
+ """
149
+ spec = resolve_override_for_messages(messages) or {}
150
+ inj_rules = spec.get("injection_rules")
151
+ params = spec.get("params")
152
+ inj_tok = None
153
+ param_tok = None
154
+ tool_tok = None
155
+ label_tok = None
156
+ try:
157
+ if inj_rules:
158
+ inj_tok = set_injection_rules(inj_rules)
159
+ if params:
160
+ param_tok = _param_overrides_ctx.set(params)
161
+ tools = spec.get("tools")
162
+ if tools:
163
+ tool_tok = _tool_overrides_ctx.set({"tools": tools})
164
+ lbl = spec.get("label")
165
+ if lbl:
166
+ label_tok = _current_override_label_ctx.set(str(lbl))
167
+ yield
168
+ finally:
169
+ if inj_tok is not None:
170
+ clear_injection_rules(inj_tok)
171
+ if param_tok is not None:
172
+ _param_overrides_ctx.reset(param_tok)
173
+ if tool_tok is not None:
174
+ _tool_overrides_ctx.reset(tool_tok)
175
+ if label_tok is not None:
176
+ _current_override_label_ctx.reset(label_tok)
177
+
178
+
179
+ def get_current_override_label() -> str | None:
180
+ return _current_override_label_ctx.get()
181
+
182
+
183
+ class LMOverridesContext:
184
+ """Context manager to register per-call override specs.
185
+
186
+ Usage:
187
+ with LMOverridesContext([
188
+ {"match": {"contains": "atm", "role": "user"}, "injection_rules": [...], "params": {...}},
189
+ {"match": {"contains": "refund"}, "params": {"temperature": 0.0}},
190
+ ]):
191
+ run_pipeline()
192
+ """
193
+
194
+ def __init__(self, override_specs: list[dict[str, Any]] | None | dict[str, Any] = None):
195
+ if isinstance(override_specs, dict):
196
+ override_specs = [override_specs]
197
+ self._specs = override_specs or []
198
+ self._tok = None
199
+
200
+ def __enter__(self):
201
+ self._tok = set_override_specs(self._specs)
202
+ return self
203
+
204
+ def __exit__(self, exc_type, exc, tb):
205
+ if self._tok is not None:
206
+ clear_override_specs(self._tok)
@@ -2,7 +2,7 @@
2
2
  Provider support for LLM services with integrated tracing.
3
3
  """
4
4
 
5
- from .openai import OpenAI, AsyncOpenAI
6
5
  from .anthropic import Anthropic, AsyncAnthropic
6
+ from .openai import AsyncOpenAI, OpenAI
7
7
 
8
8
  __all__ = ["OpenAI", "AsyncOpenAI", "Anthropic", "AsyncAnthropic"]