synth-ai 0.1.0.dev14__tar.gz → 0.1.0.dev51__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/PKG-INFO +9 -10
  2. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/pyproject.toml +10 -12
  3. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/setup.py +1 -1
  4. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/__init__.py +2 -0
  5. synth_ai-0.1.0.dev51/synth_ai/zyk/__init__.py +3 -0
  6. synth_ai-0.1.0.dev51/synth_ai/zyk/lms/caching/ephemeral.py +72 -0
  7. synth_ai-0.1.0.dev51/synth_ai/zyk/lms/caching/handler.py +137 -0
  8. synth_ai-0.1.0.dev51/synth_ai/zyk/lms/caching/persistent.py +83 -0
  9. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/config.py +2 -0
  10. synth_ai-0.1.0.dev51/synth_ai/zyk/lms/constants.py +22 -0
  11. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/core/main.py +36 -17
  12. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/core/vendor_clients.py +4 -3
  13. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/structured_outputs/handler.py +99 -46
  14. synth_ai-0.1.0.dev51/synth_ai/zyk/lms/structured_outputs/inject.py +314 -0
  15. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/structured_outputs/rehabilitate.py +4 -3
  16. synth_ai-0.1.0.dev51/synth_ai/zyk/lms/tools/base.py +118 -0
  17. synth_ai-0.1.0.dev51/synth_ai/zyk/lms/vendors/base.py +31 -0
  18. synth_ai-0.1.0.dev51/synth_ai/zyk/lms/vendors/core/anthropic_api.py +365 -0
  19. synth_ai-0.1.0.dev51/synth_ai/zyk/lms/vendors/core/gemini_api.py +282 -0
  20. synth_ai-0.1.0.dev51/synth_ai/zyk/lms/vendors/core/mistral_api.py +331 -0
  21. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/vendors/core/openai_api.py +71 -29
  22. synth_ai-0.1.0.dev51/synth_ai/zyk/lms/vendors/openai_standard.py +345 -0
  23. synth_ai-0.1.0.dev51/synth_ai/zyk/lms/vendors/supported/deepseek.py +73 -0
  24. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai.egg-info/PKG-INFO +9 -10
  25. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai.egg-info/SOURCES.txt +2 -7
  26. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai.egg-info/requires.txt +6 -8
  27. synth_ai-0.1.0.dev51/synth_ai.egg-info/top_level.txt +1 -0
  28. synth_ai-0.1.0.dev14/private_tests/try_synth_sdk.py +0 -1
  29. synth_ai-0.1.0.dev14/public_tests/test_all_structured_outputs.py +0 -201
  30. synth_ai-0.1.0.dev14/public_tests/test_synth_sdk.py +0 -389
  31. synth_ai-0.1.0.dev14/synth_ai/zyk/__init__.py +0 -3
  32. synth_ai-0.1.0.dev14/synth_ai/zyk/lms/caching/ephemeral.py +0 -50
  33. synth_ai-0.1.0.dev14/synth_ai/zyk/lms/caching/handler.py +0 -92
  34. synth_ai-0.1.0.dev14/synth_ai/zyk/lms/caching/persistent.py +0 -55
  35. synth_ai-0.1.0.dev14/synth_ai/zyk/lms/structured_outputs/inject.py +0 -185
  36. synth_ai-0.1.0.dev14/synth_ai/zyk/lms/vendors/base.py +0 -15
  37. synth_ai-0.1.0.dev14/synth_ai/zyk/lms/vendors/constants.py +0 -5
  38. synth_ai-0.1.0.dev14/synth_ai/zyk/lms/vendors/core/anthropic_api.py +0 -191
  39. synth_ai-0.1.0.dev14/synth_ai/zyk/lms/vendors/core/gemini_api.py +0 -146
  40. synth_ai-0.1.0.dev14/synth_ai/zyk/lms/vendors/core/mistral_api.py +0 -221
  41. synth_ai-0.1.0.dev14/synth_ai/zyk/lms/vendors/openai_standard.py +0 -144
  42. synth_ai-0.1.0.dev14/synth_ai/zyk/lms/vendors/supported/deepseek.py +0 -18
  43. synth_ai-0.1.0.dev14/synth_ai.egg-info/top_level.txt +0 -6
  44. synth_ai-0.1.0.dev14/tests/test_agent.py +0 -538
  45. synth_ai-0.1.0.dev14/tests/test_recursive_structured_outputs.py +0 -180
  46. synth_ai-0.1.0.dev14/tests/test_structured_outputs.py +0 -100
  47. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/LICENSE +0 -0
  48. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/README.md +0 -0
  49. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/setup.cfg +0 -0
  50. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/__init__.py +0 -0
  51. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/caching/__init__.py +0 -0
  52. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/caching/constants.py +0 -0
  53. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/caching/dbs.py +0 -0
  54. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/caching/initialize.py +0 -0
  55. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/core/__init__.py +0 -0
  56. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/core/all.py +0 -0
  57. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/core/exceptions.py +0 -0
  58. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/cost/__init__.py +0 -0
  59. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/cost/monitor.py +0 -0
  60. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/cost/statefulness.py +0 -0
  61. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/structured_outputs/__init__.py +0 -0
  62. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/vendors/__init__.py +0 -0
  63. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/vendors/core/__init__.py +0 -0
  64. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/vendors/local/__init__.py +0 -0
  65. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/vendors/local/ollama.py +0 -0
  66. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/vendors/retries.py +0 -0
  67. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/vendors/supported/__init__.py +0 -0
  68. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/vendors/supported/groq.py +0 -0
  69. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/vendors/supported/ollama.py +0 -0
  70. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai/zyk/lms/vendors/supported/together.py +0 -0
  71. {synth_ai-0.1.0.dev14 → synth_ai-0.1.0.dev51}/synth_ai.egg-info/dependency_links.txt +0 -0
  72. {synth_ai-0.1.0.dev14/public_tests → synth_ai-0.1.0.dev51/tests}/test_agent.py +0 -0
  73. {synth_ai-0.1.0.dev14/public_tests → synth_ai-0.1.0.dev51/tests}/test_recursive_structured_outputs.py +0 -0
  74. {synth_ai-0.1.0.dev14/public_tests → synth_ai-0.1.0.dev51/tests}/test_structured_outputs.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: synth-ai
