synth-ai 0.2.4.dev7__py3-none-any.whl → 0.2.4.dev9__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.

Potentially problematic release.


This version of synth-ai might be problematic. Click here for more details.

Files changed (154) hide show
  1. synth_ai/__init__.py +1 -1
  2. synth_ai/cli/__init__.py +6 -0
  3. synth_ai/cli/balance.py +3 -15
  4. synth_ai/cli/demo.py +68 -9
  5. synth_ai/cli/rl_demo.py +137 -0
  6. synth_ai/cli/root.py +65 -0
  7. synth_ai/config/base_url.py +47 -0
  8. synth_ai/demos/core/__init__.py +1 -0
  9. synth_ai/demos/core/cli.py +621 -0
  10. synth_ai/demos/demo_task_apps/__init__.py +1 -0
  11. synth_ai/demos/demo_task_apps/core.py +374 -0
  12. synth_ai/demos/demo_task_apps/math/__init__.py +1 -0
  13. synth_ai/demos/demo_task_apps/math/app.py +37 -0
  14. synth_ai/demos/demo_task_apps/math/config.toml +44 -0
  15. synth_ai/demos/demo_task_apps/math/deploy_modal.py +60 -0
  16. synth_ai/demos/demo_task_apps/math/deploy_task_app.sh +22 -0
  17. synth_ai/environments/examples/bandit/__init__.py +33 -0
  18. synth_ai/environments/examples/bandit/engine.py +294 -0
  19. synth_ai/environments/examples/bandit/environment.py +194 -0
  20. synth_ai/environments/examples/bandit/taskset.py +200 -0
  21. synth_ai/environments/examples/crafter_classic/agent_demos/analyze_semantic_words_markdown.py +250 -0
  22. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_comprehensive_evaluation.py +59 -0
  23. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_browser.py +152 -0
  24. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_config.toml +24 -0
  25. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_framework.py +1194 -0
  26. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/crafter_synth_config.toml +56 -0
  27. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_config_modal.toml +32 -0
  28. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_traces_sft_turso.py +724 -0
  29. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/kick_off_ft_modal.py +384 -0
  30. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_action_results.py +53 -0
  31. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_agent_actions.py +178 -0
  32. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_latest_run.py +222 -0
  33. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_lm_traces.py +183 -0
  34. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_no_rewards.py +210 -0
  35. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_trace_issue.py +206 -0
  36. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/check_db_schema.py +49 -0
  37. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/check_latest_results.py +64 -0
  38. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/debug_agent_responses.py +88 -0
  39. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/quick_trace_check.py +77 -0
  40. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/compare_experiments.py +324 -0
  41. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/filter_traces_sft_turso.py +580 -0
  42. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/kick_off_ft_oai.py +362 -0
  43. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/multi_model_config.toml +49 -0
  44. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_enhanced_hooks.py +332 -0
  45. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_hook_events.py +97 -0
  46. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_hook_results.py +217 -0
  47. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/check_hook_storage.py +87 -0
  48. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/check_seeds.py +88 -0
  49. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/compare_seed_performance.py +195 -0
  50. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/custom_eval_pipelines.py +400 -0
  51. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/plot_hook_frequency.py +195 -0
  52. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/seed_analysis_summary.py +56 -0
  53. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/run_rollouts_for_models_and_compare_v3.py +858 -0
  54. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_quick_evaluation.py +52 -0
  55. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_react_agent.py +874 -0
  56. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_trace_evaluation.py +1412 -0
  57. synth_ai/environments/examples/crafter_classic/agent_demos/example_v3_usage.py +216 -0
  58. synth_ai/environments/examples/crafter_classic/agent_demos/old/compare_traces.py +296 -0
  59. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_comprehensive_evaluation.py +58 -0
  60. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_env_serialization.py +464 -0
  61. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_evaluation_browser.py +152 -0
  62. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_quick_evaluation.py +51 -0
  63. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_trace_evaluation.py +1412 -0
  64. synth_ai/environments/examples/crafter_classic/agent_demos/old/debug_player_loss.py +112 -0
  65. synth_ai/environments/examples/crafter_classic/agent_demos/old/diagnose_service.py +203 -0
  66. synth_ai/environments/examples/crafter_classic/agent_demos/old/diagnose_slowness.py +305 -0
  67. synth_ai/environments/examples/crafter_classic/agent_demos/old/eval_by_difficulty.py +126 -0
  68. synth_ai/environments/examples/crafter_classic/agent_demos/old/eval_example.py +94 -0
  69. synth_ai/environments/examples/crafter_classic/agent_demos/old/explore_saved_states.py +142 -0
  70. synth_ai/environments/examples/crafter_classic/agent_demos/old/filter_traces_sft.py +26 -0
  71. synth_ai/environments/examples/crafter_classic/agent_demos/old/filter_traces_sft_OLD.py +984 -0
  72. synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_data_gemini.py +724 -0
  73. synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_data_modal.py +386 -0
  74. synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_metadata.py +205 -0
  75. synth_ai/environments/examples/crafter_classic/agent_demos/old/kick_off_ft_gemini.py +150 -0
  76. synth_ai/environments/examples/crafter_classic/agent_demos/old/kick_off_ft_modal.py +283 -0
  77. synth_ai/environments/examples/crafter_classic/agent_demos/old/prepare_vertex_ft.py +280 -0
  78. synth_ai/environments/examples/crafter_classic/agent_demos/old/profile_env_slowness.py +456 -0
  79. synth_ai/environments/examples/crafter_classic/agent_demos/old/replicate_issue.py +166 -0
  80. synth_ai/environments/examples/crafter_classic/agent_demos/old/run_and_eval.py +102 -0
  81. synth_ai/environments/examples/crafter_classic/agent_demos/old/run_comparison.py +128 -0
  82. synth_ai/environments/examples/crafter_classic/agent_demos/old/run_qwen_rollouts.py +655 -0
  83. synth_ai/environments/examples/crafter_classic/agent_demos/old/trace_eval_OLD.py +202 -0
  84. synth_ai/environments/examples/crafter_classic/agent_demos/old/validate_openai_format.py +166 -0
  85. synth_ai/environments/examples/crafter_classic/environment.py +41 -2
  86. synth_ai/environments/examples/crafter_custom/agent_demos/__init__.py +1 -0
  87. synth_ai/environments/examples/crafter_custom/agent_demos/trace_eval.py +202 -0
  88. synth_ai/environments/examples/crafter_custom/old/analyze_diamond_issue.py +159 -0
  89. synth_ai/environments/examples/crafter_custom/old/analyze_diamond_spawning.py +158 -0
  90. synth_ai/environments/examples/crafter_custom/old/compare_worlds.py +71 -0
  91. synth_ai/environments/examples/crafter_custom/old/dataset_stats.py +105 -0
  92. synth_ai/environments/examples/crafter_custom/old/diamond_spawning_summary.py +119 -0
  93. synth_ai/environments/examples/crafter_custom/old/example_dataset_usage.py +52 -0
  94. synth_ai/environments/examples/enron/units/keyword_stats.py +112 -0
  95. synth_ai/environments/examples/minigrid/agent_demos/minigrid_evaluation_framework.py +1188 -0
  96. synth_ai/environments/examples/minigrid/agent_demos/minigrid_quick_evaluation.py +48 -0
  97. synth_ai/environments/examples/minigrid/agent_demos/minigrid_react_agent.py +562 -0
  98. synth_ai/environments/examples/minigrid/agent_demos/minigrid_trace_evaluation.py +221 -0
  99. synth_ai/environments/examples/nethack/agent_demos/nethack_evaluation_framework.py +981 -0
  100. synth_ai/environments/examples/nethack/agent_demos/nethack_quick_evaluation.py +74 -0
  101. synth_ai/environments/examples/nethack/agent_demos/nethack_react_agent.py +831 -0
  102. synth_ai/environments/examples/red/agent_demos/__init__.py +1 -0
  103. synth_ai/environments/examples/red/units/__init__.py +1 -0
  104. synth_ai/environments/examples/sokoban/agent_demos/sokoban_full_eval.py +899 -0
  105. synth_ai/environments/examples/sokoban/units/astar_common.py +95 -0
  106. synth_ai/environments/service/app.py +8 -0
  107. synth_ai/http.py +102 -0
  108. synth_ai/inference/__init__.py +7 -0
  109. synth_ai/inference/client.py +20 -0
  110. synth_ai/install_sqld.sh +40 -0
  111. synth_ai/jobs/client.py +246 -0
  112. synth_ai/learning/__init__.py +24 -0
  113. synth_ai/learning/client.py +149 -0
  114. synth_ai/learning/config.py +43 -0
  115. synth_ai/learning/constants.py +29 -0
  116. synth_ai/learning/ft_client.py +59 -0
  117. synth_ai/learning/health.py +43 -0
  118. synth_ai/learning/jobs.py +205 -0
  119. synth_ai/learning/rl_client.py +256 -0
  120. synth_ai/learning/sse.py +58 -0
  121. synth_ai/learning/validators.py +48 -0
  122. synth_ai/lm/core/main_v3.py +13 -0
  123. synth_ai/lm/core/synth_models.py +48 -0
  124. synth_ai/lm/core/vendor_clients.py +9 -6
  125. synth_ai/lm/vendors/core/openai_api.py +31 -3
  126. synth_ai/lm/vendors/openai_standard.py +45 -14
  127. synth_ai/lm/vendors/supported/custom_endpoint.py +12 -2
  128. synth_ai/lm/vendors/synth_client.py +372 -28
  129. synth_ai/rl/__init__.py +30 -0
  130. synth_ai/rl/contracts.py +32 -0
  131. synth_ai/rl/env_keys.py +137 -0
  132. synth_ai/rl/secrets.py +19 -0
  133. synth_ai/scripts/verify_rewards.py +100 -0
  134. synth_ai/task/__init__.py +10 -0
  135. synth_ai/task/contracts.py +120 -0
  136. synth_ai/task/health.py +28 -0
  137. synth_ai/task/validators.py +12 -0
  138. synth_ai/tracing_v3/hooks.py +3 -1
  139. synth_ai/tracing_v3/session_tracer.py +123 -2
  140. synth_ai/tracing_v3/turso/manager.py +218 -0
  141. synth_ai/tracing_v3/turso/models.py +53 -0
  142. synth_ai-0.2.4.dev9.dist-info/METADATA +91 -0
  143. {synth_ai-0.2.4.dev7.dist-info → synth_ai-0.2.4.dev9.dist-info}/RECORD +147 -30
  144. {synth_ai-0.2.4.dev7.dist-info → synth_ai-0.2.4.dev9.dist-info}/entry_points.txt +1 -0
  145. synth_ai/tui/__init__.py +0 -1
  146. synth_ai/tui/__main__.py +0 -13
  147. synth_ai/tui/cli/__init__.py +0 -1
  148. synth_ai/tui/cli/query_experiments.py +0 -164
  149. synth_ai/tui/cli/query_experiments_v3.py +0 -164
  150. synth_ai/tui/dashboard.py +0 -340
  151. synth_ai-0.2.4.dev7.dist-info/METADATA +0 -193
  152. {synth_ai-0.2.4.dev7.dist-info → synth_ai-0.2.4.dev9.dist-info}/WHEEL +0 -0
  153. {synth_ai-0.2.4.dev7.dist-info → synth_ai-0.2.4.dev9.dist-info}/licenses/LICENSE +0 -0
  154. {synth_ai-0.2.4.dev7.dist-info → synth_ai-0.2.4.dev9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,95 @@
1
+ """
2
+ astar_common.py – one A* routine usable by both engine-level and
3
+ environment-level unit tests.
4
+ """
5
+
6
+ import heapq
7
+ import itertools
8
+ import json
9
+ from typing import Any, Awaitable, Callable, List, Tuple
10
+
11
+ import numpy as np
12
+
13
+
14
+ # ---------- generic utilities ------------------------------------ #
15
+ def _boxes_left(env_pkg) -> int:
16
+ """#targets – #boxes-on-targets (uses raw grids, never the counter)."""
17
+ return int(np.sum(env_pkg.room_fixed == 2) - np.sum(env_pkg.room_state == 3))
18
+
19
+
20
+ def solved(obj: Any) -> bool:
21
+ """Expects obj to have a .package_sokoban_env attribute."""
22
+ return _boxes_left(obj.package_sokoban_env) == 0
23
+
24
+
25
+ def heuristic(obj: Any) -> int:
26
+ """Expects obj to have a .package_sokoban_env attribute."""
27
+ return _boxes_left(obj.package_sokoban_env)
28
+
29
+
30
+ # ---------- single reusable A* ----------------------------------- #
31
+ async def astar(
32
+ root_obj: Any,
33
+ step_fn: Callable[[Any, int], Awaitable[None]],
34
+ deserialize_fn: Callable[[Any], Awaitable[Any]],
35
+ max_nodes: int = 1000,
36
+ ) -> List[int]:
37
+ """
38
+ Generic A* over Sokoban snapshots.
39
+
40
+ • `root_obj` - current engine *or* environment
41
+ • `step_fn(obj, action)` - async: apply one move to *obj*
42
+ • `deserialize_fn(snapshot)` - async: new obj from snapshot
43
+ """
44
+ start_snap = await root_obj._serialize_engine()
45
+
46
+ frontier: List[Tuple[int, int, Any, List[int]]] = []
47
+ counter = itertools.count()
48
+ frontier.append((heuristic(root_obj), next(counter), start_snap, []))
49
+ seen: set[str] = set()
50
+
51
+ nodes = 0
52
+ while frontier and nodes < max_nodes:
53
+ f, _, snap, path = heapq.heappop(frontier)
54
+ cur = await deserialize_fn(snap)
55
+ key = json.dumps(snap.engine_snapshot, sort_keys=True)
56
+ if key in seen:
57
+ continue
58
+ seen.add(key)
59
+ if solved(cur):
60
+ return path
61
+
62
+ nodes += 1
63
+ for action in range(cur.package_sokoban_env.action_space.n):
64
+ child = await deserialize_fn(snap) # fresh copy
65
+ try:
66
+ await step_fn(child, action)
67
+ except Exception: # illegal/off-board
68
+ continue
69
+
70
+ child_snap = await child._serialize_engine()
71
+ g = len(path) + 1
72
+ heapq.heappush(
73
+ frontier,
74
+ (g + heuristic(child), next(counter), child_snap, path + [action]),
75
+ )
76
+ return []
77
+
78
+
79
+ # convenience lambdas for the two concrete APIs
80
+ async def _engine_step(e, a): # `SokobanEngine`
81
+ await e._step_engine(a)
82
+
83
+
84
+ async def _env_step(env, a): # `SokobanEnvironment` (expects Move wrapper)
85
+ from synth_ai.environments.examples.sokoban.units.test_sokoban_environment import Move
86
+
87
+ await env.step([[Move(a)]])
88
+
89
+
90
+ ENGINE_ASTAR = lambda eng, **kw: astar(eng, _engine_step, eng.__class__._deserialize_engine, **kw)
91
+ ENV_ASTAR = lambda env, **kw: astar(
92
+ env.engine, _env_step, env.engine.__class__._deserialize_engine, **kw
93
+ )
94
+
95
+ # ----------------------------------------------------------------- #
@@ -47,6 +47,14 @@ except Exception as _e:
47
47
  # Keep service robust even if example env import fails
48
48
  logging.getLogger(__name__).warning(f"Wordle env not registered: {_e}")
49
49
 
50
+ # Register Bandit example environment
51
+ try:
52
+ import synth_ai.environments.examples.bandit.environment as bandit_mod
53
+
54
+ register_environment("Bandit", bandit_mod.BanditEnvironment)
55
+ except Exception as _e:
56
+ logging.getLogger(__name__).warning(f"Bandit env not registered: {_e}")
57
+
50
58
  app = FastAPI(title="Environment Service")
51
59
 
52
60
 
synth_ai/http.py ADDED
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from dataclasses import dataclass
5
+ from typing import Any, Dict, Optional
6
+
7
+ import aiohttp
8
+
9
+
10
+ @dataclass
11
+ class HTTPError(Exception):
12
+ status: int
13
+ url: str
14
+ message: str
15
+ body_snippet: str | None = None
16
+ detail: Any | None = None
17
+
18
+ def __str__(self) -> str: # pragma: no cover - trivial
19
+ base = f"HTTP {self.status} for {self.url}: {self.message}"
20
+ if self.body_snippet:
21
+ base += f" | body[0:200]={self.body_snippet[:200]}"
22
+ return base
23
+
24
+
25
+ class AsyncHttpClient:
26
+ def __init__(self, base_url: str, api_key: str, timeout: float = 30.0) -> None:
27
+ self._base_url = base_url.rstrip("/")
28
+ self._api_key = api_key
29
+ self._timeout = aiohttp.ClientTimeout(total=timeout)
30
+ self._session: Optional[aiohttp.ClientSession] = None
31
+
32
+ async def __aenter__(self) -> "AsyncHttpClient":
33
+ if self._session is None:
34
+ headers = {"authorization": f"Bearer {self._api_key}"}
35
+ self._session = aiohttp.ClientSession(headers=headers, timeout=self._timeout)
36
+ return self
37
+
38
+ async def __aexit__(self, exc_type, exc, tb) -> None: # noqa: ANN001
39
+ if self._session is not None:
40
+ await self._session.close()
41
+ self._session = None
42
+
43
+ def _abs(self, path: str) -> str:
44
+ if path.startswith("http://") or path.startswith("https://"):
45
+ return path
46
+ # If base_url already ends with /api and path starts with /api, remove duplicate
47
+ if self._base_url.endswith("/api") and path.startswith("/api"):
48
+ path = path[4:] # Remove leading /api
49
+ return f"{self._base_url}/{path.lstrip('/')}"
50
+
51
+ async def get(self, path: str, *, params: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None) -> Any:
52
+ url = self._abs(path)
53
+ assert self._session is not None, "AsyncHttpClient must be used as an async context manager"
54
+ async with self._session.get(url, params=params, headers=headers) as resp:
55
+ return await self._handle_response(resp, url)
56
+
57
+ async def post_json(self, path: str, *, json: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> Any:
58
+ url = self._abs(path)
59
+ assert self._session is not None, "AsyncHttpClient must be used as an async context manager"
60
+ async with self._session.post(url, json=json, headers=headers) as resp:
61
+ return await self._handle_response(resp, url)
62
+
63
+ async def post_multipart(self, path: str, *, data: Dict[str, Any], files: Dict[str, tuple[str, bytes, str | None]], headers: Optional[Dict[str, str]] = None) -> Any:
64
+ url = self._abs(path)
65
+ assert self._session is not None, "AsyncHttpClient must be used as an async context manager"
66
+ form = aiohttp.FormData()
67
+ for k, v in data.items():
68
+ form.add_field(k, str(v))
69
+ for field, (filename, content, content_type) in files.items():
70
+ form.add_field(field, content, filename=filename, content_type=content_type or "application/octet-stream")
71
+ async with self._session.post(url, data=form, headers=headers) as resp:
72
+ return await self._handle_response(resp, url)
73
+
74
+ async def delete(self, path: str, *, headers: Optional[Dict[str, str]] = None) -> Any:
75
+ url = self._abs(path)
76
+ assert self._session is not None, "AsyncHttpClient must be used as an async context manager"
77
+ async with self._session.delete(url, headers=headers) as resp:
78
+ return await self._handle_response(resp, url)
79
+
80
+ async def _handle_response(self, resp: aiohttp.ClientResponse, url: str) -> Any:
81
+ text = await resp.text()
82
+ body_snippet = text[:200] if text else None
83
+ if 200 <= resp.status < 300:
84
+ ctype = resp.headers.get("content-type", "")
85
+ if "application/json" in ctype:
86
+ try:
87
+ return await resp.json()
88
+ except Exception:
89
+ # Fallback to text
90
+ return text
91
+ return text
92
+ # error
93
+ detail: Any | None = None
94
+ try:
95
+ detail = await resp.json()
96
+ except Exception:
97
+ detail = None
98
+ raise HTTPError(status=resp.status, url=url, message="request_failed", body_snippet=body_snippet, detail=detail)
99
+
100
+
101
+ async def sleep(seconds: float) -> None:
102
+ await asyncio.sleep(seconds)
@@ -0,0 +1,7 @@
1
+ from .client import InferenceClient
2
+
3
+ __all__ = [
4
+ "InferenceClient",
5
+ ]
6
+
7
+
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict
4
+
5
+ from ..http import AsyncHttpClient
6
+
7
+
8
+ class InferenceClient:
9
+ def __init__(self, base_url: str, api_key: str, *, timeout: float = 30.0) -> None:
10
+ self._base_url = base_url.rstrip("/")
11
+ self._api_key = api_key
12
+ self._timeout = timeout
13
+
14
+ async def create_chat_completion(self, *, model: str, messages: list[dict], **kwargs: Any) -> Dict[str, Any]:
15
+ body: Dict[str, Any] = {"model": model, "messages": messages}
16
+ body.update(kwargs)
17
+ async with AsyncHttpClient(self._base_url, self._api_key, timeout=self._timeout) as http:
18
+ return await http.post_json("/v1/chat/completions", json=body)
19
+
20
+
@@ -0,0 +1,40 @@
1
+ #!/bin/bash
2
+ # Install sqld binary for Synth AI
3
+
4
+ set -e
5
+
6
+ SQLD_VERSION="v0.26.2"
7
+ OS=$(uname -s | tr '[:upper:]' '[:lower:]')
8
+ ARCH=$(uname -m)
9
+
10
+ # Map architecture names
11
+ case "$ARCH" in
12
+ x86_64) ARCH="x86_64" ;;
13
+ aarch64|arm64) ARCH="aarch64" ;;
14
+ *) echo "Unsupported architecture: $ARCH"; exit 1 ;;
15
+ esac
16
+
17
+ # Construct download URL
18
+ URL="https://github.com/tursodatabase/libsql/releases/download/libsql-server-${SQLD_VERSION}/sqld-${OS}-${ARCH}.tar.xz"
19
+
20
+ echo "📥 Downloading sqld ${SQLD_VERSION} for ${OS}-${ARCH}..."
21
+
22
+ # Download and extract
23
+ TMP_DIR=$(mktemp -d)
24
+ cd "$TMP_DIR"
25
+ curl -L -o sqld.tar.xz "$URL"
26
+ tar -xf sqld.tar.xz
27
+
28
+ # Install to user's local bin
29
+ mkdir -p ~/.local/bin
30
+ mv sqld ~/.local/bin/
31
+ chmod +x ~/.local/bin/sqld
32
+
33
+ # Clean up
34
+ cd -
35
+ rm -rf "$TMP_DIR"
36
+
37
+ echo "✅ sqld installed to ~/.local/bin/sqld"
38
+ echo ""
39
+ echo "🔧 Add ~/.local/bin to your PATH if needed:"
40
+ echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
@@ -0,0 +1,246 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from synth_ai.http import AsyncHttpClient
6
+
7
+
8
+ class FilesApi:
9
+ def __init__(self, http: AsyncHttpClient) -> None:
10
+ self._http = http
11
+
12
+ async def upload(self, *, filename: str, content: bytes, purpose: str, content_type: Optional[str] = None, idempotency_key: Optional[str] = None) -> Dict[str, Any]:
13
+ data = {"purpose": purpose}
14
+ files = {"file": (filename, content, content_type)}
15
+ headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
16
+ return await self._http.post_multipart("/api/files", data=data, files=files, headers=headers)
17
+
18
+ async def list(self, *, purpose: Optional[str] = None, after: Optional[str] = None, limit: int = 20) -> Dict[str, Any]:
19
+ params: Dict[str, Any] = {}
20
+ if purpose is not None:
21
+ params["purpose"] = purpose
22
+ if after is not None:
23
+ params["after"] = after
24
+ params["limit"] = limit
25
+ return await self._http.get("/api/files", params=params)
26
+
27
+ async def retrieve(self, file_id: str) -> Dict[str, Any]:
28
+ return await self._http.get(f"/api/files/{file_id}")
29
+
30
+ async def delete(self, file_id: str) -> Any:
31
+ return await self._http.delete(f"/api/files/{file_id}")
32
+
33
+ async def list_jobs(self, file_id: str, *, after: Optional[str] = None, limit: int = 20) -> Dict[str, Any]:
34
+ params: Dict[str, Any] = {"limit": limit}
35
+ if after is not None:
36
+ params["after"] = after
37
+ return await self._http.get(f"/api/files/{file_id}/jobs", params=params)
38
+
39
+
40
+ class SftJobsApi:
41
+ def __init__(self, http: AsyncHttpClient) -> None:
42
+ self._http = http
43
+
44
+ async def create(
45
+ self,
46
+ *,
47
+ training_file: str,
48
+ model: str,
49
+ validation_file: Optional[str] = None,
50
+ hyperparameters: Optional[Dict[str, Any]] = None,
51
+ suffix: Optional[str] = None,
52
+ integrations: Optional[Dict[str, Any]] = None,
53
+ metadata: Optional[Dict[str, Any]] = None,
54
+ idempotency_key: Optional[str] = None,
55
+ ) -> Dict[str, Any]:
56
+ payload: Dict[str, Any] = {
57
+ "training_file": training_file,
58
+ "model": model,
59
+ }
60
+ if validation_file is not None:
61
+ payload["validation_file"] = validation_file
62
+ if hyperparameters is not None:
63
+ payload["hyperparameters"] = hyperparameters
64
+ if suffix is not None:
65
+ payload["suffix"] = suffix
66
+ if integrations is not None:
67
+ payload["integrations"] = integrations
68
+ if metadata is not None:
69
+ payload["metadata"] = metadata
70
+ headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
71
+ return await self._http.post_json("/api/sft/jobs", json=payload, headers=headers)
72
+
73
+ async def list(
74
+ self,
75
+ *,
76
+ status: Optional[str] = None,
77
+ model: Optional[str] = None,
78
+ file_id: Optional[str] = None,
79
+ created_after: Optional[int] = None,
80
+ created_before: Optional[int] = None,
81
+ after: Optional[str] = None,
82
+ limit: int = 20,
83
+ ) -> Dict[str, Any]:
84
+ params: Dict[str, Any] = {"limit": limit}
85
+ if status is not None:
86
+ params["status"] = status
87
+ if model is not None:
88
+ params["model"] = model
89
+ if file_id is not None:
90
+ params["file_id"] = file_id
91
+ if created_after is not None:
92
+ params["created_after"] = created_after
93
+ if created_before is not None:
94
+ params["created_before"] = created_before
95
+ if after is not None:
96
+ params["after"] = after
97
+ return await self._http.get("/api/sft/jobs", params=params)
98
+
99
+ async def retrieve(self, job_id: str) -> Dict[str, Any]:
100
+ return await self._http.get(f"/api/sft/jobs/{job_id}")
101
+
102
+ async def cancel(self, job_id: str) -> Dict[str, Any]:
103
+ return await self._http.post_json(f"/api/sft/jobs/{job_id}/cancel", json={})
104
+
105
+ async def list_events(self, job_id: str, *, since_seq: int = 0, limit: int = 200) -> Dict[str, Any]:
106
+ params = {"since_seq": since_seq, "limit": limit}
107
+ return await self._http.get(f"/api/sft/jobs/{job_id}/events", params=params)
108
+
109
+ async def checkpoints(self, job_id: str, *, after: Optional[str] = None, limit: int = 10) -> Dict[str, Any]:
110
+ params: Dict[str, Any] = {"limit": limit}
111
+ if after is not None:
112
+ params["after"] = after
113
+ return await self._http.get(f"/api/sft/jobs/{job_id}/checkpoints", params=params)
114
+
115
+
116
+ class RlJobsApi:
117
+ def __init__(self, http: AsyncHttpClient) -> None:
118
+ self._http = http
119
+
120
+ async def create(
121
+ self,
122
+ *,
123
+ model: str,
124
+ endpoint_base_url: str,
125
+ trainer_id: str,
126
+ trainer: Optional[Dict[str, Any]] = None,
127
+ job_config_id: Optional[str] = None,
128
+ config: Optional[Dict[str, Any]] = None,
129
+ metadata: Optional[Dict[str, Any]] = None,
130
+ idempotency_key: Optional[str] = None,
131
+ ) -> Dict[str, Any]:
132
+ payload: Dict[str, Any] = {
133
+ "model": model,
134
+ "endpoint_base_url": endpoint_base_url,
135
+ "trainer_id": trainer_id,
136
+ }
137
+ if trainer is not None:
138
+ payload["trainer"] = trainer
139
+ if job_config_id is not None:
140
+ payload["job_config_id"] = job_config_id
141
+ if config is not None:
142
+ payload["config"] = config
143
+ if metadata is not None:
144
+ payload["metadata"] = metadata
145
+ headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
146
+ return await self._http.post_json("/api/rl/jobs", json=payload, headers=headers)
147
+
148
+ async def list(
149
+ self,
150
+ *,
151
+ status: Optional[str] = None,
152
+ model: Optional[str] = None,
153
+ created_after: Optional[int] = None,
154
+ created_before: Optional[int] = None,
155
+ after: Optional[str] = None,
156
+ limit: int = 20,
157
+ ) -> Dict[str, Any]:
158
+ params: Dict[str, Any] = {"limit": limit}
159
+ if status is not None:
160
+ params["status"] = status
161
+ if model is not None:
162
+ params["model"] = model
163
+ if created_after is not None:
164
+ params["created_after"] = created_after
165
+ if created_before is not None:
166
+ params["created_before"] = created_before
167
+ if after is not None:
168
+ params["after"] = after
169
+ return await self._http.get("/api/rl/jobs", params=params)
170
+
171
+ async def retrieve(self, job_id: str) -> Dict[str, Any]:
172
+ return await self._http.get(f"/api/rl/jobs/{job_id}")
173
+
174
+ async def cancel(self, job_id: str) -> Dict[str, Any]:
175
+ return await self._http.post_json(f"/api/rl/jobs/{job_id}/cancel", json={})
176
+
177
+ async def list_events(self, job_id: str, *, since_seq: int = 0, limit: int = 200) -> Dict[str, Any]:
178
+ params = {"since_seq": since_seq, "limit": limit}
179
+ return await self._http.get(f"/api/rl/jobs/{job_id}/events", params=params)
180
+
181
+ async def metrics(self, job_id: str, *, after_step: int = -1, limit: int = 200) -> Dict[str, Any]:
182
+ params = {"after_step": after_step, "limit": limit}
183
+ return await self._http.get(f"/api/rl/jobs/{job_id}/metrics", params=params)
184
+
185
+
186
+ class ModelsApi:
187
+ def __init__(self, http: AsyncHttpClient) -> None:
188
+ self._http = http
189
+
190
+ async def list(
191
+ self,
192
+ *,
193
+ source: Optional[str] = None,
194
+ base_model: Optional[str] = None,
195
+ status: Optional[str] = None,
196
+ after: Optional[str] = None,
197
+ limit: int = 20,
198
+ ) -> Dict[str, Any]:
199
+ params: Dict[str, Any] = {"limit": limit}
200
+ if source is not None:
201
+ params["source"] = source
202
+ if base_model is not None:
203
+ params["base_model"] = base_model
204
+ if status is not None:
205
+ params["status"] = status
206
+ if after is not None:
207
+ params["after"] = after
208
+ return await self._http.get("/api/models", params=params)
209
+
210
+ async def retrieve(self, model_id: str) -> Dict[str, Any]:
211
+ return await self._http.get(f"/api/models/{model_id}")
212
+
213
+ async def delete(self, model_id: str) -> Any:
214
+ return await self._http.delete(f"/api/models/{model_id}")
215
+
216
+ async def list_jobs(self, model_id: str, *, after: Optional[str] = None, limit: int = 20) -> Dict[str, Any]:
217
+ params: Dict[str, Any] = {"limit": limit}
218
+ if after is not None:
219
+ params["after"] = after
220
+ return await self._http.get(f"/api/models/{model_id}/jobs", params=params)
221
+
222
+
223
+ class JobsClient:
224
+ """High-level client aggregating job APIs.
225
+
226
+ Usage:
227
+ async with JobsClient(base_url, api_key) as c:
228
+ await c.files.list()
229
+ """
230
+
231
+ def __init__(self, base_url: str, api_key: str, timeout: float = 30.0, http: Optional[AsyncHttpClient] = None) -> None:
232
+ self._base_url = base_url
233
+ self._api_key = api_key
234
+ self._timeout = timeout
235
+ self._http = http or AsyncHttpClient(base_url, api_key, timeout=timeout)
236
+ self.files = FilesApi(self._http)
237
+ self.sft = SftJobsApi(self._http)
238
+ self.rl = RlJobsApi(self._http)
239
+ self.models = ModelsApi(self._http)
240
+
241
+ async def __aenter__(self) -> "JobsClient":
242
+ await self._http.__aenter__()
243
+ return self
244
+
245
+ async def __aexit__(self, exc_type, exc, tb) -> None: # noqa: ANN001
246
+ await self._http.__aexit__(exc_type, exc, tb)
@@ -0,0 +1,24 @@
1
+ from .client import LearningClient
2
+ from .rl_client import RlClient
3
+ from .ft_client import FtClient
4
+ from .validators import validate_training_jsonl, validate_trainer_cfg_rl
5
+ from synth_ai.task import validate_task_app_url, task_app_health
6
+ from .health import backend_health, pricing_preflight, balance_autumn_normalized
7
+ from .sse import stream_events as stream_job_events
8
+ from .jobs import JobHandle, JobsApiResolver
9
+
10
+ __all__ = [
11
+ "LearningClient",
12
+ "RlClient",
13
+ "FtClient",
14
+ "validate_training_jsonl",
15
+ "validate_trainer_cfg_rl",
16
+ "validate_task_app_url",
17
+ "backend_health",
18
+ "task_app_health",
19
+ "pricing_preflight",
20
+ "balance_autumn_normalized",
21
+ "stream_job_events",
22
+ "JobHandle",
23
+ "JobsApiResolver",
24
+ ]