Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ AGENT_DEBUG_MODE=false
# Note: OpenRouter does not currently support embedding or reranker models.
OPENROUTER_API_KEY=

# Get your API key from: https://app.requesty.ai/api-keys
# Requesty is an OpenAI-compatible LLM router; uses the same provider/model id
# convention as OpenRouter (e.g. openai/gpt-4o-mini).
REQUESTY_API_KEY=

# Get your API key from: https://ai.azure.com/
AZURE_OPENAI_API_KEY=
AZURE_OPENAI_ENDPOINT=
Expand Down
18 changes: 12 additions & 6 deletions docs/CONFIGURATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ ValueCell supports multiple LLM providers. Choose at least one:
| Provider | Sign Up |
| --------------- | --------------------------------------------------- |
| **OpenRouter** | [openrouter.ai](https://openrouter.ai/) |
| **Requesty** | [app.requesty.ai/api-keys](https://app.requesty.ai/api-keys) |
| **SiliconFlow** | [siliconflow.cn](https://www.siliconflow.cn/) |
| **Google** | [ai.google.dev](https://ai.google.dev/) |
| **OpenAI** | [platform.openai.com](https://platform.openai.com/) |
Expand Down Expand Up @@ -86,6 +87,7 @@ python/
│ ├── config.{environment}.yaml # Environment-specific overrides
│ ├── providers/
│ │ ├── openrouter.yaml # OpenRouter provider config
│ │ ├── requesty.yaml # Requesty provider config
│ │ ├── siliconflow.yaml # SiliconFlow provider config
│ │ ├── dashscope.yaml # DashScope (Alibaba Cloud) provider config
│ │ └── other_provider.yaml
Expand Down Expand Up @@ -191,6 +193,9 @@ models:
openrouter:
config_file: "providers/openrouter.yaml"
api_key_env: "OPENROUTER_API_KEY"
requesty:
config_file: "providers/requesty.yaml"
api_key_env: "REQUESTY_API_KEY"
siliconflow:
config_file: "providers/siliconflow.yaml"
api_key_env: "SILICONFLOW_API_KEY"
Expand Down Expand Up @@ -327,12 +332,13 @@ ValueCell automatically selects a primary provider based on available API keys:
The selection logic is implemented in `python/valuecell/config/manager.py`:

1. OpenRouter
2. SiliconFlow
3. Google
4. OpenAI
5. OpenAI-Compatible
6. Azure
7. Other configured providers (including DashScope, DeepSeek, etc.)
2. Requesty
3. SiliconFlow
4. Google
5. OpenAI
6. OpenAI-Compatible
7. Azure
8. Other configured providers (including DashScope, DeepSeek, etc.)

Override this with an environment variable:

Expand Down
4 changes: 4 additions & 0 deletions python/configs/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ models:
config_file: "providers/openrouter.yaml"
api_key_env: "OPENROUTER_API_KEY"

requesty:
config_file: "providers/requesty.yaml"
api_key_env: "REQUESTY_API_KEY"

siliconflow:
config_file: "providers/siliconflow.yaml"
api_key_env: "SILICONFLOW_API_KEY"
Expand Down
58 changes: 58 additions & 0 deletions python/configs/providers/requesty.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# ============================================
# Requesty Provider Configuration
# ============================================
name: "Requesty"
provider_type: "requesty"

enabled: true # Default is true if not specified

# Connection Configuration
connection:
base_url: "https://router.requesty.ai/v1"
api_key_env: "REQUESTY_API_KEY"

# Default model if none specified
default_model: "openai/gpt-4o-mini"

# Model Parameters Defaults
defaults:
temperature: 0.5

# Extra headers for Requesty API (used for analytics attribution)
extra_headers:
HTTP-Referer: "https://valuecell.ai"
X-Title: "ValueCell"

# Available Models (commonly used)
models:
- id: "anthropic/claude-sonnet-4-5"
name: "Claude Sonnet 4.5"
- id: "openai/gpt-4o"
name: "GPT-4o"
- id: "openai/gpt-4o-mini"
name: "GPT-4o mini"
- id: "deepseek/deepseek-chat"
name: "DeepSeek Chat"
- id: "google/gemini-2.5-flash"
name: "Gemini 2.5 Flash"

# ============================================
# Embedding Models Configuration
# ============================================
# Requesty exposes OpenAI-compatible embedding models
embedding:
# Default embedding model
default_model: "openai/text-embedding-3-small"

# Default parameters
defaults:
dimensions: 1536
encoding_format: "float"

# Available embedding models
models:
- id: "openai/text-embedding-3-small"
name: "OpenAI Text Embedding 3 Small"
dimensions: 1536
max_input: 8192
description: "OpenAI Text Embedding Model"
77 changes: 77 additions & 0 deletions python/valuecell/adapters/models/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,82 @@ def create_model(self, model_id: Optional[str] = None, **kwargs):
)


class RequestyProvider(ModelProvider):
"""Requesty model provider

Requesty (https://requesty.ai) is an OpenAI-compatible LLM router. It uses the
same ``provider/model`` id convention as OpenRouter (e.g. ``openai/gpt-4o-mini``)
and the same ``HTTP-Referer`` / ``X-Title`` analytics headers, with a fixed
OpenAI-compatible base URL (``https://router.requesty.ai/v1``). This mirrors
OpenRouterProvider, using agno's OpenAILike client since the endpoint is
OpenAI-compatible.
"""

def create_model(self, model_id: Optional[str] = None, **kwargs):
"""Create Requesty model via agno (OpenAI-compatible)"""
try:
from agno.models.openai import OpenAILike
except ImportError:
raise ImportError(
"agno package not installed. Install with: pip install agno"
)

# Use provided model_id or default
model_id = model_id or self.config.default_model

# Merge parameters: provider defaults < kwargs
params = {**self.config.parameters, **kwargs}

# Get extra headers from config (analytics attribution)
extra_headers = self.config.extra_config.get("extra_headers", {})

logger.info(f"Creating Requesty model: {model_id}")

return OpenAILike(
id=model_id,
api_key=self.config.api_key,
base_url=self.config.base_url,
default_headers=extra_headers if extra_headers else None,
temperature=params.get("temperature"),
max_tokens=params.get("max_tokens"),
top_p=params.get("top_p"),
frequency_penalty=params.get("frequency_penalty"),
presence_penalty=params.get("presence_penalty"),
)

def create_embedder(self, model_id: Optional[str] = None, **kwargs):
"""Create embedder via Requesty (OpenAI-compatible)"""
try:
from agno.knowledge.embedder.openai import OpenAIEmbedder
except ImportError:
raise ImportError("agno package not installed")

# Use provided model_id or default embedding model
model_id = model_id or self.config.default_embedding_model

if not model_id:
raise ValueError(
f"No embedding model specified for provider '{self.config.name}'"
)

# Merge parameters: provider embedding defaults < kwargs
params = {**self.config.embedding_parameters, **kwargs}

logger.info(f"Creating Requesty embedder: {model_id}")

return OpenAIEmbedder(
id=model_id,
api_key=self.config.api_key,
base_url=self.config.base_url,
dimensions=int(params.get("dimensions", 1536)),
encoding_format=params.get("encoding_format"),
)

def is_available(self) -> bool:
"""Check if provider is available (needs both API key and base URL)"""
return bool(self.config.api_key and self.config.base_url)


class GoogleProvider(ModelProvider):
"""Google Gemini model provider"""

Expand Down Expand Up @@ -601,6 +677,7 @@ class ModelFactory:
# Registry of provider classes
_providers: Dict[str, type[ModelProvider]] = {
"openrouter": OpenRouterProvider,
"requesty": RequestyProvider,
"google": GoogleProvider,
"azure": AzureProvider,
"siliconflow": SiliconFlowProvider,
Expand Down
1 change: 1 addition & 0 deletions python/valuecell/config/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ def primary_provider(self) -> str:
# Priority order for auto-selection
preferred_order = [
"openrouter",
"requesty",
"siliconflow",
"google",
"openai",
Expand Down
Empty file.
65 changes: 65 additions & 0 deletions python/valuecell/config/tests/test_provider_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Tests for provider configuration loading.

Validates that config-driven providers (openrouter, requesty) load through the
shared ConfigLoader / ConfigManager path with the expected connection settings.
"""

import os

import pytest

from valuecell.config.loader import ConfigLoader
from valuecell.config.manager import ConfigManager


@pytest.fixture()
def manager() -> ConfigManager:
loader = ConfigLoader()
return ConfigManager(loader=loader)


def test_requesty_provider_is_discovered():
"""requesty.yaml is picked up by the provider glob, like openrouter.yaml."""
loader = ConfigLoader()
providers = loader.list_providers()
assert "openrouter" in providers
assert "requesty" in providers


def test_requesty_provider_config_matches_expected(manager: ConfigManager):
"""Requesty loads through the same code path as OpenRouter with its
fixed OpenAI-compatible base URL, API-key env var, and analytics headers."""
config = manager.get_provider_config("requesty")

assert config is not None
assert config.name == "requesty"
assert config.enabled is True
assert config.base_url == "https://router.requesty.ai/v1"
assert config.default_model == "openai/gpt-4o-mini"

# provider/model id convention, mirroring OpenRouter
model_ids = {m["id"] for m in config.models}
assert "openai/gpt-4o-mini" in model_ids
assert all("/" in mid for mid in model_ids)

# Analytics headers mirror OpenRouter's HTTP-Referer / X-Title
extra_headers = config.extra_config.get("extra_headers", {})
assert extra_headers.get("X-Title") == "ValueCell"
assert "HTTP-Referer" in extra_headers


def test_requesty_reads_api_key_env(manager: ConfigManager, monkeypatch):
"""REQUESTY_API_KEY is resolved into the provider config."""
monkeypatch.setenv("REQUESTY_API_KEY", "sk-test-requesty")
# Fresh manager to avoid loader cache picking up an earlier (empty) value
fresh = ConfigManager(loader=ConfigLoader())
config = fresh.get_provider_config("requesty")
assert config is not None
assert config.api_key == "sk-test-requesty"


def test_requesty_registered_in_factory():
"""The requesty provider name maps to a provider class in the factory."""
from valuecell.adapters.models.factory import ModelFactory, RequestyProvider

assert ModelFactory._providers.get("requesty") is RequestyProvider