Skip to content

Commit 97251b0

Browse files
v31.5.0 (#136)
1 parent ac49375 commit 97251b0

File tree

7 files changed

+2251
-1922
lines changed

7 files changed

+2251
-1922
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ Smart workflows are as easy as combining your tools and prompts.
8787
* [Python](https://python.org) - Programming Language
8888
* [OpenAI](https://openai.com) - AI Model Provider
8989
* [Grok](https://x.ai) - Alternative AI Model Provider (optional)
90+
* [Groq](https://groq.com) - Alternative AI Model Provider (optional)
9091
* [MongoDB](https://mongodb.com) - Conversational History (optional)
9192
* [Zep Cloud](https://getzep.com) - Conversational Memory (optional)
9293
* [Pinecone](https://pinecone.io) - Knowledge Base (optional)
@@ -571,6 +572,40 @@ config = {
571572
- ✅ Structured outputs (via Instructor TOOLS_STRICT and JSON modes)
572573
- ✅ Native JSON mode
573574

575+
### Groq
576+
577+
Solana Agent supports using Groq as an alternative to OpenAI. When Groq is configured, it will be used for all LLM operations except embeddings, TTS, and STT (which still require OpenAI).
578+
579+
**Note:** Grok configuration takes priority over Groq, and Groq takes priority over OpenAI. If multiple are present, the highest priority provider will be used.
580+
581+
```python
582+
config = {
583+
"groq": {
584+
"api_key": "your-groq-api-key",
585+
"base_url": "https://api.groq.com/openai/v1", # Optional, defaults to https://api.groq.com/openai/v1
586+
"model": "openai/gpt-oss-120b" # Optional, defaults to openai/gpt-oss-120b
587+
},
588+
# You can still include OpenAI for embeddings, TTS, and STT
589+
"openai": {
590+
"api_key": "your-openai-api-key"
591+
},
592+
"agents": [
593+
{
594+
"name": "research_specialist",
595+
"instructions": "You are an expert researcher.",
596+
"specialization": "Research",
597+
}
598+
],
599+
}
600+
```
601+
602+
**Verified Capabilities:**
603+
- ✅ Chat completions
604+
- ✅ Streaming responses
605+
- ✅ Function calling/Tool usage
606+
- ✅ Structured outputs (via Instructor TOOLS_STRICT and JSON modes)
607+
- ✅ Native JSON mode
608+
574609
### Business Alignment
575610

576611
```python

poetry.lock

Lines changed: 2078 additions & 1905 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "solana-agent"
3-
version = "31.4.0"
3+
version = "31.5.0"
44
description = "AI Agents for Solana"
55
authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
66
license = "MIT"
@@ -24,17 +24,17 @@ testpaths = ["tests"]
2424

2525
[tool.poetry.dependencies]
2626
python = ">=3.12,<4.0"
27-
openai = "2.8.1"
27+
openai = "2.9.0"
2828
pydantic = ">=2"
29-
pymongo = "4.15.4"
29+
pymongo = "4.15.5"
3030
zep-cloud = "3.13.0"
3131
instructor = "1.13.0"
3232
pinecone = { version = "8.0.0", extras = ["asyncio"] }
33-
llama-index-core = "0.14.8"
33+
llama-index-core = "0.14.10"
3434
llama-index-embeddings-openai = "0.5.1"
3535
pypdf = "6.4.0"
3636
scrubadub = "2.0.1"
37-
logfire = "4.15.1"
37+
logfire = "4.16.0"
3838
typer = "0.20.0"
3939
rich = ">=13,<14.0"
4040
pillow = "12.0.0"

solana_agent/adapters/openai_adapter.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def __init__(
6565
self.api_key = api_key
6666
self.base_url = base_url
6767

68-
# Create client with base_url if provided (for Grok support)
68+
# Create client with base_url if provided (for Grok/Groq support)
6969
if base_url:
7070
self.client = AsyncOpenAI(api_key=api_key, base_url=base_url)
7171
else:
@@ -85,9 +85,9 @@ def __init__(
8585
logger.error(f"Failed to configure Logfire: {e}")
8686
self.logfire = False
8787

88-
# Use provided model or defaults (for Grok or OpenAI)
88+
# Use provided model or defaults (for Grok/Groq or OpenAI)
8989
if model:
90-
# Custom model provided (e.g., from Grok config)
90+
# Custom model provided (e.g., from Grok or Groq config)
9191
self.parse_model = model
9292
self.text_model = model
9393
self.vision_model = model

solana_agent/factories/agent_factory.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ def create_from_config(config: Dict[str, Any]) -> QueryService: # pragma: no co
104104
else:
105105
db_adapter = None
106106

107-
# Determine which LLM provider to use (Grok or OpenAI)
108-
# Priority: grok > openai
107+
# Determine which LLM provider to use (Grok, Groq, or OpenAI)
108+
# Priority: grok > groq > openai
109109
llm_api_key = None
110110
llm_base_url = None
111111
llm_model = None
@@ -115,13 +115,22 @@ def create_from_config(config: Dict[str, Any]) -> QueryService: # pragma: no co
115115
llm_base_url = config["grok"].get("base_url", "https://api.x.ai/v1")
116116
llm_model = config["grok"].get("model", "grok-4-1-fast-non-reasoning")
117117
logger.info(f"Using Grok as LLM provider with model: {llm_model}")
118+
elif "groq" in config and "api_key" in config["groq"]:
119+
llm_api_key = config["groq"]["api_key"]
120+
llm_base_url = config["groq"].get(
121+
"base_url", "https://api.groq.com/openai/v1"
122+
)
123+
llm_model = config["groq"].get("model", "openai/gpt-oss-120b")
124+
logger.info(f"Using Groq as LLM provider with model: {llm_model}")
118125
elif "openai" in config and "api_key" in config["openai"]:
119126
llm_api_key = config["openai"]["api_key"]
120127
llm_base_url = None # Use default OpenAI endpoint
121128
llm_model = None # Will use OpenAI adapter defaults
122129
logger.info("Using OpenAI as LLM provider")
123130
else:
124-
raise ValueError("Either OpenAI or Grok API key is required in config.")
131+
raise ValueError(
132+
"Either OpenAI, Grok, or Groq API key is required in config."
133+
)
125134

126135
if "logfire" in config:
127136
if "api_key" not in config["logfire"]:
@@ -198,10 +207,10 @@ def create_from_config(config: Dict[str, Any]) -> QueryService: # pragma: no co
198207
)
199208

200209
# Create routing service
201-
# Use Grok model if configured, otherwise check for OpenAI routing_model override
210+
# Use Grok/Groq model if configured, otherwise check for OpenAI routing_model override
202211
routing_model = llm_model # Use the same model as the main LLM by default
203212
if not routing_model:
204-
# Fall back to OpenAI routing_model config if no Grok model
213+
# Fall back to OpenAI routing_model config if no Grok/Groq model
205214
routing_model = (
206215
config.get("openai", {}).get("routing_model")
207216
if isinstance(config.get("openai"), dict)

solana_agent/services/routing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def __init__(
3939
if model:
4040
self.model = model
4141
elif base_url:
42-
# Using custom provider (e.g., Grok) but no model specified - use provider's default
42+
# Using custom provider (e.g., Grok or Groq) but no model specified - use provider's default
4343
self.model = None # Will use adapter's default
4444
else:
4545
# Using OpenAI - default to small, cheap model for routing

tests/unit/factories/test_agent_factory.py

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,33 @@ def grok_with_logfire_config(grok_config):
223223
return config
224224

225225

226+
@pytest.fixture
227+
def groq_config():
228+
"""Config with Groq as the LLM provider."""
229+
return {
230+
"groq": {
231+
"api_key": "test-groq-key",
232+
"base_url": "https://api.groq.com/openai/v1",
233+
"model": "llama-3.3-70b-versatile",
234+
},
235+
"agents": [
236+
{
237+
"name": "test_agent",
238+
"instructions": "You are a test agent.",
239+
"specialization": "Testing",
240+
}
241+
],
242+
}
243+
244+
245+
@pytest.fixture
246+
def groq_with_logfire_config(groq_config):
247+
"""Config with Groq and Logfire enabled."""
248+
config = deepcopy(groq_config)
249+
config["logfire"] = {"api_key": "test-logfire-key"}
250+
return config
251+
252+
226253
@pytest.fixture
227254
def invalid_logfire_config_missing_key(base_config):
228255
"""Config with logfire section missing api_key."""
@@ -294,9 +321,10 @@ def test_create_from_config_minimal(
294321

295322
def test_missing_openai_section(self, config_missing_openai_section):
296323
"""Test factory creation when the entire openai section is missing."""
297-
# This should raise ValueError since OpenAI or Grok API key is required
324+
# This should raise ValueError since OpenAI, Grok, or Groq API key is required
298325
with pytest.raises(
299-
ValueError, match="Either OpenAI or Grok API key is required in config."
326+
ValueError,
327+
match="Either OpenAI, Grok, or Groq API key is required in config.",
300328
):
301329
SolanaAgentFactory.create_from_config(config_missing_openai_section)
302330

@@ -1618,7 +1646,8 @@ def test_logfire_config_missing_openai_key(self, logfire_config_missing_openai):
16181646
"""Test handling of Logfire config when OpenAI key is missing."""
16191647
# Based on the current factory code, this should raise a ValueError
16201648
with pytest.raises(
1621-
ValueError, match="Either OpenAI or Grok API key is required in config."
1649+
ValueError,
1650+
match="Either OpenAI, Grok, or Groq API key is required in config.",
16221651
):
16231652
SolanaAgentFactory.create_from_config(logfire_config_missing_openai)
16241653

@@ -1743,3 +1772,86 @@ def test_create_grok_without_logfire(
17431772
mock_routing_service.assert_called_once()
17441773
mock_query_service.assert_called_once()
17451774
assert result == mock_query_instance
1775+
1776+
@patch("solana_agent.factories.agent_factory.MongoDBAdapter")
1777+
@patch("solana_agent.factories.agent_factory.OpenAIAdapter")
1778+
@patch("solana_agent.factories.agent_factory.AgentService")
1779+
@patch("solana_agent.factories.agent_factory.RoutingService")
1780+
@patch("solana_agent.factories.agent_factory.QueryService")
1781+
def test_create_groq_with_logfire(
1782+
self,
1783+
mock_query_service,
1784+
mock_routing_service,
1785+
mock_agent_service,
1786+
mock_openai_adapter,
1787+
mock_mongo_adapter,
1788+
groq_with_logfire_config,
1789+
):
1790+
"""Test creating services with Groq and Logfire configuration."""
1791+
# Setup mocks
1792+
mock_openai_instance = MagicMock()
1793+
mock_openai_adapter.return_value = mock_openai_instance
1794+
mock_agent_instance = MagicMock()
1795+
mock_agent_service.return_value = mock_agent_instance
1796+
mock_agent_instance.tool_registry.list_all_tools.return_value = []
1797+
mock_routing_instance = MagicMock()
1798+
mock_routing_service.return_value = mock_routing_instance
1799+
mock_query_instance = MagicMock()
1800+
mock_query_service.return_value = mock_query_instance
1801+
1802+
# Call the factory
1803+
result = SolanaAgentFactory.create_from_config(groq_with_logfire_config)
1804+
1805+
# Verify OpenAIAdapter was called with Groq config and logfire key
1806+
mock_openai_adapter.assert_called_once_with(
1807+
api_key="test-groq-key",
1808+
base_url="https://api.groq.com/openai/v1",
1809+
model="llama-3.3-70b-versatile",
1810+
logfire_api_key="test-logfire-key",
1811+
)
1812+
# Verify other services were called
1813+
mock_agent_service.assert_called_once()
1814+
mock_routing_service.assert_called_once()
1815+
mock_query_service.assert_called_once()
1816+
assert result == mock_query_instance
1817+
1818+
@patch("solana_agent.factories.agent_factory.MongoDBAdapter")
1819+
@patch("solana_agent.factories.agent_factory.OpenAIAdapter")
1820+
@patch("solana_agent.factories.agent_factory.AgentService")
1821+
@patch("solana_agent.factories.agent_factory.RoutingService")
1822+
@patch("solana_agent.factories.agent_factory.QueryService")
1823+
def test_create_groq_without_logfire(
1824+
self,
1825+
mock_query_service,
1826+
mock_routing_service,
1827+
mock_agent_service,
1828+
mock_openai_adapter,
1829+
mock_mongo_adapter,
1830+
groq_config,
1831+
):
1832+
"""Test creating services with Groq but no Logfire configuration."""
1833+
# Setup mocks
1834+
mock_openai_instance = MagicMock()
1835+
mock_openai_adapter.return_value = mock_openai_instance
1836+
mock_agent_instance = MagicMock()
1837+
mock_agent_service.return_value = mock_agent_instance
1838+
mock_agent_instance.tool_registry.list_all_tools.return_value = []
1839+
mock_routing_instance = MagicMock()
1840+
mock_routing_service.return_value = mock_routing_instance
1841+
mock_query_instance = MagicMock()
1842+
mock_query_service.return_value = mock_query_instance
1843+
1844+
# Call the factory
1845+
result = SolanaAgentFactory.create_from_config(groq_config)
1846+
1847+
# Verify OpenAIAdapter was called with Groq config but no logfire key
1848+
mock_openai_adapter.assert_called_once_with(
1849+
api_key="test-groq-key",
1850+
base_url="https://api.groq.com/openai/v1",
1851+
model="llama-3.3-70b-versatile",
1852+
)
1853+
# Verify other services were called
1854+
mock_agent_service.assert_called_once()
1855+
mock_routing_service.assert_called_once()
1856+
mock_query_service.assert_called_once()
1857+
assert result == mock_query_instance

0 commit comments

Comments
 (0)