-
-
Notifications
You must be signed in to change notification settings - Fork 276
Add Requesty as an AI integration provider #212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -126,6 +126,11 @@ def configure_ruby_llm! | |
| when 'open_router' | ||
| # RubyLLM has native OpenRouter support | ||
| config.openrouter_api_key = @integration.credential(:api_key) | ||
| when 'requesty' | ||
| # Requesty exposes an OpenAI-compatible API, so we point RubyLLM's | ||
| # OpenAI client at the Requesty router base URL. | ||
| config.openai_api_key = @integration.credential(:api_key) | ||
| config.openai_api_base = Integrations::Providers::Requesty::API_BASE_URL | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because To prevent this cross-request configuration pollution, def configure_ruby_llm!
RubyLLM.configure do |config|
# Reset any provider-specific base URLs to prevent cross-request pollution
config.openai_api_base = nil
# ... |
||
| end | ||
| else | ||
| # Fall back to ENV variables | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| module Integrations | ||
| module Providers | ||
| # Requesty integration provider. | ||
| # | ||
| # Provides unified access to 400+ AI models from multiple providers | ||
| # (Anthropic, OpenAI, Google, DeepSeek, etc.) through a single | ||
| # OpenAI-compatible API. | ||
| # | ||
| # Requesty exposes an OpenAI-compatible API, so we configure RubyLLM | ||
| # with the Requesty API key and base URL. | ||
| # | ||
| # Required credentials: | ||
| # - api_key: Requesty API key from app.requesty.ai/api-keys | ||
| # | ||
| # Settings: | ||
| # - default_model: Which model to use (in provider/model format) | ||
| # - max_tokens: Maximum tokens in response | ||
| # | ||
| class Requesty < Base | ||
| self.category = :ai | ||
| self.display_name = 'Requesty' | ||
| self.description = 'Access 400+ AI models from multiple providers through a single OpenAI-compatible API' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Metrics/LineLength: Line is too long. [111/80] |
||
|
|
||
| API_BASE_URL = 'https://router.requesty.ai/v1' | ||
|
|
||
| # Popular models available through Requesty | ||
| # Format: [Display Name, provider/model-id] | ||
| AVAILABLE_MODELS = [ | ||
| ['Claude Sonnet 4.5 (Anthropic)', 'anthropic/claude-sonnet-4-5'], | ||
| ['GPT-4o (OpenAI)', 'openai/gpt-4o'], | ||
| ['GPT-4o Mini (OpenAI)', 'openai/gpt-4o-mini'], | ||
| ['GPT-4.1 (OpenAI)', 'openai/gpt-4.1'], | ||
| ['Gemini 2.5 Flash (Google)', 'google/gemini-2.5-flash'], | ||
| ['Gemini 2.5 Pro (Google)', 'google/gemini-2.5-pro'], | ||
| ['DeepSeek Chat (DeepSeek)', 'deepseek/deepseek-chat'] | ||
| ].freeze | ||
|
|
||
| credential_field :api_key, | ||
| required: true, | ||
| label: 'API Key', | ||
| help: 'Get your API key from app.requesty.ai/api-keys' | ||
|
|
||
| setting_field :default_model, | ||
| type: :select, | ||
| options: AVAILABLE_MODELS, | ||
| default: 'openai/gpt-4o-mini', | ||
| label: 'Default Model', | ||
| help: 'The model to use for content generation' | ||
|
|
||
| setting_field :max_tokens, | ||
| type: :number, | ||
| default: 4096, | ||
| label: 'Max Tokens', | ||
| help: 'Maximum number of tokens in generated responses' | ||
|
|
||
| def validate_connection | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Metrics/AbcSize: Assignment Branch Condition size for validate_connection is too high. [18.92/15] |
||
| unless credentials_valid? | ||
| errors.add(:base, 'API key is required') | ||
| return false | ||
| end | ||
|
|
||
| # Validate by fetching the models list from Requesty | ||
| response = Faraday.get("#{API_BASE_URL}/models") do |req| | ||
| req.headers['Authorization'] = "Bearer #{credential(:api_key)}" | ||
| req.options.timeout = 10 | ||
| end | ||
|
|
||
| if response.status == 401 | ||
| errors.add(:base, 'Invalid API key') | ||
| return false | ||
| end | ||
|
|
||
| response.success? | ||
| rescue Faraday::Error => e | ||
| errors.add(:base, "Connection failed: #{e.message}") | ||
| false | ||
| rescue StandardError => e | ||
| errors.add(:base, "Connection failed: #{e.message}") | ||
| false | ||
| end | ||
|
Comment on lines
+58
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improve the connection validation by:
def validate_connection
unless credentials_valid?
errors.add(:base, 'API key is required')
return false
end
# Validate by fetching the models list from Requesty
response = Faraday.get("#{API_BASE_URL}/models") do |req|
req.headers['Authorization'] = "Bearer #{credential(:api_key)}"
req.options.timeout = 10
end
if response.success?
true
else
error_message = response.status == 401 ? 'Invalid API key' : "API returned status #{response.status}"
errors.add(:base, error_message)
false
end
rescue StandardError => e
errors.add(:base, "Connection failed: #{e.message}")
false
end |
||
| end | ||
| end | ||
| end | ||
|
|
||
| # Register with the integrations registry | ||
| Integrations::Registry.register(:ai, :requesty, Integrations::Providers::Requesty) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Metrics/LineLength: Line is too long. [82/80] |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| # Requesty Integration | ||
|
|
||
| ## Overview | ||
|
|
||
| [Requesty](https://requesty.ai) is an API router that provides unified access to 400+ AI models from multiple providers (Anthropic, OpenAI, Google, DeepSeek, and more) through a single OpenAI-compatible API endpoint. | ||
|
|
||
| ## Why Requesty? | ||
|
|
||
| | Benefit | Description | | ||
| |---------|-------------| | ||
| | **Model Variety** | Access Claude, GPT-4o, Gemini, DeepSeek, and 400+ other models | | ||
| | **Single API Key** | One API key for all providers | | ||
| | **Cost Optimization** | Compare pricing across providers, pay-as-you-go | | ||
| | **Automatic Fallbacks** | Route to backup models if primary is unavailable | | ||
| | **No Provider Lock-in** | Switch models without code changes | | ||
|
|
||
| ## Configuration | ||
|
|
||
| ### Site Admin Setup | ||
|
|
||
| 1. Navigate to **Site Admin > Integrations** | ||
| 2. Click **Configure** next to Requesty | ||
| 3. Enter your API key from [app.requesty.ai/api-keys](https://app.requesty.ai/api-keys) | ||
| 4. Select a default model | ||
| 5. Click **Save** | ||
|
|
||
| ### Credentials | ||
|
|
||
| | Field | Required | Description | | ||
| |-------|----------|-------------| | ||
| | API Key | Yes | Your Requesty API key | | ||
|
|
||
| ### Settings | ||
|
|
||
| | Field | Type | Default | Description | | ||
| |-------|------|---------|-------------| | ||
| | Default Model | Select | `openai/gpt-4o-mini` | Model used for AI generation | | ||
| | Max Tokens | Number | 4096 | Maximum response length | | ||
|
|
||
| ## Available Models | ||
|
|
||
| Requesty provides access to models in the format `provider/model-name`: | ||
|
|
||
| ### Recommended Models | ||
|
|
||
| | Model | Provider | Best For | | ||
| |-------|----------|----------| | ||
| | `anthropic/claude-sonnet-4-5` | Anthropic | General purpose, good balance of quality/cost | | ||
| | `openai/gpt-4o` | OpenAI | Fast, multimodal | | ||
| | `openai/gpt-4o-mini` | OpenAI | Cost-effective, good quality | | ||
| | `openai/gpt-4.1` | OpenAI | High quality, large context | | ||
| | `google/gemini-2.5-flash` | Google | Fast, multimodal | | ||
| | `google/gemini-2.5-pro` | Google | Long context, multimodal | | ||
| | `deepseek/deepseek-chat` | DeepSeek | Cost-effective, strong reasoning | | ||
|
|
||
| ### Full Model List | ||
|
|
||
| See [requesty.ai](https://requesty.ai) for the complete list of available models with pricing. | ||
|
|
||
| ## Technical Implementation | ||
|
|
||
| ### API Compatibility | ||
|
|
||
| Requesty uses an **OpenAI-compatible API**, which means: | ||
| - Same request/response format as OpenAI | ||
| - Uses `https://router.requesty.ai/v1` as the base URL | ||
| - Works with existing OpenAI client libraries | ||
|
|
||
| ### RubyLLM Configuration | ||
|
|
||
| PropertyWebBuilder uses RubyLLM for AI interactions. Requesty integration works by: | ||
|
|
||
| ```ruby | ||
| RubyLLM.configure do |config| | ||
| config.openai_api_key = requesty_api_key | ||
| config.openai_api_base = 'https://router.requesty.ai/v1' | ||
| end | ||
| ``` | ||
|
|
||
| ### Service Layer Integration | ||
|
|
||
| The `Ai::BaseService` automatically handles Requesty configuration: | ||
|
|
||
| ```ruby | ||
| # In app/services/ai/base_service.rb | ||
| case @integration.provider | ||
| when 'requesty' | ||
| config.openai_api_key = @integration.credential(:api_key) | ||
| config.openai_api_base = 'https://router.requesty.ai/v1' | ||
| end | ||
| ``` | ||
|
|
||
| ## Usage in AI Services | ||
|
|
||
| All AI services automatically use the configured provider: | ||
|
|
||
| ```ruby | ||
| # Listing Description Generation | ||
| result = Ai::ListingDescriptionGenerator.new( | ||
| property: property, | ||
| locale: 'en', | ||
| tone: 'professional' | ||
| ).generate | ||
|
|
||
| # Social Post Generation | ||
| result = Ai::SocialPostGenerator.new( | ||
| property: property, | ||
| platforms: [:instagram, :facebook], | ||
| category: :just_listed | ||
| ).generate | ||
| ``` | ||
|
|
||
| The service will use Requesty if it's the configured AI integration for the website. | ||
|
|
||
| ## Cost Tracking | ||
|
|
||
| Requesty usage is tracked in `pwb_ai_generation_requests`: | ||
| - `ai_provider`: 'requesty' | ||
| - `ai_model`: The specific model used (e.g., 'openai/gpt-4o-mini') | ||
| - `input_tokens`: Tokens in the prompt | ||
| - `output_tokens`: Tokens in the response | ||
| - `cost_cents`: Calculated cost based on model pricing | ||
|
|
||
| ## Error Handling | ||
|
|
||
| | Error | Cause | Resolution | | ||
| |-------|-------|------------| | ||
| | `401 Unauthorized` | Invalid API key | Check API key in Site Admin | | ||
| | `402 Payment Required` | Insufficient credits | Add credits at app.requesty.ai | | ||
| | `429 Rate Limited` | Too many requests | Automatic retry with backoff | | ||
| | `503 Model Unavailable` | Model temporarily down | Requesty auto-routes to fallback | | ||
|
|
||
| ## Testing | ||
|
|
||
| ### Factory Trait | ||
|
|
||
| ```ruby | ||
| # In specs | ||
| let!(:integration) { create(:pwb_website_integration, :requesty, website: website) } | ||
| ``` | ||
|
|
||
| ### Connection Validation | ||
|
|
||
| ```ruby | ||
| # Test connection in Site Admin | ||
| POST /site_admin/integrations/:id/test_connection | ||
| ``` | ||
|
|
||
| ## Multi-Tenant Support | ||
|
|
||
| Each website can have its own Requesty configuration: | ||
| - Separate API keys per website | ||
| - Different default models per website | ||
| - Independent usage tracking | ||
| - Isolated error states | ||
|
|
||
| ## Security Considerations | ||
|
|
||
| 1. **API Key Storage**: Encrypted at rest in `pwb_website_integrations.credentials` | ||
| 2. **Data Transit**: All requests use HTTPS | ||
| 3. **Data Processing**: Requests routed through Requesty's servers | ||
| 4. **Compliance**: Review Requesty's privacy policy for data handling | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| ### "AI is not configured" Error | ||
|
|
||
| 1. Check integration is enabled in Site Admin > Integrations | ||
| 2. Verify API key is set correctly | ||
| 3. Test connection using the "Test Connection" button | ||
|
|
||
| ### Model Not Found | ||
|
|
||
| 1. Verify model name format: `provider/model-name` | ||
| 2. Check model availability at requesty.ai | ||
| 3. Ensure sufficient credits for the model | ||
|
|
||
| ### Slow Responses | ||
|
|
||
| 1. Consider using a faster model (e.g., `openai/gpt-4o-mini`) | ||
| 2. Reduce `max_tokens` setting | ||
| 3. Check Requesty status page for outages | ||
|
|
||
| ## Related Documentation | ||
|
|
||
| - [AI Features Overview](./README.md) | ||
| - [OpenRouter Integration](./openrouter_integration.md) | ||
| - [Integrations System](../architecture/integrations.md) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,6 +60,13 @@ | |
| settings { { 'default_model' => 'anthropic/claude-3.5-sonnet', 'max_tokens' => 4096 } } | ||
| end | ||
|
|
||
| trait :requesty do | ||
| category { 'ai' } | ||
| provider { 'requesty' } | ||
| credentials { { 'api_key' => 'sk-rq-...2345' } } | ||
| settings { { 'default_model' => 'openai/gpt-4o-mini', 'max_tokens' => 4096 } } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Metrics/LineLength: Line is too long. [84/80] |
||
| end | ||
|
|
||
| trait :spp do | ||
| category { 'spp' } | ||
| provider { 'single_property_pages' } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Metrics/LineLength: Line is too long. [84/80]