synth-ai 0.1.3__py3-none-any.whl → 0.1.4__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 CHANGED
@@ -2,9 +2,7 @@
2
2
  Synth AI - Software for aiding the best and multiplying the will.
3
3
  """
4
4
 
5
- from importlib.metadata import version
6
-
7
5
  from synth_ai.zyk import LM # Assuming LM is in zyk.py in the same directory
8
6
 
9
- __version__ = version("synth-ai") # Gets version from installed package metadata
7
+ __version__ = "0.1.4"
10
8
  __all__ = ["LM"] # Explicitly define public API
@@ -8,6 +8,7 @@ from synth_ai.zyk.lms.vendors.supported.deepseek import DeepSeekAPI
8
8
  from synth_ai.zyk.lms.vendors.supported.together import TogetherAPI
9
9
  from synth_ai.zyk.lms.vendors.supported.groq import GroqAPI
10
10
  from synth_ai.zyk.lms.vendors.core.mistral_api import MistralAPI
11
+ from synth_ai.zyk.lms.vendors.supported.custom_endpoint import CustomEndpointAPI
11
12
 
12
13
 
13
14
  class OpenAIClient(OpenAIPrivate):
@@ -45,3 +46,8 @@ class GroqClient(GroqAPI):
45
46
  class MistralClient(MistralAPI):
46
47
  def __init__(self):
47
48
  super().__init__()
49
+
50
+
51
+ class CustomEndpointClient(CustomEndpointAPI):
52
+ def __init__(self, endpoint_url: str):
53
+ super().__init__(endpoint_url=endpoint_url)
@@ -5,11 +5,12 @@ from synth_ai.zyk.lms.core.all import (
5
5
  AnthropicClient,
6
6
  DeepSeekClient,
7
7
  GeminiClient,
8
- GroqAPI,
9
- MistralAPI,
8
+ GroqClient,
9
+ MistralClient,
10
10
  # OpenAIClient,
11
11
  OpenAIStructuredOutputClient,
12
12
  TogetherClient,
13
+ CustomEndpointClient,
13
14
  )
14
15
 
15
16
  openai_naming_regexes: List[Pattern] = [
@@ -53,6 +54,15 @@ mistral_naming_regexes: List[Pattern] = [
53
54
  re.compile(r"^mistral-.*$"),
54
55
  ]
55
56
 
57
+ # Custom endpoint patterns - check these before generic patterns
58
+ custom_endpoint_naming_regexes: List[Pattern] = [
59
+ # Modal endpoints: org--app.modal.run
60
+ re.compile(r"^[a-zA-Z0-9\-]+--[a-zA-Z0-9\-]+\.modal\.run$"),
61
+ # Generic domain patterns for custom endpoints
62
+ re.compile(r"^[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-]+\.[a-zA-Z]+$"), # domain.tld
63
+ re.compile(r"^[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-]+\.[a-zA-Z]+\/[a-zA-Z0-9\-\/]+$"), # domain.tld/path
64
+ ]
65
+
56
66
 
57
67
  def get_client(
58
68
  model_name: str,
@@ -79,9 +89,12 @@ def get_client(
79
89
  elif any(regex.match(model_name) for regex in deepseek_naming_regexes):
80
90
  return DeepSeekClient()
81
91
  elif any(regex.match(model_name) for regex in groq_naming_regexes):
82
- return GroqAPI()
92
+ return GroqClient()
83
93
  elif any(regex.match(model_name) for regex in mistral_naming_regexes):
84
- return MistralAPI()
94
+ return MistralClient()
95
+ elif any(regex.match(model_name) for regex in custom_endpoint_naming_regexes):
96
+ # Custom endpoints are passed as the endpoint URL
97
+ return CustomEndpointClient(endpoint_url=model_name)
85
98
  elif any(regex.match(model_name) for regex in together_naming_regexes):
86
99
  return TogetherClient()
87
100
  else:
@@ -0,0 +1,394 @@
1
+ import re
2
+ import os
3
+ import json
4
+ import asyncio
5
+ import time
6
+ from typing import Any, Dict, List, Optional, Tuple, Type
7
+ import requests
8
+ import httpx
9
+ from requests.adapters import HTTPAdapter
10
+ from urllib3.util.retry import Retry
11
+ import random
12
+ from urllib.parse import urlparse
13
+
14
+ from synth_ai.zyk.lms.vendors.base import BaseLMResponse, VendorBase
15
+ from synth_ai.zyk.lms.tools.base import BaseTool
16
+ from synth_ai.zyk.lms.caching.initialize import get_cache_handler
17
+
18
+ # Exception types for retry
19
+ CUSTOM_ENDPOINT_EXCEPTIONS_TO_RETRY: Tuple[Type[Exception], ...] = (
20
+ requests.RequestException,
21
+ requests.Timeout,
22
+ httpx.RequestError,
23
+ httpx.TimeoutException
24
+ )
25
+
26
+ class CustomEndpointAPI(VendorBase):
27
+ """Generic vendor client for custom OpenAI-compatible endpoints."""
28
+
29
+ used_for_structured_outputs: bool = False
30
+ exceptions_to_retry: List = list(CUSTOM_ENDPOINT_EXCEPTIONS_TO_RETRY)
31
+
32
+ def __init__(self, endpoint_url: str):
33
+ # Validate and sanitize URL
34
+ self._validate_endpoint_url(endpoint_url)
35
+ self.endpoint_url = endpoint_url
36
+
37
+ # Construct full chat completions URL
38
+ if endpoint_url.endswith('/'):
39
+ endpoint_url = endpoint_url[:-1]
40
+ self.chat_completions_url = f"https://{endpoint_url}/chat/completions"
41
+ self.health_url = f"https://{endpoint_url}/health"
42
+
43
+ # Setup session with connection pooling and retries
44
+ self.session = self._create_session()
45
+ self.async_client = None # Lazy init
46
+
47
+ # Get auth token from environment (generic support for any auth)
48
+ self.auth_token = os.environ.get("CUSTOM_ENDPOINT_API_TOKEN")
49
+
50
+ def _validate_endpoint_url(self, url: str) -> None:
51
+ """Validate endpoint URL format and prevent SSRF."""
52
+ # Block dangerous URL patterns
53
+ dangerous_patterns = [
54
+ "file://", "ftp://", "gopher://",
55
+ "localhost", "127.", "0.0.0.0",
56
+ "10.", "192.168.", "172.16.", "172.17.", "172.18.", "172.19.",
57
+ "172.20.", "172.21.", "172.22.", "172.23.", "172.24.", "172.25.",
58
+ "172.26.", "172.27.", "172.28.", "172.29.", "172.30.", "172.31.",
59
+ "169.254.", # link-local
60
+ "::1", "fc00:", "fd00:", "fe80:", # IPv6 private
61
+ ]
62
+
63
+ for pattern in dangerous_patterns:
64
+ if pattern in url.lower():
65
+ raise ValueError(f"Blocked URL pattern for security: {pattern}")
66
+
67
+ # Limit URL length
68
+ if len(url) > 256:
69
+ raise ValueError(f"Endpoint URL too long (max 256 chars)")
70
+
71
+ # Basic URL format check
72
+ if not re.match(r'^[a-zA-Z0-9\-._~:/?#\[\]@!$&\'()*+,;=]+$', url):
73
+ raise ValueError(f"Invalid URL format: {url}")
74
+
75
+ def _create_session(self) -> requests.Session:
76
+ """Create session with retry strategy and connection pooling."""
77
+ session = requests.Session()
78
+
79
+ # Exponential backoff with jitter
80
+ retry_strategy = Retry(
81
+ total=3,
82
+ backoff_factor=1,
83
+ status_forcelist=[429, 500, 502, 503, 504],
84
+ allowed_methods=["POST", "GET"]
85
+ )
86
+
87
+ adapter = HTTPAdapter(
88
+ max_retries=retry_strategy,
89
+ pool_connections=10,
90
+ pool_maxsize=20
91
+ )
92
+
93
+ session.mount("http://", adapter)
94
+ session.mount("https://", adapter)
95
+
96
+ return session
97
+
98
+ async def _get_async_client(self) -> httpx.AsyncClient:
99
+ """Lazy init async client with shared retry logic."""
100
+ if self.async_client is None:
101
+ self.async_client = httpx.AsyncClient(
102
+ timeout=httpx.Timeout(30.0),
103
+ limits=httpx.Limits(max_connections=10, max_keepalive_connections=5)
104
+ )
105
+ return self.async_client
106
+
107
+ def _get_timeout(self, lm_config: Dict[str, Any]) -> float:
108
+ """Get timeout with per-call override support."""
109
+ return lm_config.get("timeout",
110
+ float(os.environ.get("CUSTOM_ENDPOINT_REQUEST_TIMEOUT", "30")))
111
+
112
+ def _get_temperature_override(self) -> Optional[float]:
113
+ """Get temperature override from environment for this specific endpoint."""
114
+ # Create a safe env var key from the endpoint URL
115
+ # e.g., "example.com/api" -> "CUSTOM_ENDPOINT_TEMP_EXAMPLE_COM_API"
116
+ safe_key = re.sub(r'[^A-Za-z0-9]', '_', self.endpoint_url).upper()
117
+ safe_key = safe_key[:64] # Limit length
118
+
119
+ env_key = f"CUSTOM_ENDPOINT_TEMP_{safe_key}"
120
+ temp_str = os.environ.get(env_key)
121
+ return float(temp_str) if temp_str else None
122
+
123
+ def _compress_tool_schema(self, schema: Dict[str, Any]) -> Dict[str, Any]:
124
+ """Compress JSON schema to reduce token usage."""
125
+ if isinstance(schema, dict):
126
+ # Remove verbose keys
127
+ compressed = {
128
+ k: self._compress_tool_schema(v)
129
+ for k, v in schema.items()
130
+ if k not in ["title", "$ref", "$schema"]
131
+ }
132
+ # Shorten descriptions
133
+ if "description" in compressed and len(compressed["description"]) > 50:
134
+ compressed["description"] = compressed["description"][:47] + "..."
135
+ return compressed
136
+ elif isinstance(schema, list):
137
+ return [self._compress_tool_schema(item) for item in schema]
138
+ return schema
139
+
140
+ def _inject_tools_into_prompt(self, system_message: str, tools: List[BaseTool]) -> str:
141
+ """Inject tool definitions with compressed schemas and clear output format."""
142
+ if not tools:
143
+ return system_message
144
+
145
+ tool_descriptions = []
146
+ for tool in tools:
147
+ schema = tool.arguments.model_json_schema()
148
+ compressed_schema = self._compress_tool_schema(schema)
149
+
150
+ tool_desc = f"Tool: {tool.name}\nDesc: {tool.description}\nParams: {json.dumps(compressed_schema, separators=(',', ':'))}"
151
+ tool_descriptions.append(tool_desc)
152
+
153
+ tools_text = "\n".join(tool_descriptions)
154
+
155
+ return f"""{system_message}
156
+
157
+ Available tools:
158
+ {tools_text}
159
+
160
+ IMPORTANT: To use a tool, respond with JSON wrapped in ```json fences:
161
+ ```json
162
+ {{"tool_call": {{"name": "tool_name", "arguments": {{...}}}}}}
163
+ ```
164
+
165
+ For regular responses, just respond normally without JSON fences."""
166
+
167
+ def _extract_tool_calls(self, content: str, tools: List[BaseTool]) -> tuple[Optional[List], str]:
168
+ """Extract and validate tool calls from response."""
169
+ # Look for JSON fenced blocks
170
+ json_pattern = r'```json\s*(\{.*?\})\s*```'
171
+ matches = re.findall(json_pattern, content, re.DOTALL)
172
+
173
+ if not matches:
174
+ return None, content
175
+
176
+ tool_calls = []
177
+ cleaned_content = content
178
+
179
+ for match in matches:
180
+ try:
181
+ tool_data = json.loads(match)
182
+ if "tool_call" in tool_data:
183
+ call_data = tool_data["tool_call"]
184
+ tool_name = call_data.get("name")
185
+
186
+ # Validate against available tools
187
+ matching_tool = next((t for t in tools if t.name == tool_name), None)
188
+ if matching_tool:
189
+ # Validate arguments with pydantic
190
+ validated_args = matching_tool.arguments(**call_data.get("arguments", {}))
191
+ tool_calls.append({
192
+ "name": tool_name,
193
+ "arguments": validated_args.model_dump()
194
+ })
195
+
196
+ # Remove tool call from content
197
+ cleaned_content = cleaned_content.replace(f"```json\n{match}\n```", "").strip()
198
+
199
+ except (json.JSONDecodeError, Exception):
200
+ # Fall back to treating as normal text if validation fails
201
+ continue
202
+
203
+ return tool_calls if tool_calls else None, cleaned_content
204
+
205
+ def _exponential_backoff_with_jitter(self, attempt: int) -> float:
206
+ """Calculate backoff time with jitter to prevent thundering herd."""
207
+ base_delay = min(2 ** attempt, 32) # Cap at 32 seconds
208
+ jitter = random.uniform(0, 1)
209
+ return base_delay + jitter
210
+
211
+ def _handle_rate_limit(self, response: requests.Response) -> None:
212
+ """Extract and propagate rate limit information."""
213
+ if response.status_code == 429:
214
+ retry_after = response.headers.get("Retry-After")
215
+ if retry_after:
216
+ # Bubble up to synth-ai scheduler
217
+ raise requests.exceptions.RetryError(f"Rate limited. Retry after {retry_after}s")
218
+
219
+ async def _hit_api_async(
220
+ self,
221
+ model: str,
222
+ messages: List[Dict[str, Any]],
223
+ lm_config: Dict[str, Any],
224
+ use_ephemeral_cache_only: bool = False,
225
+ reasoning_effort: str = "low",
226
+ tools: Optional[List[BaseTool]] = None,
227
+ ) -> BaseLMResponse:
228
+ """Async API call with comprehensive error handling and streaming support."""
229
+
230
+ # Cache integration - check first
231
+ used_cache_handler = get_cache_handler(use_ephemeral_cache_only)
232
+ cache_result = used_cache_handler.hit_managed_cache(
233
+ model, messages, lm_config=lm_config, tools=tools
234
+ )
235
+ if cache_result:
236
+ return cache_result
237
+
238
+ # Apply tool injection
239
+ if tools and messages:
240
+ messages = messages.copy()
241
+ if messages and messages[0].get("role") == "system":
242
+ messages[0]["content"] = self._inject_tools_into_prompt(
243
+ messages[0]["content"], tools
244
+ )
245
+
246
+ # Prepare request
247
+ headers = {"Content-Type": "application/json"}
248
+ if self.auth_token:
249
+ headers["Authorization"] = f"Bearer {self.auth_token}"
250
+
251
+ # Apply temperature override
252
+ temp_override = self._get_temperature_override()
253
+ request_temp = temp_override if temp_override else lm_config.get("temperature", 0.7)
254
+
255
+ payload = {
256
+ "model": model, # Pass through the model name
257
+ "messages": messages,
258
+ "temperature": request_temp,
259
+ "stream": lm_config.get("stream", False)
260
+ }
261
+
262
+ timeout = self._get_timeout(lm_config)
263
+ client = await self._get_async_client()
264
+
265
+ # Make request with retry logic
266
+ for attempt in range(3):
267
+ try:
268
+ response = await client.post(
269
+ self.chat_completions_url,
270
+ json=payload,
271
+ headers=headers,
272
+ timeout=timeout
273
+ )
274
+
275
+ if response.status_code == 429:
276
+ self._handle_rate_limit(response)
277
+
278
+ response.raise_for_status()
279
+
280
+ response_data = response.json()
281
+ content = response_data["choices"][0]["message"]["content"]
282
+
283
+ # Extract tool calls
284
+ tool_calls, clean_content = self._extract_tool_calls(content, tools or [])
285
+
286
+ lm_response = BaseLMResponse(
287
+ raw_response=clean_content,
288
+ structured_output=None,
289
+ tool_calls=tool_calls
290
+ )
291
+
292
+ # Add to cache
293
+ used_cache_handler.add_to_managed_cache(
294
+ model, messages, lm_config=lm_config, output=lm_response, tools=tools
295
+ )
296
+
297
+ return lm_response
298
+
299
+ except (httpx.RequestError, httpx.TimeoutException) as e:
300
+ if attempt == 2: # Last attempt
301
+ raise
302
+ await asyncio.sleep(self._exponential_backoff_with_jitter(attempt))
303
+
304
+ def _hit_api_sync(
305
+ self,
306
+ model: str,
307
+ messages: List[Dict[str, Any]],
308
+ lm_config: Dict[str, Any],
309
+ use_ephemeral_cache_only: bool = False,
310
+ reasoning_effort: str = "low",
311
+ tools: Optional[List[BaseTool]] = None,
312
+ ) -> BaseLMResponse:
313
+ """Sync version with same logic as async."""
314
+
315
+ # Cache integration - check first
316
+ used_cache_handler = get_cache_handler(use_ephemeral_cache_only)
317
+ cache_result = used_cache_handler.hit_managed_cache(
318
+ model, messages, lm_config=lm_config, tools=tools
319
+ )
320
+ if cache_result:
321
+ return cache_result
322
+
323
+ # Apply tool injection
324
+ if tools and messages:
325
+ messages = messages.copy()
326
+ if messages and messages[0].get("role") == "system":
327
+ messages[0]["content"] = self._inject_tools_into_prompt(
328
+ messages[0]["content"], tools
329
+ )
330
+
331
+ # Prepare request
332
+ headers = {"Content-Type": "application/json"}
333
+ if self.auth_token:
334
+ headers["Authorization"] = f"Bearer {self.auth_token}"
335
+
336
+ # Apply temperature override
337
+ temp_override = self._get_temperature_override()
338
+ request_temp = temp_override if temp_override else lm_config.get("temperature", 0.7)
339
+
340
+ payload = {
341
+ "model": model, # Pass through the model name
342
+ "messages": messages,
343
+ "temperature": request_temp,
344
+ "stream": lm_config.get("stream", False)
345
+ }
346
+
347
+ timeout = self._get_timeout(lm_config)
348
+
349
+ # Make request with retry logic
350
+ for attempt in range(3):
351
+ try:
352
+ response = self.session.post(
353
+ self.chat_completions_url,
354
+ json=payload,
355
+ headers=headers,
356
+ timeout=timeout
357
+ )
358
+
359
+ if response.status_code == 429:
360
+ self._handle_rate_limit(response)
361
+
362
+ response.raise_for_status()
363
+
364
+ response_data = response.json()
365
+ content = response_data["choices"][0]["message"]["content"]
366
+
367
+ # Extract tool calls
368
+ tool_calls, clean_content = self._extract_tool_calls(content, tools or [])
369
+
370
+ lm_response = BaseLMResponse(
371
+ raw_response=clean_content,
372
+ structured_output=None,
373
+ tool_calls=tool_calls
374
+ )
375
+
376
+ # Add to cache
377
+ used_cache_handler.add_to_managed_cache(
378
+ model, messages, lm_config=lm_config, output=lm_response, tools=tools
379
+ )
380
+
381
+ return lm_response
382
+
383
+ except (requests.RequestException, requests.Timeout) as e:
384
+ if attempt == 2: # Last attempt
385
+ raise
386
+ time.sleep(self._exponential_backoff_with_jitter(attempt))
387
+
388
+ def __del__(self):
389
+ """Cleanup resources."""
390
+ if hasattr(self, 'session'):
391
+ self.session.close()
392
+ if hasattr(self, 'async_client') and self.async_client:
393
+ # Schedule cleanup for async client
394
+ pass
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: synth-ai
3
+ Version: 0.1.4
4
+ Home-page: https://github.com/synth-laboratories/synth-ai
5
+ Author: Josh Purtell
6
+ Author-email: josh@usesynth.com
7
+ License: MIT
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: openai
11
+ Requires-Dist: pydantic>=2.9.2
12
+ Requires-Dist: diskcache
13
+ Requires-Dist: backoff>=2.2.1
14
+ Requires-Dist: anthropic>=0.34.2
15
+ Requires-Dist: google>=3.0.0
16
+ Requires-Dist: google-generativeai>=0.8.1
17
+ Requires-Dist: together>=1.2.12
18
+ Dynamic: author
19
+ Dynamic: author-email
20
+ Dynamic: description
21
+ Dynamic: description-content-type
22
+ Dynamic: home-page
23
+ Dynamic: license
24
+ Dynamic: license-file
25
+ Dynamic: requires-dist
26
+
27
+ AI Infra used by the Synth AI Team
28
+ ```
29
+ _____ _ _ _____
30
+ / ____| | | | | /\ |_ _|
31
+ | (___ _ _ _ __ | |_| |__ / \ | |
32
+ \___ \| | | | '_ \| __| '_ \ / /\ \ | |
33
+ ____) | |_| | | | | |_| | | | / ____ \ _| |_
34
+ |_____/ \__, |_| |_|\__|_| |_| /_/ \_\_____|
35
+ __/ |
36
+ |___/
37
+ ```
@@ -1,4 +1,4 @@
1
- synth_ai/__init__.py,sha256=dflUvGJ59nrEo81cf7GqUA4ExYbjePhQShSLsr1B0qE,325
1
+ synth_ai/__init__.py,sha256=06VvZlwVtT7OXvu5iqFvWhLw46BmQsIAYQe8wHNUiB0,225
2
2
  synth_ai/zyk/__init__.py,sha256=kGMD-drlBVdsyT-QFODMwaZUtxPCJ9mg58GKQUvFqo0,134
3
3
  synth_ai/zyk/lms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  synth_ai/zyk/lms/config.py,sha256=UBMi0DIFQDBV_eGPK5vG8R7VwxXcV10BPGq1iV8vVjg,282
@@ -11,10 +11,10 @@ synth_ai/zyk/lms/caching/handler.py,sha256=HEo_e8q3kqDSVW0hN1AA6Rx5cBvlEeizi-kiO
11
11
  synth_ai/zyk/lms/caching/initialize.py,sha256=zZls6RKAax6Z-8oJInGaSg_RPN_fEZ6e_RCX64lMLJw,416
12
12
  synth_ai/zyk/lms/caching/persistent.py,sha256=ZaY1A9qhvfNKzcAI9FnwbIrgMKvVeIfb_yCyl3M8dxE,2860
13
13
  synth_ai/zyk/lms/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- synth_ai/zyk/lms/core/all.py,sha256=wakK0HhvYRuaQZmxClURyNf3vUkTbm3OABw3TgpMjOQ,1185
14
+ synth_ai/zyk/lms/core/all.py,sha256=MAlhGk5LBK7En-oUzFBoemGl_GZbenmvqhlkSQ7cfGc,1410
15
15
  synth_ai/zyk/lms/core/exceptions.py,sha256=K0BVdAzxVIchsvYZAaHEH1GAWBZvpxhFi-SPcJOjyPQ,205
16
16
  synth_ai/zyk/lms/core/main.py,sha256=pLz7COTdvDWQivYaA1iYYF2onUOosD_sFaPJG48bdKM,10598
17
- synth_ai/zyk/lms/core/vendor_clients.py,sha256=ar5kUbzF91l0uWcBIDp5fKkrv6KWpvw-O3LN7dkP4Nk,2952
17
+ synth_ai/zyk/lms/core/vendor_clients.py,sha256=7D30Ol-eVaJ8pZnbqeKADJ6q_Nz5buOd2qQJrrn8Rlk,3647
18
18
  synth_ai/zyk/lms/cost/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  synth_ai/zyk/lms/cost/monitor.py,sha256=cSKIvw6WdPZIRubADWxQoh1MdB40T8-jjgfNUeUHIn0,5
20
20
  synth_ai/zyk/lms/cost/statefulness.py,sha256=TOsuXL8IjtKOYJ2aJQF8TwJVqn_wQ7AIwJJmdhMye7U,36
@@ -22,7 +22,6 @@ synth_ai/zyk/lms/structured_outputs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQ
22
22
  synth_ai/zyk/lms/structured_outputs/handler.py,sha256=4DboLNZyXpqNB5YNCSgGHBsNbWqFk-8uwV944nOYNo8,16919
23
23
  synth_ai/zyk/lms/structured_outputs/inject.py,sha256=Fy-zDeleRxOZ8ZRM6IuZ6CP2XZnMe4K2PEn4Q9c_KPY,11777
24
24
  synth_ai/zyk/lms/structured_outputs/rehabilitate.py,sha256=ecKGWrgWYUSplqHzK40KdohwaN8gBV0xl4LUReLN_vg,7910
25
- synth_ai/zyk/lms/tools/base.py,sha256=i-AIVRlitiQ4JMJ_BBFRSpUcWgxWIUYoHxAqfxHN_7E,4056
26
25
  synth_ai/zyk/lms/vendors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
26
  synth_ai/zyk/lms/vendors/base.py,sha256=aK4PEtkMLt_o3qD22kW-x3HJUEKdIk06zlH4kX0VkAE,760
28
27
  synth_ai/zyk/lms/vendors/openai_standard.py,sha256=dgHC7RWrxwaWto6_frKdfEKazKvvAMYJM1YgJgbVpb8,12279
@@ -35,12 +34,13 @@ synth_ai/zyk/lms/vendors/core/openai_api.py,sha256=O5KbRpy0pDDofVjxgZdeU69ueUl6S
35
34
  synth_ai/zyk/lms/vendors/local/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
35
  synth_ai/zyk/lms/vendors/local/ollama.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
36
  synth_ai/zyk/lms/vendors/supported/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
+ synth_ai/zyk/lms/vendors/supported/custom_endpoint.py,sha256=awluDoUgcRROsZdH_o_Whe2D0MjQfOZOnmgWbESAbIQ,15523
38
38
  synth_ai/zyk/lms/vendors/supported/deepseek.py,sha256=BElW0NGpkSA62wOqzzMtDw8XR36rSNXK5LldeHJkQrc,2430
39
39
  synth_ai/zyk/lms/vendors/supported/groq.py,sha256=Fbi7QvhdLx0F-VHO5PY-uIQlPR0bo3C9h1MvIOx8nz0,388
40
40
  synth_ai/zyk/lms/vendors/supported/ollama.py,sha256=K30VBFRTd7NYyPmyBVRZS2sm0UB651AHp9i3wd55W64,469
41
41
  synth_ai/zyk/lms/vendors/supported/together.py,sha256=Ni_jBqqGPN0PkkY-Ew64s3gNKk51k3FCpLSwlNhKbf0,342
42
- synth_ai-0.1.3.dist-info/licenses/LICENSE,sha256=ynhjRQUfqA_RdGRATApfFA_fBAy9cno04sLtLUqxVFM,1069
43
- synth_ai-0.1.3.dist-info/METADATA,sha256=BG1WVpscaA1qM4CBePMnBecdIu-p0X6CLXVvMOOWeFo,2760
44
- synth_ai-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
45
- synth_ai-0.1.3.dist-info/top_level.txt,sha256=fBmtZyVHuKaGa29oHBaaUkrUIWTqSpoVMPiVdCDP3k8,9
46
- synth_ai-0.1.3.dist-info/RECORD,,
42
+ synth_ai-0.1.4.dist-info/licenses/LICENSE,sha256=ynhjRQUfqA_RdGRATApfFA_fBAy9cno04sLtLUqxVFM,1069
43
+ synth_ai-0.1.4.dist-info/METADATA,sha256=Bz_Gf7gZtnLIJdgjrD17XS_HQNj_oGlXdFufcrLP2Ko,1084
44
+ synth_ai-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
45
+ synth_ai-0.1.4.dist-info/top_level.txt,sha256=fBmtZyVHuKaGa29oHBaaUkrUIWTqSpoVMPiVdCDP3k8,9
46
+ synth_ai-0.1.4.dist-info/RECORD,,
@@ -1,118 +0,0 @@
1
- from typing import Type
2
-
3
- from pydantic import BaseModel
4
-
5
-
6
- class BaseTool(BaseModel):
7
- name: str
8
- arguments: Type[BaseModel]
9
- description: str = ""
10
- strict: bool = True
11
-
12
- def to_openai_tool(self):
13
- schema = self.arguments.model_json_schema()
14
- schema["additionalProperties"] = False
15
-
16
- if "properties" in schema:
17
- for prop_name, prop_schema in schema["properties"].items():
18
- if prop_schema.get("type") == "array":
19
- items_schema = prop_schema.get("items", {})
20
- if not isinstance(items_schema, dict) or not items_schema.get("type"):
21
- prop_schema["items"] = {"type": "string"}
22
-
23
- elif "anyOf" in prop_schema:
24
- for sub_schema in prop_schema["anyOf"]:
25
- if isinstance(sub_schema, dict) and sub_schema.get("type") == "array":
26
- items_schema = sub_schema.get("items", {})
27
- if not isinstance(items_schema, dict) or not items_schema.get("type"):
28
- sub_schema["items"] = {"type": "string"}
29
-
30
- return {
31
- "type": "function",
32
- "function": {
33
- "name": self.name,
34
- "description": self.description,
35
- "parameters": schema,
36
- "strict": self.strict,
37
- },
38
- }
39
-
40
- def to_anthropic_tool(self):
41
- schema = self.arguments.model_json_schema()
42
- schema["additionalProperties"] = False
43
-
44
- return {
45
- "name": self.name,
46
- "description": self.description,
47
- "input_schema": {
48
- "type": "object",
49
- "properties": schema["properties"],
50
- "required": schema.get("required", []),
51
- },
52
- }
53
-
54
- def to_mistral_tool(self):
55
- schema = self.arguments.model_json_schema()
56
- properties = {}
57
- for prop_name, prop in schema.get("properties", {}).items():
58
- prop_type = prop["type"]
59
- if prop_type == "array" and "items" in prop:
60
- properties[prop_name] = {
61
- "type": "array",
62
- "items": prop["items"],
63
- "description": prop.get("description", ""),
64
- }
65
- continue
66
-
67
- properties[prop_name] = {
68
- "type": prop_type,
69
- "description": prop.get("description", ""),
70
- }
71
- if "enum" in prop:
72
- properties[prop_name]["enum"] = prop["enum"]
73
-
74
- parameters = {
75
- "type": "object",
76
- "properties": properties,
77
- "required": schema.get("required", []),
78
- "additionalProperties": False,
79
- }
80
- return {
81
- "type": "function",
82
- "function": {
83
- "name": self.name,
84
- "description": self.description,
85
- "parameters": parameters,
86
- },
87
- }
88
-
89
- def to_gemini_tool(self):
90
- schema = self.arguments.model_json_schema()
91
- # Convert Pydantic schema types to Gemini schema types
92
- properties = {}
93
- for name, prop in schema["properties"].items():
94
- prop_type = prop.get("type", "string")
95
- if prop_type == "array" and "items" in prop:
96
- properties[name] = {
97
- "type": "array",
98
- "items": prop["items"],
99
- "description": prop.get("description", ""),
100
- }
101
- continue
102
-
103
- properties[name] = {
104
- "type": prop_type,
105
- "description": prop.get("description", ""),
106
- }
107
- if "enum" in prop:
108
- properties[name]["enum"] = prop["enum"]
109
-
110
- return {
111
- "name": self.name,
112
- "description": self.description,
113
- "parameters": {
114
- "type": "object",
115
- "properties": properties,
116
- "required": schema.get("required", []),
117
- },
118
- }
@@ -1,67 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: synth-ai
3
- Version: 0.1.3
4
- Summary: Software for aiding the best and multiplying the will.
5
- Home-page: https://github.com/synth-laboratories/synth-ai
6
- Author: Josh Purtell
7
- Author-email: Josh Purtell <josh@usesynth.ai>
8
- License: MIT License
9
-
10
- Copyright (c) 2024 Josh Purtell
11
-
12
- Permission is hereby granted, free of charge, to any person obtaining a copy
13
- of this software and associated documentation files (the "Software"), to deal
14
- in the Software without restriction, including without limitation the rights
15
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
- copies of the Software, and to permit persons to whom the Software is
17
- furnished to do so, subject to the following conditions:
18
-
19
- The above copyright notice and this permission notice shall be included in all
20
- copies or substantial portions of the Software.
21
-
22
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
- SOFTWARE.
29
-
30
- Project-URL: Homepage, https://github.com/synth-laboratories/synth-ai
31
- Keywords: synth-ai
32
- Classifier: License :: OSI Approved :: MIT License
33
- Classifier: Programming Language :: Python
34
- Classifier: Programming Language :: Python :: 3
35
- Requires-Python: >=3.10
36
- Description-Content-Type: text/markdown
37
- License-File: LICENSE
38
- Requires-Dist: openai>=1.0.0
39
- Requires-Dist: pydantic>=2.0.0
40
- Requires-Dist: diskcache>=5.0.0
41
- Requires-Dist: backoff>=2.2.1
42
- Requires-Dist: anthropic>=0.34.2
43
- Requires-Dist: google>=3.0.0
44
- Requires-Dist: google-api-core
45
- Requires-Dist: google-generativeai
46
- Requires-Dist: google-genai
47
- Requires-Dist: together>=1.2.12
48
- Requires-Dist: langfuse<3.0.0,>=2.53.9
49
- Requires-Dist: datasets>=3.2.0
50
- Requires-Dist: groq>=0.18.0
51
- Requires-Dist: pytest-timeout>=2.3.1
52
- Requires-Dist: mistralai
53
- Dynamic: author
54
- Dynamic: home-page
55
- Dynamic: license-file
56
-
57
- AI Infra used by the Synth AI Team
58
- ```
59
- _____ _ _ _____
60
- / ____| | | | | /\ |_ _|
61
- | (___ _ _ _ __ | |_| |__ / \ | |
62
- \___ \| | | | '_ \| __| '_ \ / /\ \ | |
63
- ____) | |_| | | | | |_| | | | / ____ \ _| |_
64
- |_____/ \__, |_| |_|\__|_| |_| /_/ \_\_____|
65
- __/ |
66
- |___/
67
- ```