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.
- synth_ai/__init__.py +18 -9
- synth_ai/cli/__init__.py +10 -5
- synth_ai/cli/balance.py +22 -17
- synth_ai/cli/calc.py +2 -3
- synth_ai/cli/demo.py +3 -5
- synth_ai/cli/legacy_root_backup.py +58 -32
- synth_ai/cli/man.py +22 -19
- synth_ai/cli/recent.py +9 -8
- synth_ai/cli/root.py +58 -13
- synth_ai/cli/status.py +13 -6
- synth_ai/cli/traces.py +45 -21
- synth_ai/cli/watch.py +40 -37
- synth_ai/config/base_url.py +1 -3
- synth_ai/core/experiment.py +1 -2
- synth_ai/environments/__init__.py +2 -6
- synth_ai/environments/environment/artifacts/base.py +3 -1
- synth_ai/environments/environment/db/sqlite.py +1 -1
- synth_ai/environments/environment/registry.py +19 -20
- synth_ai/environments/environment/resources/sqlite.py +2 -3
- synth_ai/environments/environment/rewards/core.py +3 -2
- synth_ai/environments/environment/tools/__init__.py +6 -4
- synth_ai/environments/examples/crafter_classic/__init__.py +1 -1
- synth_ai/environments/examples/crafter_classic/engine.py +21 -17
- synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +1 -0
- synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +2 -1
- synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +2 -1
- synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +3 -2
- synth_ai/environments/examples/crafter_classic/environment.py +16 -15
- synth_ai/environments/examples/crafter_classic/taskset.py +2 -2
- synth_ai/environments/examples/crafter_classic/trace_hooks_v3.py +2 -3
- synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +2 -1
- synth_ai/environments/examples/crafter_custom/crafter/__init__.py +2 -2
- synth_ai/environments/examples/crafter_custom/crafter/config.py +2 -2
- synth_ai/environments/examples/crafter_custom/crafter/env.py +1 -5
- synth_ai/environments/examples/crafter_custom/crafter/objects.py +1 -2
- synth_ai/environments/examples/crafter_custom/crafter/worldgen.py +1 -2
- synth_ai/environments/examples/crafter_custom/dataset_builder.py +5 -5
- synth_ai/environments/examples/crafter_custom/environment.py +13 -13
- synth_ai/environments/examples/crafter_custom/run_dataset.py +5 -5
- synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +2 -2
- synth_ai/environments/examples/enron/art_helpers/local_email_db.py +5 -4
- synth_ai/environments/examples/enron/art_helpers/types_enron.py +2 -1
- synth_ai/environments/examples/enron/engine.py +18 -14
- synth_ai/environments/examples/enron/environment.py +12 -11
- synth_ai/environments/examples/enron/taskset.py +7 -7
- synth_ai/environments/examples/minigrid/__init__.py +6 -6
- synth_ai/environments/examples/minigrid/engine.py +6 -6
- synth_ai/environments/examples/minigrid/environment.py +6 -6
- synth_ai/environments/examples/minigrid/puzzle_loader.py +3 -2
- synth_ai/environments/examples/minigrid/taskset.py +13 -13
- synth_ai/environments/examples/nethack/achievements.py +1 -1
- synth_ai/environments/examples/nethack/engine.py +8 -7
- synth_ai/environments/examples/nethack/environment.py +10 -9
- synth_ai/environments/examples/nethack/helpers/__init__.py +8 -9
- synth_ai/environments/examples/nethack/helpers/action_mapping.py +1 -1
- synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +2 -1
- synth_ai/environments/examples/nethack/helpers/observation_utils.py +1 -1
- synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +3 -4
- synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +6 -5
- synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +5 -5
- synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +7 -6
- synth_ai/environments/examples/nethack/taskset.py +5 -5
- synth_ai/environments/examples/red/engine.py +9 -8
- synth_ai/environments/examples/red/engine_helpers/reward_components.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +7 -7
- synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +2 -1
- synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +3 -2
- synth_ai/environments/examples/red/engine_helpers/state_extraction.py +2 -1
- synth_ai/environments/examples/red/environment.py +18 -15
- synth_ai/environments/examples/red/taskset.py +5 -3
- synth_ai/environments/examples/sokoban/engine.py +16 -13
- synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +3 -2
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +2 -1
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +1 -1
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +7 -5
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +1 -1
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +2 -1
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +5 -4
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +3 -2
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +2 -1
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +5 -4
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +1 -1
- synth_ai/environments/examples/sokoban/environment.py +15 -14
- synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +5 -3
- synth_ai/environments/examples/sokoban/puzzle_loader.py +3 -2
- synth_ai/environments/examples/sokoban/taskset.py +13 -10
- synth_ai/environments/examples/tictactoe/engine.py +6 -6
- synth_ai/environments/examples/tictactoe/environment.py +8 -7
- synth_ai/environments/examples/tictactoe/taskset.py +6 -5
- synth_ai/environments/examples/verilog/engine.py +4 -3
- synth_ai/environments/examples/verilog/environment.py +11 -10
- synth_ai/environments/examples/verilog/taskset.py +14 -12
- synth_ai/environments/examples/wordle/__init__.py +29 -0
- synth_ai/environments/examples/wordle/engine.py +398 -0
- synth_ai/environments/examples/wordle/environment.py +159 -0
- synth_ai/environments/examples/wordle/helpers/generate_instances_wordfreq.py +75 -0
- synth_ai/environments/examples/wordle/taskset.py +230 -0
- synth_ai/environments/reproducibility/core.py +1 -1
- synth_ai/environments/reproducibility/tree.py +21 -21
- synth_ai/environments/service/app.py +11 -2
- synth_ai/environments/service/core_routes.py +137 -105
- synth_ai/environments/service/external_registry.py +1 -2
- synth_ai/environments/service/registry.py +1 -1
- synth_ai/environments/stateful/core.py +1 -2
- synth_ai/environments/stateful/engine.py +1 -1
- synth_ai/environments/tasks/api.py +4 -4
- synth_ai/environments/tasks/core.py +14 -12
- synth_ai/environments/tasks/filters.py +6 -4
- synth_ai/environments/tasks/utils.py +13 -11
- synth_ai/evals/base.py +2 -3
- synth_ai/experimental/synth_oss.py +4 -4
- synth_ai/learning/gateway.py +1 -3
- synth_ai/learning/prompts/banking77_injection_eval.py +168 -0
- synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +213 -0
- synth_ai/learning/prompts/mipro.py +282 -1
- synth_ai/learning/prompts/random_search.py +246 -0
- synth_ai/learning/prompts/run_mipro_banking77.py +172 -0
- synth_ai/learning/prompts/run_random_search_banking77.py +324 -0
- synth_ai/lm/__init__.py +5 -5
- synth_ai/lm/caching/ephemeral.py +9 -9
- synth_ai/lm/caching/handler.py +20 -20
- synth_ai/lm/caching/persistent.py +10 -10
- synth_ai/lm/config.py +3 -3
- synth_ai/lm/constants.py +7 -7
- synth_ai/lm/core/all.py +17 -3
- synth_ai/lm/core/exceptions.py +0 -2
- synth_ai/lm/core/main.py +26 -41
- synth_ai/lm/core/main_v3.py +20 -10
- synth_ai/lm/core/vendor_clients.py +18 -17
- synth_ai/lm/injection.py +80 -0
- synth_ai/lm/overrides.py +206 -0
- synth_ai/lm/provider_support/__init__.py +1 -1
- synth_ai/lm/provider_support/anthropic.py +51 -24
- synth_ai/lm/provider_support/openai.py +51 -22
- synth_ai/lm/structured_outputs/handler.py +34 -32
- synth_ai/lm/structured_outputs/inject.py +24 -27
- synth_ai/lm/structured_outputs/rehabilitate.py +19 -15
- synth_ai/lm/tools/base.py +17 -16
- synth_ai/lm/unified_interface.py +17 -18
- synth_ai/lm/vendors/base.py +20 -18
- synth_ai/lm/vendors/core/anthropic_api.py +50 -25
- synth_ai/lm/vendors/core/gemini_api.py +31 -36
- synth_ai/lm/vendors/core/mistral_api.py +19 -19
- synth_ai/lm/vendors/core/openai_api.py +11 -10
- synth_ai/lm/vendors/openai_standard.py +144 -88
- synth_ai/lm/vendors/openai_standard_responses.py +74 -61
- synth_ai/lm/vendors/retries.py +9 -1
- synth_ai/lm/vendors/supported/custom_endpoint.py +26 -26
- synth_ai/lm/vendors/supported/deepseek.py +10 -10
- synth_ai/lm/vendors/supported/grok.py +8 -8
- synth_ai/lm/vendors/supported/ollama.py +2 -1
- synth_ai/lm/vendors/supported/openrouter.py +11 -9
- synth_ai/lm/vendors/synth_client.py +69 -63
- synth_ai/lm/warmup.py +8 -7
- synth_ai/tracing/__init__.py +22 -10
- synth_ai/tracing_v1/__init__.py +22 -20
- synth_ai/tracing_v3/__init__.py +7 -7
- synth_ai/tracing_v3/abstractions.py +56 -52
- synth_ai/tracing_v3/config.py +4 -2
- synth_ai/tracing_v3/db_config.py +6 -8
- synth_ai/tracing_v3/decorators.py +29 -30
- synth_ai/tracing_v3/examples/basic_usage.py +12 -12
- synth_ai/tracing_v3/hooks.py +21 -21
- synth_ai/tracing_v3/llm_call_record_helpers.py +85 -98
- synth_ai/tracing_v3/lm_call_record_abstractions.py +2 -4
- synth_ai/tracing_v3/migration_helper.py +3 -5
- synth_ai/tracing_v3/replica_sync.py +30 -32
- synth_ai/tracing_v3/session_tracer.py +35 -29
- synth_ai/tracing_v3/storage/__init__.py +1 -1
- synth_ai/tracing_v3/storage/base.py +8 -7
- synth_ai/tracing_v3/storage/config.py +4 -4
- synth_ai/tracing_v3/storage/factory.py +4 -4
- synth_ai/tracing_v3/storage/utils.py +9 -9
- synth_ai/tracing_v3/turso/__init__.py +3 -3
- synth_ai/tracing_v3/turso/daemon.py +9 -9
- synth_ai/tracing_v3/turso/manager.py +60 -48
- synth_ai/tracing_v3/turso/models.py +24 -19
- synth_ai/tracing_v3/utils.py +5 -5
- synth_ai/tui/__main__.py +1 -1
- synth_ai/tui/cli/query_experiments.py +2 -3
- synth_ai/tui/cli/query_experiments_v3.py +2 -3
- synth_ai/tui/dashboard.py +97 -86
- synth_ai/v0/tracing/abstractions.py +28 -28
- synth_ai/v0/tracing/base_client.py +9 -9
- synth_ai/v0/tracing/client_manager.py +7 -7
- synth_ai/v0/tracing/config.py +7 -7
- synth_ai/v0/tracing/context.py +6 -6
- synth_ai/v0/tracing/decorators.py +6 -5
- synth_ai/v0/tracing/events/manage.py +1 -1
- synth_ai/v0/tracing/events/store.py +5 -4
- synth_ai/v0/tracing/immediate_client.py +4 -5
- synth_ai/v0/tracing/local.py +3 -3
- synth_ai/v0/tracing/log_client_base.py +4 -5
- synth_ai/v0/tracing/retry_queue.py +5 -6
- synth_ai/v0/tracing/trackers.py +25 -25
- synth_ai/v0/tracing/upload.py +6 -0
- synth_ai/v0/tracing_v1/__init__.py +1 -1
- synth_ai/v0/tracing_v1/abstractions.py +28 -28
- synth_ai/v0/tracing_v1/base_client.py +9 -9
- synth_ai/v0/tracing_v1/client_manager.py +7 -7
- synth_ai/v0/tracing_v1/config.py +7 -7
- synth_ai/v0/tracing_v1/context.py +6 -6
- synth_ai/v0/tracing_v1/decorators.py +7 -6
- synth_ai/v0/tracing_v1/events/manage.py +1 -1
- synth_ai/v0/tracing_v1/events/store.py +5 -4
- synth_ai/v0/tracing_v1/immediate_client.py +4 -5
- synth_ai/v0/tracing_v1/local.py +3 -3
- synth_ai/v0/tracing_v1/log_client_base.py +4 -5
- synth_ai/v0/tracing_v1/retry_queue.py +5 -6
- synth_ai/v0/tracing_v1/trackers.py +25 -25
- synth_ai/v0/tracing_v1/upload.py +25 -24
- synth_ai/zyk/__init__.py +1 -0
- {synth_ai-0.2.4.dev5.dist-info → synth_ai-0.2.4.dev7.dist-info}/METADATA +2 -11
- synth_ai-0.2.4.dev7.dist-info/RECORD +299 -0
- synth_ai-0.2.4.dev5.dist-info/RECORD +0 -287
- {synth_ai-0.2.4.dev5.dist-info → synth_ai-0.2.4.dev7.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.4.dev5.dist-info → synth_ai-0.2.4.dev7.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.4.dev5.dist-info → synth_ai-0.2.4.dev7.dist-info}/licenses/LICENSE +0 -0
- {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.
|
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:
|
22
|
-
model_name:
|
23
|
-
) ->
|
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:
|
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:
|
117
|
-
|
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:
|
174
|
-
user_message:
|
175
|
-
messages:
|
176
|
-
images_as_bytes:
|
177
|
-
response_model:
|
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:
|
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:
|
235
|
-
user_message:
|
236
|
-
messages:
|
237
|
-
images_as_bytes:
|
238
|
-
response_model:
|
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:
|
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:
|
304
|
-
argument_values:
|
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
|
|
synth_ai/lm/core/main_v3.py
CHANGED
@@ -53,7 +53,9 @@ def build_messages(
|
|
53
53
|
],
|
54
54
|
},
|
55
55
|
]
|
56
|
-
elif len(images_bytes) > 0 and any(
|
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",
|
228
|
-
"
|
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,
|
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
|
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
|
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:
|
26
|
+
openai_naming_regexes: list[Pattern] = [
|
26
27
|
re.compile(r"^(ft:)?(o[1,3,4](-.*)?|gpt-.*)$"),
|
27
28
|
]
|
28
|
-
openai_formatting_model_regexes:
|
29
|
+
openai_formatting_model_regexes: list[Pattern] = [
|
29
30
|
re.compile(r"^(ft:)?gpt-4o(-.*)?$"),
|
30
31
|
]
|
31
|
-
anthropic_naming_regexes:
|
32
|
+
anthropic_naming_regexes: list[Pattern] = [
|
32
33
|
re.compile(r"^claude-.*$"),
|
33
34
|
]
|
34
|
-
gemini_naming_regexes:
|
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:
|
39
|
+
deepseek_naming_regexes: list[Pattern] = [
|
39
40
|
re.compile(r"^deepseek-.*$"),
|
40
41
|
]
|
41
|
-
together_naming_regexes:
|
42
|
+
together_naming_regexes: list[Pattern] = [
|
42
43
|
re.compile(r"^.*\/.*$"),
|
43
44
|
]
|
44
45
|
|
45
|
-
groq_naming_regexes:
|
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:
|
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:
|
72
|
+
openrouter_naming_regexes: list[Pattern] = [
|
72
73
|
re.compile(r"^openrouter/.*$"), # openrouter/model-name pattern
|
73
74
|
]
|
74
75
|
|
75
|
-
openrouter_naming_regexes:
|
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:
|
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:
|
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:
|
108
|
+
provider: str | None = None,
|
108
109
|
) -> Any:
|
109
110
|
"""
|
110
111
|
Get a vendor client for the specified model.
|
synth_ai/lm/injection.py
ADDED
@@ -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)
|
synth_ai/lm/overrides.py
ADDED
@@ -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"]
|