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 app/services/ai/base_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

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]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

Because RubyLLM.configure modifies global configuration state, setting config.openai_api_base here will persist across requests. In a multi-tenant environment, subsequent requests that use the standard openai provider or fall back to environment variables will incorrectly route their requests to Requesty because openai_api_base is never reset.

To prevent this cross-request configuration pollution, config.openai_api_base should be explicitly reset to nil (or its default) when configuring other providers or at the start of the configure_ruby_llm! block, for example:

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
Expand Down
88 changes: 88 additions & 0 deletions app/services/integrations/providers/requesty.rb
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'

Copy link
Copy Markdown

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. [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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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]
Metrics/MethodLength: Method has too many lines. [19/10]

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Improve the connection validation by:

  1. Ensuring that any non-success response (e.g., 403, 429, 500) adds a descriptive error message to errors instead of failing silently with an empty error message.
  2. Removing the redundant rescue Faraday::Error block, as Faraday::Error is a subclass of StandardError and is already caught by the subsequent block.
      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)

Copy link
Copy Markdown

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. [82/80]

188 changes: 188 additions & 0 deletions docs/ai_features/requesty_integration.md
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)
7 changes: 7 additions & 0 deletions spec/factories/pwb_website_integrations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }

Copy link
Copy Markdown

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]

end

trait :spp do
category { 'spp' }
provider { 'single_property_pages' }
Expand Down
Loading