3
- Version: 0.1.0.dev14
3
+ Version: 0.1.0.dev51
4
4
  Summary: Software for aiding the best and multiplying the will.
5
5
  Home-page: https://github.com/synth-laboratories/synth-ai
6
6
  Author: Josh Purtell
@@ -35,24 +35,23 @@ Classifier: Programming Language :: Python :: 3
35
35
  Requires-Python: >=3.10
36
36
  Description-Content-Type: text/markdown
37
37
  License-File: LICENSE
38
- Requires-Dist: openai
39
- Requires-Dist: pydantic
40
- Requires-Dist: diskcache
38
+ Requires-Dist: openai>=1.0.0
39
+ Requires-Dist: pydantic>=2.0.0
40
+ Requires-Dist: diskcache>=5.0.0
41
41
  Requires-Dist: backoff>=2.2.1
42
42
  Requires-Dist: anthropic>=0.34.2
43
43
  Requires-Dist: google>=3.0.0
44
- Requires-Dist: google-generativeai>=0.8.1
44
+ Requires-Dist: google-api-core
45
+ Requires-Dist: google-generativeai
45
46
  Requires-Dist: together>=1.2.12
46
47
  Requires-Dist: langfuse>=2.56.1
47
- Requires-Dist: synth-sdk>=0.3.1.dev4
48
48
  Requires-Dist: datasets>=3.2.0
49
49
  Requires-Dist: groq>=0.18.0
50
50
  Requires-Dist: pytest-timeout>=2.3.1
51
- Requires-Dist: lock>=2018.3.25.2110
52
- Requires-Dist: ollama>=0.4.7
53
- Requires-Dist: mistralai>=1.5.0
51
+ Requires-Dist: mistralai
54
52
  Dynamic: author
