synth-ai 0.2.4.dev6__py3-none-any.whl → 0.2.4.dev8__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 +25 -32
- 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 +47 -2
- 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 +13 -13
- 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 +5 -5
- synth_ai/environments/examples/wordle/engine.py +32 -25
- synth_ai/environments/examples/wordle/environment.py +21 -16
- synth_ai/environments/examples/wordle/helpers/generate_instances_wordfreq.py +6 -6
- synth_ai/environments/examples/wordle/taskset.py +20 -12
- synth_ai/environments/reproducibility/core.py +1 -1
- synth_ai/environments/reproducibility/tree.py +21 -21
- synth_ai/environments/service/app.py +3 -2
- synth_ai/environments/service/core_routes.py +104 -110
- 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/http.py +102 -0
- synth_ai/inference/__init__.py +7 -0
- synth_ai/inference/client.py +20 -0
- synth_ai/jobs/client.py +246 -0
- synth_ai/learning/__init__.py +24 -0
- synth_ai/learning/client.py +149 -0
- synth_ai/learning/config.py +43 -0
- synth_ai/learning/constants.py +29 -0
- synth_ai/learning/ft_client.py +59 -0
- synth_ai/learning/gateway.py +1 -3
- synth_ai/learning/health.py +43 -0
- synth_ai/learning/jobs.py +205 -0
- synth_ai/learning/prompts/banking77_injection_eval.py +15 -10
- synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +26 -14
- synth_ai/learning/prompts/mipro.py +61 -52
- synth_ai/learning/prompts/random_search.py +42 -43
- synth_ai/learning/prompts/run_mipro_banking77.py +32 -20
- synth_ai/learning/prompts/run_random_search_banking77.py +71 -52
- synth_ai/learning/rl_client.py +256 -0
- synth_ai/learning/sse.py +58 -0
- synth_ai/learning/validators.py +48 -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 +33 -10
- synth_ai/lm/core/synth_models.py +48 -0
- synth_ai/lm/core/vendor_clients.py +26 -22
- synth_ai/lm/injection.py +7 -8
- synth_ai/lm/overrides.py +21 -19
- synth_ai/lm/provider_support/__init__.py +1 -1
- synth_ai/lm/provider_support/anthropic.py +15 -15
- synth_ai/lm/provider_support/openai.py +23 -21
- 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 +36 -27
- 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 +42 -13
- synth_ai/lm/vendors/openai_standard.py +158 -101
- 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 +38 -28
- 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 +425 -75
- synth_ai/lm/warmup.py +8 -7
- synth_ai/rl/__init__.py +30 -0
- synth_ai/rl/contracts.py +32 -0
- synth_ai/rl/env_keys.py +137 -0
- synth_ai/rl/secrets.py +19 -0
- synth_ai/scripts/verify_rewards.py +100 -0
- synth_ai/task/__init__.py +10 -0
- synth_ai/task/contracts.py +120 -0
- synth_ai/task/health.py +28 -0
- synth_ai/task/validators.py +12 -0
- 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 +24 -22
- 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 +158 -31
- 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 +278 -48
- synth_ai/tracing_v3/turso/models.py +77 -19
- synth_ai/tracing_v3/utils.py +5 -5
- 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.dev8.dist-info/METADATA +635 -0
- synth_ai-0.2.4.dev8.dist-info/RECORD +317 -0
- synth_ai/tui/__init__.py +0 -1
- synth_ai/tui/__main__.py +0 -13
- synth_ai/tui/cli/__init__.py +0 -1
- synth_ai/tui/cli/query_experiments.py +0 -165
- synth_ai/tui/cli/query_experiments_v3.py +0 -165
- synth_ai/tui/dashboard.py +0 -329
- synth_ai-0.2.4.dev6.dist-info/METADATA +0 -203
- synth_ai-0.2.4.dev6.dist-info/RECORD +0 -299
- {synth_ai-0.2.4.dev6.dist-info → synth_ai-0.2.4.dev8.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.4.dev6.dist-info → synth_ai-0.2.4.dev8.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.4.dev6.dist-info → synth_ai-0.2.4.dev8.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.4.dev6.dist-info → synth_ai-0.2.4.dev8.dist-info}/top_level.txt +0 -0
@@ -5,14 +5,11 @@ This module contains the Responses API and Harmony encoding methods
|
|
5
5
|
that extend the OpenAIStandard class functionality.
|
6
6
|
"""
|
7
7
|
|
8
|
-
from typing import Any, Dict, List, Optional
|
9
8
|
import uuid
|
10
|
-
from
|
9
|
+
from typing import Any
|
11
10
|
|
12
11
|
from synth_ai.lm.tools.base import BaseTool
|
13
12
|
from synth_ai.lm.vendors.base import BaseLMResponse
|
14
|
-
from synth_ai.lm.vendors.retries import MAX_BACKOFF
|
15
|
-
import backoff
|
16
13
|
|
17
14
|
|
18
15
|
def _silent_backoff_handler(_details):
|
@@ -27,23 +24,23 @@ DEFAULT_EXCEPTIONS_TO_RETRY = (
|
|
27
24
|
|
28
25
|
class OpenAIResponsesAPIMixin:
|
29
26
|
"""Mixin class providing Responses API functionality for OpenAI vendors."""
|
30
|
-
|
27
|
+
|
31
28
|
async def _hit_api_async_responses(
|
32
29
|
self,
|
33
30
|
model: str,
|
34
|
-
messages:
|
35
|
-
lm_config:
|
36
|
-
previous_response_id:
|
31
|
+
messages: list[dict[str, Any]],
|
32
|
+
lm_config: dict[str, Any],
|
33
|
+
previous_response_id: str | None = None,
|
37
34
|
use_ephemeral_cache_only: bool = False,
|
38
|
-
tools:
|
35
|
+
tools: list[BaseTool] | None = None,
|
39
36
|
) -> BaseLMResponse:
|
40
37
|
"""Use OpenAI Responses API for supported models."""
|
41
|
-
|
38
|
+
|
42
39
|
print(f"🔍 RESPONSES API: Called for model {model}")
|
43
40
|
print(f"🔍 RESPONSES API: previous_response_id = {previous_response_id}")
|
44
|
-
|
41
|
+
|
45
42
|
# Check if the client has responses attribute
|
46
|
-
if not hasattr(self.async_client,
|
43
|
+
if not hasattr(self.async_client, "responses"):
|
47
44
|
print("🔍 RESPONSES API: Client doesn't have responses attribute, using fallback")
|
48
45
|
# Fallback - use chat completions with simulated response_id
|
49
46
|
response = await self._hit_api_async(
|
@@ -53,21 +50,22 @@ class OpenAIResponsesAPIMixin:
|
|
53
50
|
use_ephemeral_cache_only=use_ephemeral_cache_only,
|
54
51
|
tools=tools,
|
55
52
|
)
|
56
|
-
|
53
|
+
|
57
54
|
# Add Responses API fields
|
58
55
|
if not response.response_id:
|
59
56
|
import uuid
|
57
|
+
|
60
58
|
response.response_id = str(uuid.uuid4())
|
61
59
|
response.api_type = "responses"
|
62
60
|
return response
|
63
|
-
|
61
|
+
|
64
62
|
# Use the official Responses API
|
65
63
|
try:
|
66
64
|
# Common API call params for Responses API
|
67
65
|
api_params = {
|
68
66
|
"model": model,
|
69
67
|
}
|
70
|
-
|
68
|
+
|
71
69
|
# For Responses API, we use 'input' parameter
|
72
70
|
if previous_response_id:
|
73
71
|
# Continue existing thread
|
@@ -92,35 +90,37 @@ class OpenAIResponsesAPIMixin:
|
|
92
90
|
elif role == "assistant":
|
93
91
|
input_parts.append(f"Assistant: {content}")
|
94
92
|
api_params["input"] = "\n".join(input_parts)
|
95
|
-
|
93
|
+
|
96
94
|
# Add tools if provided
|
97
95
|
if tools and all(isinstance(tool, BaseTool) for tool in tools):
|
98
96
|
api_params["tools"] = [tool.to_openai_tool() for tool in tools]
|
99
97
|
elif tools:
|
100
98
|
api_params["tools"] = tools
|
101
|
-
|
99
|
+
|
102
100
|
# Add other parameters from lm_config if needed
|
103
101
|
if "max_tokens" in lm_config:
|
104
102
|
api_params["max_tokens"] = lm_config["max_tokens"]
|
105
|
-
|
103
|
+
|
106
104
|
print(f"🔍 RESPONSES API: Calling with params: {list(api_params.keys())}")
|
107
|
-
|
105
|
+
|
108
106
|
# Call the Responses API
|
109
107
|
response = await self.async_client.responses.create(**api_params)
|
110
|
-
|
108
|
+
|
111
109
|
print(f"🔍 RESPONSES API: Response received, type: {type(response)}")
|
112
|
-
|
110
|
+
|
113
111
|
# Extract fields from response
|
114
|
-
output_text = getattr(response,
|
115
|
-
reasoning_obj = getattr(response,
|
116
|
-
response_id = getattr(response,
|
117
|
-
|
112
|
+
output_text = getattr(response, "output_text", getattr(response, "content", ""))
|
113
|
+
reasoning_obj = getattr(response, "reasoning", None)
|
114
|
+
response_id = getattr(response, "id", None)
|
115
|
+
|
118
116
|
# Debug reasoning type (only first time)
|
119
|
-
if reasoning_obj and not hasattr(self,
|
117
|
+
if reasoning_obj and not hasattr(self, "_reasoning_logged"):
|
120
118
|
print(f"🔍 RESPONSES API: Reasoning type: {type(reasoning_obj)}")
|
121
|
-
print(
|
119
|
+
print(
|
120
|
+
f"🔍 RESPONSES API: Reasoning attributes: {[x for x in dir(reasoning_obj) if not x.startswith('_')]}"
|
121
|
+
)
|
122
122
|
self._reasoning_logged = True
|
123
|
-
|
123
|
+
|
124
124
|
# Handle reasoning - it might be an object or a string
|
125
125
|
reasoning = None
|
126
126
|
if reasoning_obj:
|
@@ -130,22 +130,23 @@ class OpenAIResponsesAPIMixin:
|
|
130
130
|
else:
|
131
131
|
# OpenAI returns a Reasoning object
|
132
132
|
# Try to get summary first, but preserve entire object if no summary
|
133
|
-
if hasattr(reasoning_obj,
|
133
|
+
if hasattr(reasoning_obj, "summary") and reasoning_obj.summary:
|
134
134
|
reasoning = reasoning_obj.summary
|
135
135
|
else:
|
136
136
|
# Preserve the full object structure as JSON
|
137
137
|
# This includes effort level and any other fields
|
138
|
-
if hasattr(reasoning_obj,
|
138
|
+
if hasattr(reasoning_obj, "model_dump_json"):
|
139
139
|
reasoning = reasoning_obj.model_dump_json()
|
140
|
-
elif hasattr(reasoning_obj,
|
140
|
+
elif hasattr(reasoning_obj, "to_dict"):
|
141
141
|
import json
|
142
|
+
|
142
143
|
reasoning = json.dumps(reasoning_obj.to_dict())
|
143
144
|
else:
|
144
145
|
reasoning = str(reasoning_obj)
|
145
|
-
|
146
|
+
|
146
147
|
# Handle tool calls if present
|
147
148
|
tool_calls = None
|
148
|
-
if hasattr(response,
|
149
|
+
if hasattr(response, "tool_calls") and response.tool_calls:
|
149
150
|
tool_calls = [
|
150
151
|
{
|
151
152
|
"id": tc.id,
|
@@ -157,9 +158,9 @@ class OpenAIResponsesAPIMixin:
|
|
157
158
|
}
|
158
159
|
for tc in response.tool_calls
|
159
160
|
]
|
160
|
-
|
161
|
+
|
161
162
|
print(f"🔍 RESPONSES API: Extracted response_id = {response_id}")
|
162
|
-
|
163
|
+
|
163
164
|
return BaseLMResponse(
|
164
165
|
raw_response=output_text,
|
165
166
|
response_id=response_id,
|
@@ -167,7 +168,7 @@ class OpenAIResponsesAPIMixin:
|
|
167
168
|
api_type="responses",
|
168
169
|
tool_calls=tool_calls,
|
169
170
|
)
|
170
|
-
|
171
|
+
|
171
172
|
except (AttributeError, Exception) as e:
|
172
173
|
print(f"🔍 RESPONSES API: Error calling Responses API: {e}")
|
173
174
|
# No fallback - raise the error
|
@@ -176,68 +177,80 @@ class OpenAIResponsesAPIMixin:
|
|
176
177
|
async def _hit_api_async_harmony(
|
177
178
|
self,
|
178
179
|
model: str,
|
179
|
-
messages:
|
180
|
-
lm_config:
|
181
|
-
previous_response_id:
|
180
|
+
messages: list[dict[str, Any]],
|
181
|
+
lm_config: dict[str, Any],
|
182
|
+
previous_response_id: str | None = None,
|
182
183
|
use_ephemeral_cache_only: bool = False,
|
183
|
-
tools:
|
184
|
+
tools: list[BaseTool] | None = None,
|
184
185
|
) -> BaseLMResponse:
|
185
186
|
"""Use Harmony encoding for OSS-GPT models."""
|
186
187
|
if not self.harmony_available:
|
187
|
-
raise ImportError(
|
188
|
-
|
189
|
-
|
190
|
-
|
188
|
+
raise ImportError(
|
189
|
+
"openai-harmony package required for OSS-GPT models. Install with: pip install openai-harmony"
|
190
|
+
)
|
191
|
+
|
192
|
+
from openai_harmony import Conversation, Message, Role
|
193
|
+
|
191
194
|
# Convert messages to Harmony format
|
192
195
|
harmony_messages = []
|
193
196
|
for msg in messages:
|
194
|
-
role =
|
195
|
-
Role.
|
197
|
+
role = (
|
198
|
+
Role.SYSTEM
|
199
|
+
if msg["role"] == "system"
|
200
|
+
else (Role.USER if msg["role"] == "user" else Role.ASSISTANT)
|
196
201
|
)
|
197
202
|
content = msg["content"]
|
198
203
|
# Handle multimodal content
|
199
204
|
if isinstance(content, list):
|
200
205
|
# Extract text content for now
|
201
|
-
text_parts = [
|
206
|
+
text_parts = [
|
207
|
+
part.get("text", "") for part in content if part.get("type") == "text"
|
208
|
+
]
|
202
209
|
content = " ".join(text_parts)
|
203
210
|
harmony_messages.append(Message.from_role_and_content(role, content))
|
204
|
-
|
211
|
+
|
205
212
|
conv = Conversation.from_messages(harmony_messages)
|
206
213
|
tokens = self.harmony_enc.render_conversation_for_completion(conv, Role.ASSISTANT)
|
207
|
-
|
214
|
+
|
208
215
|
# For now, we'll need to integrate with Synth GPU endpoint
|
209
216
|
# This would require the actual endpoint to be configured
|
210
217
|
# Placeholder for actual Synth GPU call
|
211
|
-
import aiohttp
|
212
218
|
import os
|
213
|
-
|
219
|
+
|
220
|
+
import aiohttp
|
221
|
+
|
214
222
|
synth_gpu_endpoint = os.getenv("SYNTH_GPU_HARMONY_ENDPOINT")
|
215
223
|
if not synth_gpu_endpoint:
|
216
224
|
raise ValueError("SYNTH_GPU_HARMONY_ENDPOINT environment variable not set")
|
217
|
-
|
218
|
-
async with aiohttp.ClientSession() as session
|
219
|
-
async with session.post(
|
225
|
+
|
226
|
+
async with aiohttp.ClientSession() as session, session.post(
|
220
227
|
f"{synth_gpu_endpoint}/v1/completions",
|
221
228
|
json={
|
222
229
|
"model": model,
|
223
230
|
"prompt": tokens,
|
224
231
|
"max_tokens": lm_config.get("max_tokens", 4096),
|
225
232
|
"temperature": lm_config.get("temperature", 0.8),
|
226
|
-
}
|
233
|
+
},
|
227
234
|
) as resp:
|
228
235
|
result = await resp.json()
|
229
|
-
|
236
|
+
|
230
237
|
# Parse response using Harmony
|
231
238
|
response_tokens = result.get("choices", [{}])[0].get("text", "")
|
232
|
-
parsed = self.harmony_enc.parse_messages_from_completion_tokens(
|
233
|
-
|
239
|
+
parsed = self.harmony_enc.parse_messages_from_completion_tokens(
|
240
|
+
response_tokens, Role.ASSISTANT
|
241
|
+
)
|
242
|
+
|
234
243
|
if parsed:
|
235
|
-
assistant_msg =
|
244
|
+
assistant_msg = (
|
245
|
+
parsed[-1].content_text()
|
246
|
+
if hasattr(parsed[-1], "content_text")
|
247
|
+
else str(parsed[-1])
|
248
|
+
)
|
236
249
|
else:
|
237
250
|
assistant_msg = response_tokens
|
238
|
-
|
251
|
+
|
239
252
|
return BaseLMResponse(
|
240
253
|
raw_response=assistant_msg,
|
241
254
|
response_id=previous_response_id or str(uuid.uuid4()),
|
242
255
|
api_type="harmony",
|
243
|
-
)
|
256
|
+
)
|
synth_ai/lm/vendors/retries.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
import backoff
|
2
1
|
import os
|
3
2
|
|
3
|
+
import backoff
|
4
|
+
|
4
5
|
# Number of retry attempts that some legacy decorators rely on.
|
5
6
|
BACKOFF_TOLERANCE: int = 20
|
6
7
|
|
@@ -12,3 +13,10 @@ try:
|
|
12
13
|
MAX_BACKOFF: int = max(1, int(os.getenv("SYNTH_AI_MAX_BACKOFF", "120")))
|
13
14
|
except ValueError:
|
14
15
|
MAX_BACKOFF = 120
|
16
|
+
|
17
|
+
# Re-export backoff for convenient import patterns elsewhere
|
18
|
+
__all__ = [
|
19
|
+
"BACKOFF_TOLERANCE",
|
20
|
+
"MAX_BACKOFF",
|
21
|
+
"backoff",
|
22
|
+
]
|
@@ -1,22 +1,22 @@
|
|
1
|
-
import re
|
2
|
-
import os
|
3
|
-
import json
|
4
1
|
import asyncio
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
import random
|
5
|
+
import re
|
5
6
|
import time
|
6
|
-
from typing import Any
|
7
|
-
|
7
|
+
from typing import Any
|
8
|
+
|
8
9
|
import httpx
|
10
|
+
import requests
|
9
11
|
from requests.adapters import HTTPAdapter
|
10
12
|
from urllib3.util.retry import Retry
|
11
|
-
import random
|
12
|
-
from urllib.parse import urlparse
|
13
13
|
|
14
|
-
from synth_ai.lm.vendors.base import BaseLMResponse, VendorBase
|
15
|
-
from synth_ai.lm.tools.base import BaseTool
|
16
14
|
from synth_ai.lm.caching.initialize import get_cache_handler
|
15
|
+
from synth_ai.lm.tools.base import BaseTool
|
16
|
+
from synth_ai.lm.vendors.base import BaseLMResponse, VendorBase
|
17
17
|
|
18
18
|
# Exception types for retry
|
19
|
-
CUSTOM_ENDPOINT_EXCEPTIONS_TO_RETRY:
|
19
|
+
CUSTOM_ENDPOINT_EXCEPTIONS_TO_RETRY: tuple[type[Exception], ...] = (
|
20
20
|
requests.RequestException,
|
21
21
|
requests.Timeout,
|
22
22
|
httpx.RequestError,
|
@@ -28,7 +28,7 @@ class CustomEndpointAPI(VendorBase):
|
|
28
28
|
"""Generic vendor client for custom OpenAI-compatible endpoints."""
|
29
29
|
|
30
30
|
used_for_structured_outputs: bool = False
|
31
|
-
exceptions_to_retry:
|
31
|
+
exceptions_to_retry: list = list(CUSTOM_ENDPOINT_EXCEPTIONS_TO_RETRY)
|
32
32
|
|
33
33
|
def __init__(self, endpoint_url: str):
|
34
34
|
# Validate and sanitize URL
|
@@ -38,8 +38,18 @@ class CustomEndpointAPI(VendorBase):
|
|
38
38
|
# Construct full chat completions URL
|
39
39
|
if endpoint_url.endswith("/"):
|
40
40
|
endpoint_url = endpoint_url[:-1]
|
41
|
-
|
42
|
-
|
41
|
+
|
42
|
+
# Handle full URLs that already include protocol
|
43
|
+
if endpoint_url.startswith(("http://", "https://")):
|
44
|
+
# Remove protocol and domain part, keep only the base path if any
|
45
|
+
parsed = endpoint_url.replace("https://", "").replace("http://", "")
|
46
|
+
base_url = parsed.split("/")[0] # Get domain only
|
47
|
+
self.chat_completions_url = f"https://{base_url}/chat/completions"
|
48
|
+
self.health_url = f"https://{base_url}/health"
|
49
|
+
else:
|
50
|
+
# Original logic for domain-only URLs
|
51
|
+
self.chat_completions_url = f"https://{endpoint_url}/chat/completions"
|
52
|
+
self.health_url = f"https://{endpoint_url}/health"
|
43
53
|
|
44
54
|
# Setup session with connection pooling and retries
|
45
55
|
self.session = self._create_session()
|
@@ -89,7 +99,7 @@ class CustomEndpointAPI(VendorBase):
|
|
89
99
|
|
90
100
|
# Limit URL length
|
91
101
|
if len(url) > 256:
|
92
|
-
raise ValueError(
|
102
|
+
raise ValueError("Endpoint URL too long (max 256 chars)")
|
93
103
|
|
94
104
|
# Basic URL format check
|
95
105
|
if not re.match(r"^[a-zA-Z0-9\-._~:/?#\[\]@!$&\'()*+,;=]+$", url):
|
@@ -123,13 +133,13 @@ class CustomEndpointAPI(VendorBase):
|
|
123
133
|
)
|
124
134
|
return self.async_client
|
125
135
|
|
126
|
-
def _get_timeout(self, lm_config:
|
136
|
+
def _get_timeout(self, lm_config: dict[str, Any]) -> float:
|
127
137
|
"""Get timeout with per-call override support."""
|
128
138
|
return lm_config.get(
|
129
139
|
"timeout", float(os.environ.get("CUSTOM_ENDPOINT_REQUEST_TIMEOUT", "30"))
|
130
140
|
)
|
131
141
|
|
132
|
-
def _get_temperature_override(self) ->
|
142
|
+
def _get_temperature_override(self) -> float | None:
|
133
143
|
"""Get temperature override from environment for this specific endpoint."""
|
134
144
|
# Create a safe env var key from the endpoint URL
|
135
145
|
# e.g., "example.com/api" -> "CUSTOM_ENDPOINT_TEMP_EXAMPLE_COM_API"
|
@@ -140,7 +150,7 @@ class CustomEndpointAPI(VendorBase):
|
|
140
150
|
temp_str = os.environ.get(env_key)
|
141
151
|
return float(temp_str) if temp_str else None
|
142
152
|
|
143
|
-
def _compress_tool_schema(self, schema:
|
153
|
+
def _compress_tool_schema(self, schema: dict[str, Any]) -> dict[str, Any]:
|
144
154
|
"""Compress JSON schema to reduce token usage."""
|
145
155
|
if isinstance(schema, dict):
|
146
156
|
# Remove verbose keys
|
@@ -157,7 +167,7 @@ class CustomEndpointAPI(VendorBase):
|
|
157
167
|
return [self._compress_tool_schema(item) for item in schema]
|
158
168
|
return schema
|
159
169
|
|
160
|
-
def _inject_tools_into_prompt(self, system_message: str, tools:
|
170
|
+
def _inject_tools_into_prompt(self, system_message: str, tools: list[BaseTool]) -> str:
|
161
171
|
"""Inject tool definitions with compressed schemas and clear output format."""
|
162
172
|
if not tools:
|
163
173
|
return system_message
|
@@ -185,8 +195,8 @@ IMPORTANT: To use a tool, respond with JSON wrapped in ```json fences:
|
|
185
195
|
For regular responses, just respond normally without JSON fences."""
|
186
196
|
|
187
197
|
def _extract_tool_calls(
|
188
|
-
self, content: str, tools:
|
189
|
-
) -> tuple[
|
198
|
+
self, content: str, tools: list[BaseTool]
|
199
|
+
) -> tuple[list | None, str]:
|
190
200
|
"""Extract and validate tool calls from response."""
|
191
201
|
# Look for JSON fenced blocks
|
192
202
|
json_pattern = r"```json\s*(\{.*?\})\s*```"
|
@@ -242,11 +252,11 @@ For regular responses, just respond normally without JSON fences."""
|
|
242
252
|
async def _hit_api_async(
|
243
253
|
self,
|
244
254
|
model: str,
|
245
|
-
messages:
|
246
|
-
lm_config:
|
255
|
+
messages: list[dict[str, Any]],
|
256
|
+
lm_config: dict[str, Any],
|
247
257
|
use_ephemeral_cache_only: bool = False,
|
248
258
|
reasoning_effort: str = "low",
|
249
|
-
tools:
|
259
|
+
tools: list[BaseTool] | None = None,
|
250
260
|
) -> BaseLMResponse:
|
251
261
|
"""Async API call with comprehensive error handling and streaming support."""
|
252
262
|
|
@@ -314,7 +324,7 @@ For regular responses, just respond normally without JSON fences."""
|
|
314
324
|
|
315
325
|
return lm_response
|
316
326
|
|
317
|
-
except (httpx.RequestError, httpx.TimeoutException)
|
327
|
+
except (httpx.RequestError, httpx.TimeoutException):
|
318
328
|
if attempt == 2: # Last attempt
|
319
329
|
raise
|
320
330
|
await asyncio.sleep(self._exponential_backoff_with_jitter(attempt))
|
@@ -322,11 +332,11 @@ For regular responses, just respond normally without JSON fences."""
|
|
322
332
|
def _hit_api_sync(
|
323
333
|
self,
|
324
334
|
model: str,
|
325
|
-
messages:
|
326
|
-
lm_config:
|
335
|
+
messages: list[dict[str, Any]],
|
336
|
+
lm_config: dict[str, Any],
|
327
337
|
use_ephemeral_cache_only: bool = False,
|
328
338
|
reasoning_effort: str = "low",
|
329
|
-
tools:
|
339
|
+
tools: list[BaseTool] | None = None,
|
330
340
|
) -> BaseLMResponse:
|
331
341
|
"""Sync version with same logic as async."""
|
332
342
|
|
@@ -393,7 +403,7 @@ For regular responses, just respond normally without JSON fences."""
|
|
393
403
|
|
394
404
|
return lm_response
|
395
405
|
|
396
|
-
except (requests.RequestException, requests.Timeout)
|
406
|
+
except (requests.RequestException, requests.Timeout):
|
397
407
|
if attempt == 2: # Last attempt
|
398
408
|
raise
|
399
409
|
time.sleep(self._exponential_backoff_with_jitter(attempt))
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import os
|
2
|
-
from typing import Any
|
2
|
+
from typing import Any
|
3
3
|
|
4
4
|
from openai import AsyncOpenAI, OpenAI
|
5
5
|
|
@@ -19,18 +19,18 @@ class DeepSeekAPI(OpenAIStandard):
|
|
19
19
|
base_url="https://api.deepseek.com",
|
20
20
|
)
|
21
21
|
|
22
|
-
def _convert_tools_to_openai_format(self, tools:
|
22
|
+
def _convert_tools_to_openai_format(self, tools: list[BaseTool]) -> list[dict]:
|
23
23
|
return [tool.to_openai_tool() for tool in tools]
|
24
24
|
|
25
25
|
async def _private_request_async(
|
26
26
|
self,
|
27
|
-
messages:
|
27
|
+
messages: list[dict],
|
28
28
|
temperature: float = 0,
|
29
29
|
model_name: str = "deepseek-chat",
|
30
30
|
reasoning_effort: str = "high",
|
31
|
-
tools:
|
32
|
-
lm_config:
|
33
|
-
) ->
|
31
|
+
tools: list[BaseTool] | None = None,
|
32
|
+
lm_config: dict[str, Any] | None = None,
|
33
|
+
) -> tuple[str, list[dict] | None]:
|
34
34
|
request_params = {
|
35
35
|
"model": model_name,
|
36
36
|
"messages": messages,
|
@@ -47,13 +47,13 @@ class DeepSeekAPI(OpenAIStandard):
|
|
47
47
|
|
48
48
|
def _private_request_sync(
|
49
49
|
self,
|
50
|
-
messages:
|
50
|
+
messages: list[dict],
|
51
51
|
temperature: float = 0,
|
52
52
|
model_name: str = "deepseek-chat",
|
53
53
|
reasoning_effort: str = "high",
|
54
|
-
tools:
|
55
|
-
lm_config:
|
56
|
-
) ->
|
54
|
+
tools: list[BaseTool] | None = None,
|
55
|
+
lm_config: dict[str, Any] | None = None,
|
56
|
+
) -> tuple[str, list[dict] | None]:
|
57
57
|
request_params = {
|
58
58
|
"model": model_name,
|
59
59
|
"messages": messages,
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import os
|
2
|
-
from typing import Any
|
2
|
+
from typing import Any
|
3
3
|
|
4
4
|
from openai import AsyncOpenAI, OpenAI
|
5
5
|
|
@@ -19,7 +19,7 @@ class GrokAPI(OpenAIStandard):
|
|
19
19
|
def __init__(
|
20
20
|
self,
|
21
21
|
*,
|
22
|
-
api_key:
|
22
|
+
api_key: str | None = None,
|
23
23
|
base_url: str = "https://api.x.ai/v1",
|
24
24
|
) -> None:
|
25
25
|
api_key = api_key or os.getenv("XAI_API_KEY")
|
@@ -35,11 +35,11 @@ class GrokAPI(OpenAIStandard):
|
|
35
35
|
async def _hit_api_async(
|
36
36
|
self,
|
37
37
|
model: str,
|
38
|
-
messages:
|
39
|
-
lm_config:
|
38
|
+
messages: list[dict[str, Any]],
|
39
|
+
lm_config: dict[str, Any],
|
40
40
|
use_ephemeral_cache_only: bool = False,
|
41
41
|
reasoning_effort: str = "high",
|
42
|
-
tools:
|
42
|
+
tools: list[BaseTool] | None = None,
|
43
43
|
):
|
44
44
|
if not model:
|
45
45
|
raise ValueError("Model name is required for Grok API calls")
|
@@ -56,11 +56,11 @@ class GrokAPI(OpenAIStandard):
|
|
56
56
|
def _hit_api_sync(
|
57
57
|
self,
|
58
58
|
model: str,
|
59
|
-
messages:
|
60
|
-
lm_config:
|
59
|
+
messages: list[dict[str, Any]],
|
60
|
+
lm_config: dict[str, Any],
|
61
61
|
use_ephemeral_cache_only: bool = False,
|
62
62
|
reasoning_effort: str = "high",
|
63
|
-
tools:
|
63
|
+
tools: list[BaseTool] | None = None,
|
64
64
|
):
|
65
65
|
if not model:
|
66
66
|
raise ValueError("Model name is required for Grok API calls")
|
@@ -1,9 +1,11 @@
|
|
1
1
|
import os
|
2
|
-
from typing import Any
|
2
|
+
from typing import Any
|
3
|
+
|
3
4
|
from openai import AsyncOpenAI, OpenAI
|
4
|
-
|
5
|
-
from synth_ai.lm.vendors.base import BaseLMResponse
|
5
|
+
|
6
6
|
from synth_ai.lm.tools.base import BaseTool
|
7
|
+
from synth_ai.lm.vendors.base import BaseLMResponse
|
8
|
+
from synth_ai.lm.vendors.openai_standard import OpenAIStandard
|
7
9
|
|
8
10
|
|
9
11
|
class OpenRouterAPI(OpenAIStandard):
|
@@ -44,11 +46,11 @@ class OpenRouterAPI(OpenAIStandard):
|
|
44
46
|
async def _hit_api_async(
|
45
47
|
self,
|
46
48
|
model: str,
|
47
|
-
messages:
|
48
|
-
lm_config:
|
49
|
+
messages: list[dict[str, Any]],
|
50
|
+
lm_config: dict[str, Any],
|
49
51
|
use_ephemeral_cache_only: bool = False,
|
50
52
|
reasoning_effort: str = "high",
|
51
|
-
tools:
|
53
|
+
tools: list[BaseTool] | None = None,
|
52
54
|
) -> BaseLMResponse:
|
53
55
|
# Strip the 'openrouter/' prefix before calling the API
|
54
56
|
model = self._strip_prefix(model)
|
@@ -59,11 +61,11 @@ class OpenRouterAPI(OpenAIStandard):
|
|
59
61
|
def _hit_api_sync(
|
60
62
|
self,
|
61
63
|
model: str,
|
62
|
-
messages:
|
63
|
-
lm_config:
|
64
|
+
messages: list[dict[str, Any]],
|
65
|
+
lm_config: dict[str, Any],
|
64
66
|
use_ephemeral_cache_only: bool = False,
|
65
67
|
reasoning_effort: str = "high",
|
66
|
-
tools:
|
68
|
+
tools: list[BaseTool] | None = None,
|
67
69
|
) -> BaseLMResponse:
|
68
70
|
# Strip the 'openrouter/' prefix before calling the API
|
69
71
|
model = self._strip_prefix(model)
|