55
53
  Dynamic: home-page
54
+ Dynamic: license-file
56
55
 
57
56
  AI Infra used by the Synth AI Team
58
57
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "synth-ai"
3
- version = "0.1.0.dev14"
3
+ version = "0.1.0.dev51"
4
4
  description = "Software for aiding the best and multiplying the will."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Josh Purtell", email = "josh@usesynth.ai" }]
@@ -12,22 +12,20 @@ classifiers = [
12
12
  ]
13
13
  keywords = ["synth-ai"]
14
14
  dependencies = [
15
- "openai",
16
- "pydantic",
17
- "diskcache",
15
+ "openai>=1.0.0",
16
+ "pydantic>=2.0.0",
17
+ "diskcache>=5.0.0",
18
18
  "backoff>=2.2.1",
19
19
  "anthropic>=0.34.2",
20
20
  "google>=3.0.0",
21
- "google-generativeai>=0.8.1",
21
+ "google-api-core",
22
+ "google-generativeai",
22
23
  "together>=1.2.12",
23
24
  "langfuse>=2.56.1",
24
- "synth-sdk>=0.3.1.dev4",
25
25
  "datasets>=3.2.0",
26
26
  "groq>=0.18.0",
27
27
  "pytest-timeout>=2.3.1",
28
- "lock>=2018.3.25.2110",
29
- "ollama>=0.4.7",
30
- "mistralai>=1.5.0",
28
+ "mistralai",
31
29
  ]
32
30
  requires-python = ">=3.10"
33
31
 
@@ -38,9 +36,9 @@ Homepage = "https://github.com/synth-laboratories/synth-ai"
38
36
  requires = ["setuptools>=61.0"]
39
37
  build-backend = "setuptools.build_meta"
40
38
 
41
- [tool.setuptools]
42
- packages.find = {namespaces = true}
43
- package-dir = {"synth_ai" = "synth_ai"}
39
+ [tool.setuptools.packages.find]
40
+ where = ["."] # Assumes 'synth_ai' package is in a dir named 'synth_ai' at this level
41
+ include = ["synth_ai*"]
44
42
 
45
43
  # Explicitly exclude test directories and files
46
44
  [tool.setuptools.exclude-package-data]
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
2
2
 
3
3
  setup(
4
4
  name="synth-ai",
5
- version="0.1.0.dev14",
5
+ version="0.0.0.dev1",
6
6
  packages=find_packages(),
7
7
  install_requires=[
8
8
  "openai",
@@ -4,5 +4,7 @@ Synth AI - Software for aiding the best and multiplying the will.
4
4
 
5
5
  from importlib.metadata import version
6
6
 
7
+ from synth_ai.zyk import LM # Assuming LM is in zyk.py in the same directory
7
8
 
8
9
  __version__ = version("synth-ai") # Gets version from installed package metadata
10
+ __all__ = ["LM"] # Explicitly define public API
@@ -0,0 +1,3 @@
1
+ from synth_ai.zyk.lms.core.main import LM
2
+ from synth_ai.zyk.lms.vendors.base import BaseLMResponse
3
+ __all__ = ["LM", "BaseLMResponse"]
@@ -0,0 +1,72 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+ from typing import Optional, Union
4
+
5
+ from diskcache import Cache
6
+ from pydantic import BaseModel
7
+
8
+ from synth_ai.zyk.lms.caching.constants import DISKCACHE_SIZE_LIMIT
9
+ from synth_ai.zyk.lms.vendors.base import BaseLMResponse
10
+
11
+
12
+ @dataclass
13
+ class EphemeralCache:
14
+ def __init__(self, fast_cache_dir: str = ".cache/ephemeral_cache"):
15
+ os.makedirs(fast_cache_dir, exist_ok=True)
16
+ self.fast_cache = Cache(fast_cache_dir, size_limit=DISKCACHE_SIZE_LIMIT)
17
+
18
+ def hit_cache(
19
+ self, key: str, response_model: Optional[BaseModel] = None
20
+ ) -> Optional[BaseLMResponse]:
21
+ if key not in self.fast_cache:
22
+ return None
23
+
24
+ try:
25
+ cache_data = self.fast_cache[key]
26
+ except AttributeError:
27
+ return None
28
+
29
+ if not isinstance(cache_data, dict):
30
+ return BaseLMResponse(
31
+ raw_response=cache_data, structured_output=None, tool_calls=None
32
+ )
33
+
34
+ raw_response = cache_data.get("raw_response")
35
+ tool_calls = cache_data.get("tool_calls")
36
+ structured_output = cache_data.get("structured_output")
37
+
38
+ if response_model and structured_output:
39
+ structured_output = response_model(**structured_output)
40
+
41
+ return BaseLMResponse(
42
+ raw_response=raw_response,
43
+ structured_output=structured_output,
44
+ tool_calls=tool_calls,
45
+ )
46
+
47
+ def add_to_cache(self, key: str, response: Union[BaseLMResponse, str]) -> None:
48
+ if isinstance(response, str):
49
+ self.fast_cache[key] = response
50
+ return
51
+
52
+ if isinstance(response, BaseLMResponse):
53
+ cache_data = {
54
+ "raw_response": response.raw_response
55
+ if response.raw_response is not None
56
+ else None,
57
+ "tool_calls": response.tool_calls
58
+ if response.tool_calls is not None
59
+ else None,
60
+ "structured_output": (
61
+ response.structured_output.model_dump()
62
+ if response.structured_output is not None
63
+ else None
64
+ ),
65
+ }
66
+ self.fast_cache[key] = cache_data
67
+ return
68
+
69
+ raise ValueError(f"Invalid response type: {type(response)}")
70
+
71
+ def close(self):
72
+ self.fast_cache.close()
@@ -0,0 +1,137 @@
1
+ import hashlib
2
+ from typing import Any, Dict, List, Optional, Type
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from synth_ai.zyk.lms.caching.ephemeral import EphemeralCache
7
+ from synth_ai.zyk.lms.caching.persistent import PersistentCache
8
+ from synth_ai.zyk.lms.tools.base import BaseTool
9
+ from synth_ai.zyk.lms.vendors.base import BaseLMResponse
10
+
11
+ persistent_cache = PersistentCache()
12
+ ephemeral_cache = EphemeralCache()
13
+
14
+ import logging
15
+ logger = logging.getLogger(__name__)
16
+
17
+ def map_params_to_key(
18
+ messages: List[Dict],
19
+ model: str,
20
+ temperature: float,
21
+ response_model: Optional[Type[BaseModel]],
22
+ tools: Optional[List[BaseTool]] = None,
23
+ reasoning_effort: str = "low",
24
+ ) -> str:
25
+ if any(m is None for m in messages):
26
+ raise ValueError("Messages cannot contain None values - messages: ", messages)
27
+ if not all([isinstance(msg["content"], str) for msg in messages]):
28
+ normalized_messages = "".join([str(msg["content"]) for msg in messages])
29
+ else:
30
+ normalized_messages = "".join([msg["content"] for msg in messages])
31
+ normalized_model = model
32
+ normalized_temperature = f"{temperature:.2f}"[:4]
33
+ normalized_response_model = str(response_model.schema()) if response_model else ""
34
+ normalized_reasoning_effort = reasoning_effort if reasoning_effort else ""
35
+
36
+ # Normalize tools if present
37
+ normalized_tools = ""
38
+ if tools and all(isinstance(tool, BaseTool) for tool in tools):
39
+ tool_schemas = []
40
+ for tool in sorted(tools, key=lambda x: x.name): # Sort by name for consistency
41
+ tool_schema = {
42
+ "name": tool.name,
43
+ "description": tool.description,
44
+ "arguments": tool.arguments.schema(),
45
+ }
46
+ tool_schemas.append(str(tool_schema))
47
+ #logger.error(f"Tool schemas: {tool_schemas}")
48
+ normalized_tools = "".join(tool_schemas)
49
+ elif tools:
50
+ logger.error(f"Tools: {tools}")
51
+ normalized_tools = "".join([str(tool) for tool in tools])
52
+
53
+ key_str = ""
54
+ components = [
55
+ normalized_messages,
56
+ normalized_model,
57
+ normalized_temperature,
58
+ normalized_response_model,
59
+ normalized_tools,
60
+ normalized_reasoning_effort
61
+ ]
62
+ for component in components:
63
+ if component:
64
+ key_str += str(component)
65
+
66
+ return hashlib.sha256(key_str.encode()).hexdigest()
67
+
68
+
69
+ class CacheHandler:
70
+ use_persistent_store: bool = False
71
+ use_ephemeral_store: bool = True
72
+
73
+ def __init__(
74
+ self, use_persistent_store: bool = False, use_ephemeral_store: bool = True
75
+ ):
76
+ self.use_persistent_store = use_persistent_store
77
+ self.use_ephemeral_store = use_ephemeral_store
78
+
79
+ def _validate_messages(self, messages: List[Dict[str, Any]]) -> None:
80
+ """Validate that messages are in the correct format."""
81
+ assert all(
82
+ [type(msg["content"]) == str for msg in messages]
83
+ ), "All message contents must be strings"
84
+
85
+ def hit_managed_cache(
86
+ self,
87
+ model: str,
88
+ messages: List[Dict[str, Any]],
89
+ lm_config: Dict[str, Any],
90
+ tools: Optional[List[BaseTool]] = None,
91
+ ) -> Optional[BaseLMResponse]:
92
+ """Hit the cache with the given key."""
93
+ self._validate_messages(messages)
94
+ assert type(lm_config) == dict, "lm_config must be a dictionary"
95
+ key = map_params_to_key(
96
+ messages,
97
+ model,
98
+ lm_config.get("temperature", 0.0),
99
+ lm_config.get("response_model", None),
100
+ tools,
101
+ lm_config.get("reasoning_effort", "low"),
102
+ )
103
+ if self.use_persistent_store:
104
+ return persistent_cache.hit_cache(
105
+ key=key, response_model=lm_config.get("response_model", None)
106
+ )
107
+ elif self.use_ephemeral_store:
108
+ return ephemeral_cache.hit_cache(
109
+ key=key, response_model=lm_config.get("response_model", None)
110
+ )
111
+ else:
112
+ return None
113
+
114
+ def add_to_managed_cache(
115
+ self,
116
+ model: str,
117
+ messages: List[Dict[str, Any]],
118
+ lm_config: Dict[str, Any],
119
+ output: BaseLMResponse,
120
+ tools: Optional[List[BaseTool]] = None,
121
+ ) -> None:
122
+ """Add the given output to the cache."""
123
+ self._validate_messages(messages)
124
+ assert type(output) == BaseLMResponse, "output must be a BaseLMResponse"
125
+ assert type(lm_config) == dict, "lm_config must be a dictionary"
126
+ key = map_params_to_key(
127
+ messages,
128
+ model,
129
+ lm_config.get("temperature", 0.0),
130
+ lm_config.get("response_model", None),
131
+ tools,
132
+ lm_config.get("reasoning_effort", "low"),
133
+ )
134
+ if self.use_persistent_store:
135
+ persistent_cache.add_to_cache(key, output)
136
+ if self.use_ephemeral_store:
137
+ ephemeral_cache.add_to_cache(key, output)
@@ -0,0 +1,83 @@
1
+ import json
2
+ import os
3
+ import sqlite3
4
+ from dataclasses import dataclass
5
+ from typing import Optional, Type, Union
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from synth_ai.zyk.lms.vendors.base import BaseLMResponse
10
+
11
+
12
+ @dataclass
13
+ class PersistentCache:
14
+ def __init__(self, db_path: str = ".cache/persistent_cache.db"):
15
+ os.makedirs(os.path.dirname(db_path), exist_ok=True)
16
+ self.conn = sqlite3.connect(db_path)
17
+ self.cursor = self.conn.cursor()
18
+ self.cursor.execute("""CREATE TABLE IF NOT EXISTS cache
19
+ (key TEXT PRIMARY KEY, response TEXT)""")
20
+ self.conn.commit()
21
+
22
+ def hit_cache(
23
+ self, key: str, response_model: Optional[Type[BaseModel]] = None
24
+ ) -> Optional[BaseLMResponse]:
25
+ self.cursor.execute("SELECT response FROM cache WHERE key = ?", (key,))
26
+ result = self.cursor.fetchone()
27
+ if not result:
28
+ return None
29
+
30
+ try:
31
+ cache_data = json.loads(result[0])
32
+ except json.JSONDecodeError:
33
+ # Handle legacy string responses
34
+ return BaseLMResponse(
35
+ raw_response=result[0], structured_output=None, tool_calls=None
36
+ )
37
+
38
+ if not isinstance(cache_data, dict):
39
+ return BaseLMResponse(
40
+ raw_response=cache_data, structured_output=None, tool_calls=None
41
+ )
42
+
43
+ raw_response = cache_data.get("raw_response")
44
+ tool_calls = cache_data.get("tool_calls")
45
+ structured_output = cache_data.get("structured_output")
46
+
47
+ if response_model and structured_output:
48
+ structured_output = response_model(**structured_output)
49
+
50
+ return BaseLMResponse(
51
+ raw_response=raw_response,
52
+ structured_output=structured_output,
53
+ tool_calls=tool_calls,
54
+ )
55
+
56
+ def add_to_cache(self, key: str, response: Union[BaseLMResponse, str]) -> None:
57
+ if isinstance(response, str):
58
+ cache_data = response
59
+ elif isinstance(response, BaseLMResponse):
60
+ cache_data = {
61
+ "raw_response": response.raw_response
62
+ if response.raw_response is not None
63
+ else None,
64
+ "tool_calls": response.tool_calls
65
+ if response.tool_calls is not None
66
+ else None,
67
+ "structured_output": (
68
+ response.structured_output.model_dump()
69
+ if response.structured_output is not None
70
+ else None
71
+ ),
72
+ }
73
+ else:
74
+ raise ValueError(f"Invalid response type: {type(response)}")
75
+
76
+ self.cursor.execute(
77
+ "INSERT OR REPLACE INTO cache (key, response) VALUES (?, ?)",
78
+ (key, json.dumps(cache_data)),
79
+ )
80
+ self.conn.commit()
81
+
82
+ def close(self) -> None:
83
+ self.conn.close()
@@ -6,3 +6,5 @@ def should_use_cache() -> bool:
6
6
  load_dotenv()
7
7
  cache_env = os.getenv("USE_ZYK_CACHE", "true").lower()
8
8
  return cache_env not in ("false", "0", "no")
9
+
10
+ reasoning_models = ["o1","o3-mini", "o3", "o4-mini", "claude-3-7-sonnet-latest"]
@@ -0,0 +1,22 @@
1
+ OPENAI_REASONING_MODELS = ["o4", "o4-mini", "o3","o3-mini", "o1-mini", "o1"]
2
+ CLAUDE_REASONING_MODELS = ["claude-3-7-sonnet-latest"]
3
+ GEMINI_REASONING_MODELS = ["gemini-2.5-flash", "gemini-2.5-pro"]
4
+
5
+ # Gemini models that support thinking
6
+ GEMINI_REASONING_MODELS = ["gemini-2.5-flash", "gemini-2.5-pro"]
7
+ GEMINI_THINKING_BUDGETS = {
8
+ "high": 10000,
9
+ "medium": 5000,
10
+ "low": 2500,
11
+ }
12
+
13
+ # Anthropic Sonnet 3.7 budgets
14
+ SONNET_37_BUDGETS = {
15
+ "high": 8192,
16
+ "medium": 4096,
17
+ "low": 2048,
18
+ }
19
+
20
+ REASONING_MODELS = OPENAI_REASONING_MODELS + CLAUDE_REASONING_MODELS + GEMINI_REASONING_MODELS
21
+
22
+ SPECIAL_BASE_TEMPS = {model: 1 for model in REASONING_MODELS}
@@ -10,9 +10,8 @@ from synth_ai.zyk.lms.core.vendor_clients import (
10
10
  )
11
11
  from synth_ai.zyk.lms.structured_outputs.handler import StructuredOutputHandler
12
12
  from synth_ai.zyk.lms.vendors.base import VendorBase
13
-
14
- REASONING_MODELS = ["deepseek-reasoner", "o1-mini", "o1-preview", "o1", "o3"]
15
-
13
+ from synth_ai.zyk.lms.tools.base import BaseTool
14
+ from synth_ai.zyk.lms.config import reasoning_models
16
15
 
17
16
  def build_messages(
18
17
  sys_msg: str,
@@ -108,7 +107,7 @@ class LM:
108
107
  {"max_retries": max_retries_dict.get(max_retries, 2)},
109
108
  )
110
109
  # Override temperature to 1 for reasoning models
111
- effective_temperature = 1.0 if model_name in REASONING_MODELS else temperature
110
+ effective_temperature = 1.0 if model_name in reasoning_models else temperature
112
111
  self.lm_config = {"temperature": effective_temperature}
113
112
  self.model_name = model_name
114
113
 
@@ -120,6 +119,8 @@ class LM:
120
119
  images_as_bytes: List[Any] = [],
121
120
  response_model: Optional[BaseModel] = None,
122
121
  use_ephemeral_cache_only: bool = False,
122
+ tools: Optional[List[BaseTool]] = None,
123
+ reasoning_effort: str = "low",
123
124
  ):
124
125
  assert (system_message is None) == (
125
126
  user_message is None
@@ -127,37 +128,45 @@ class LM:
127
128
  assert (
128
129
  (messages is None) != (system_message is None)
129
130
  ), "Must provide either messages or system_message/user_message pair, but not both"
130
-
131
+ assert not (response_model and tools), "Cannot provide both response_model and tools"
131
132
  if messages is None:
132
133
  messages = build_messages(
133
134
  system_message, user_message, images_as_bytes, self.model_name
134
135
  )
135
-
136
+ result = None
136
137
  if response_model:
137
138
  try:
138
- return self.structured_output_handler.call_sync(
139
+ result = self.structured_output_handler.call_sync(
139
140
  messages,
140
141
  model=self.model_name,
141
142
  lm_config=self.lm_config,
142
143
  response_model=response_model,
143
144
  use_ephemeral_cache_only=use_ephemeral_cache_only,
145
+ reasoning_effort=reasoning_effort,
144
146
  )
145
147
  except StructuredOutputCoercionFailureException:
146
148
  # print("Falling back to backup handler")
147
- return self.backup_structured_output_handler.call_sync(
149
+ result = self.backup_structured_output_handler.call_sync(
148
150
  messages,
149
151
  model=self.model_name,
150
152
  lm_config=self.lm_config,
151
153
  response_model=response_model,
152
154
  use_ephemeral_cache_only=use_ephemeral_cache_only,
155
+ reasoning_effort=reasoning_effort,
153
156
  )
154
157
  else:
155
- return self.client._hit_api_sync(
158
+ result = self.client._hit_api_sync(
156
159
  messages=messages,
157
160
  model=self.model_name,
158
161
  lm_config=self.lm_config,
159
162
  use_ephemeral_cache_only=use_ephemeral_cache_only,
163
+ tools=tools,
164
+ reasoning_effort=reasoning_effort,
160
165
  )
166
+ assert isinstance(result.raw_response, str), "Raw response must be a string"
167
+ assert (isinstance(result.structured_output, BaseModel) or result.structured_output is None), "Structured output must be a Pydantic model or None"
168
+ assert (isinstance(result.tool_calls, list) or result.tool_calls is None), "Tool calls must be a list or None"
169
+ return result
161
170
 
162
171
  async def respond_async(
163
172
  self,
@@ -167,6 +176,8 @@ class LM:
167
176
  images_as_bytes: List[Any] = [],
168
177
  response_model: Optional[BaseModel] = None,
169
178
  use_ephemeral_cache_only: bool = False,
179
+ tools: Optional[List[BaseTool]] = None,
180
+ reasoning_effort: str = "low",
170
181
  ):
171
182
  # "In respond_async")
172
183
  assert (system_message is None) == (
@@ -176,39 +187,47 @@ class LM:
176
187
  (messages is None) != (system_message is None)
177
188
  ), "Must provide either messages or system_message/user_message pair, but not both"
178
189
 
190
+ assert not (response_model and tools), "Cannot provide both response_model and tools"
179
191
  if messages is None:
180
192
  messages = build_messages(
181
193
  system_message, user_message, images_as_bytes, self.model_name
182
194
  )
183
-
195
+ result = None
184
196
  if response_model:
185
197
  try:
186
- # "Trying structured output handler")
187
- return await self.structured_output_handler.call_async(
198
+ print("Trying structured output handler")
199
+ result = await self.structured_output_handler.call_async(
188
200
  messages,
189
201
  model=self.model_name,
190
202
  lm_config=self.lm_config,
191
203
  response_model=response_model,
192
204
  use_ephemeral_cache_only=use_ephemeral_cache_only,
205
+ reasoning_effort=reasoning_effort,
193
206
  )
194
207
  except StructuredOutputCoercionFailureException:
195
- # print("Falling back to backup handler")
196
- return await self.backup_structured_output_handler.call_async(
208
+ print("Falling back to backup handler")
209
+ result = await self.backup_structured_output_handler.call_async(
197
210
  messages,
198
211
  model=self.model_name,
199
212
  lm_config=self.lm_config,
200
213
  response_model=response_model,
201
214
  use_ephemeral_cache_only=use_ephemeral_cache_only,
215
+ reasoning_effort=reasoning_effort,
202
216
  )
203
217
  else:
204
- # print("Calling API no response model")
205
- return await self.client._hit_api_async(
218
+ #print("Calling API no response model")
219
+ result = await self.client._hit_api_async(
206
220
  messages=messages,
207
221
  model=self.model_name,
208
222
  lm_config=self.lm_config,
209
223
  use_ephemeral_cache_only=use_ephemeral_cache_only,
224
+ tools=tools,
225
+ reasoning_effort=reasoning_effort,
210
226
  )
211
-
227
+ assert isinstance(result.raw_response, str), "Raw response must be a string"
228
+ assert (isinstance(result.structured_output, BaseModel) or result.structured_output is None), "Structured output must be a Pydantic model or None"
229
+ assert (isinstance(result.tool_calls, list) or result.tool_calls is None), "Tool calls must be a list or None"
230
+ return result
212
231
 
213
232
  if __name__ == "__main__":
214
233
  import asyncio
@@ -5,15 +5,15 @@ from synth_ai.zyk.lms.core.all import (
5
5
  AnthropicClient,
6
6
  DeepSeekClient,
7
7
  GeminiClient,
8
+ GroqAPI,
9
+ MistralAPI,
8
10
  # OpenAIClient,
9
11
  OpenAIStructuredOutputClient,
10
12
  TogetherClient,
11
- GroqAPI,
12
- MistralAPI,
13
13
  )
14
14
 
15
15
  openai_naming_regexes: List[Pattern] = [
16
- re.compile(r"^(ft:)?(o[1,3](-.*)?|gpt-.*)$"),
16
+ re.compile(r"^(ft:)?(o[1,3,4](-.*)?|gpt-.*)$"),
17
17
  ]
18
18
  openai_formatting_model_regexes: List[Pattern] = [
19
19
  re.compile(r"^(ft:)?gpt-4o(-.*)?$"),
@@ -23,6 +23,7 @@ anthropic_naming_regexes: List[Pattern] = [
23
23
  ]
24
24
  gemini_naming_regexes: List[Pattern] = [
25
25
  re.compile(r"^gemini-.*$"),
26
+ re.compile(r"^gemma[2-9].*$"),
26
27
  ]
27
28
  deepseek_naming_regexes: List[Pattern] = [
28
29
  re.compile(r"^deepseek-.*$"),