Skip to content

Add universal OpenAI-compatible gateway support#43

Open
bigtcze wants to merge 4 commits into
presswizards:mainfrom
bigtcze:main
Open

Add universal OpenAI-compatible gateway support#43
bigtcze wants to merge 4 commits into
presswizards:mainfrom
bigtcze:main

Conversation

@bigtcze
Copy link
Copy Markdown

@bigtcze bigtcze commented Mar 23, 2026

Summary

Reworked the original LiteLLM proxy PR into a universal OpenAI-Compatible Gateway approach, as suggested by @presswizards. Instead of adding LiteLLM as a named option, this adds a single API Base URL field that works with any OpenAI-compatible gateway or proxy — OpenRouter, PortKey, LiteLLM, BitFrost, or any other.

What changed

  • Merged OpenAI + LiteLLM code paths into one unified Guzzle-based flow — no more Tectalic dependency
  • Added API Base URL field — leave empty for direct OpenAI, set a URL to route through any gateway
  • Smart role handling — uses developer/user roles for direct OpenAI, system role for gateways (universal compatibility)
  • max_tokens instead of max_output_tokens in Chat Completions for broad gateway support
  • Optional API key — local proxies without auth (e.g. LiteLLM on localhost) work without a key
  • /v1 path stripping — prevents URL doubling when users enter https://openrouter.ai/api/v1 (code appends /v1/... itself)
  • Model dropdown fetches from configured endpoint — works with any gateway's /v1/models
  • Removed: LiteLLM-specific UI section, route, controller method, migration, and Tectalic vendor dependency

How it works

Base URL field Behavior
Empty (default) Direct OpenAI — same as before
https://openrouter.ai/api Routes through OpenRouter
http://localhost:4000 Routes through local LiteLLM/proxy
Any OpenAI-compatible URL Just works

Migration

New DB migration adds api_base_url column. No breaking changes for existing OpenAI-direct users.

bigtcze and others added 2 commits March 23, 2026 20:50
Allow routing all API requests through a LiteLLM proxy server,
enabling use of any LLM provider (Anthropic, local models, etc.)
via a single OpenAI-compatible endpoint. Supports both Chat
Completions and Responses API through the proxy. LiteLLM is
mutually exclusive with Infomaniak but works alongside the
Responses API toggle.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 23, 2026

Important

Review skipped

Too many files!

This PR contains 171 files, which is 21 over the limit of 150.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d8b983dc-d844-47d2-887d-c33fa59710b1

📥 Commits

Reviewing files that changed from the base of the PR and between c7952ce and 89d3bae.

📒 Files selected for processing (171)
  • Database/Migrations/2025_07_08_000001_rework_openai_compatible_gateway.php
  • Entities/GPTSettings.php
  • Http/Controllers/FreeScoutGPTController.php
  • Public/js/settings.js
  • Resources/views/settings.blade.php
  • composer.json
  • vendor/tectalic/openai/CHANGELOG.md
  • vendor/tectalic/openai/LICENSE
  • vendor/tectalic/openai/README.md
  • vendor/tectalic/openai/composer.json
  • vendor/tectalic/openai/phpcs.xml.dist
  • vendor/tectalic/openai/phpstan.neon
  • vendor/tectalic/openai/phpunit.xml.dist
  • vendor/tectalic/openai/src/Authentication.php
  • vendor/tectalic/openai/src/Client.php
  • vendor/tectalic/openai/src/ClientException.php
  • vendor/tectalic/openai/src/Handlers/AudioTranscriptions.php
  • vendor/tectalic/openai/src/Handlers/AudioTranslations.php
  • vendor/tectalic/openai/src/Handlers/ChatCompletions.php
  • vendor/tectalic/openai/src/Handlers/Completions.php
  • vendor/tectalic/openai/src/Handlers/Edits.php
  • vendor/tectalic/openai/src/Handlers/Embeddings.php
  • vendor/tectalic/openai/src/Handlers/Files.php
  • vendor/tectalic/openai/src/Handlers/FilesContent.php
  • vendor/tectalic/openai/src/Handlers/FineTunes.php
  • vendor/tectalic/openai/src/Handlers/FineTunesCancel.php
  • vendor/tectalic/openai/src/Handlers/FineTunesEvents.php
  • vendor/tectalic/openai/src/Handlers/ImagesEdits.php
  • vendor/tectalic/openai/src/Handlers/ImagesGenerations.php
  • vendor/tectalic/openai/src/Handlers/ImagesVariations.php
  • vendor/tectalic/openai/src/Handlers/Models.php
  • vendor/tectalic/openai/src/Handlers/Moderations.php
  • vendor/tectalic/openai/src/Manager.php
  • vendor/tectalic/openai/src/Models/AbstractModel.php
  • vendor/tectalic/openai/src/Models/AbstractModelCollection.php
  • vendor/tectalic/openai/src/Models/AudioTranscriptions/CreateRequest.php
  • vendor/tectalic/openai/src/Models/AudioTranscriptions/CreateResponse.php
  • vendor/tectalic/openai/src/Models/AudioTranslations/CreateRequest.php
  • vendor/tectalic/openai/src/Models/AudioTranslations/CreateResponse.php
  • vendor/tectalic/openai/src/Models/ChatCompletions/CreateRequest.php
  • vendor/tectalic/openai/src/Models/ChatCompletions/CreateRequestLogitBias.php
  • vendor/tectalic/openai/src/Models/ChatCompletions/CreateRequestMessagesItem.php
  • vendor/tectalic/openai/src/Models/ChatCompletions/CreateResponse.php
  • vendor/tectalic/openai/src/Models/ChatCompletions/CreateResponseChoicesItem.php
  • vendor/tectalic/openai/src/Models/ChatCompletions/CreateResponseChoicesItemMessage.php
  • vendor/tectalic/openai/src/Models/ChatCompletions/CreateResponseUsage.php
  • vendor/tectalic/openai/src/Models/Completions/CreateRequest.php
  • vendor/tectalic/openai/src/Models/Completions/CreateRequestLogitBias.php
  • vendor/tectalic/openai/src/Models/Completions/CreateResponse.php
  • vendor/tectalic/openai/src/Models/Completions/CreateResponseChoicesItem.php
  • vendor/tectalic/openai/src/Models/Completions/CreateResponseChoicesItemLogprobs.php
  • vendor/tectalic/openai/src/Models/Completions/CreateResponseChoicesItemLogprobsTopLogprobsItem.php
  • vendor/tectalic/openai/src/Models/Completions/CreateResponseUsage.php
  • vendor/tectalic/openai/src/Models/Edits/CreateRequest.php
  • vendor/tectalic/openai/src/Models/Edits/CreateResponse.php
  • vendor/tectalic/openai/src/Models/Edits/CreateResponseChoicesItem.php
  • vendor/tectalic/openai/src/Models/Edits/CreateResponseChoicesItemLogprobs.php
  • vendor/tectalic/openai/src/Models/Edits/CreateResponseChoicesItemLogprobsTopLogprobsItem.php
  • vendor/tectalic/openai/src/Models/Edits/CreateResponseUsage.php
  • vendor/tectalic/openai/src/Models/Embeddings/CreateRequest.php
  • vendor/tectalic/openai/src/Models/Embeddings/CreateResponse.php
  • vendor/tectalic/openai/src/Models/Embeddings/CreateResponseDataItem.php
  • vendor/tectalic/openai/src/Models/Embeddings/CreateResponseUsage.php
  • vendor/tectalic/openai/src/Models/Files/CreateRequest.php
  • vendor/tectalic/openai/src/Models/Files/CreateResponse.php
  • vendor/tectalic/openai/src/Models/Files/CreateResponseStatusDetails.php
  • vendor/tectalic/openai/src/Models/Files/DeleteResponse.php
  • vendor/tectalic/openai/src/Models/Files/ListResponse.php
  • vendor/tectalic/openai/src/Models/Files/ListResponseDataItem.php
  • vendor/tectalic/openai/src/Models/Files/ListResponseDataItemStatusDetails.php
  • vendor/tectalic/openai/src/Models/Files/RetrieveResponse.php
  • vendor/tectalic/openai/src/Models/Files/RetrieveResponseStatusDetails.php
  • vendor/tectalic/openai/src/Models/FilesContent/DownloadResponse.php
  • vendor/tectalic/openai/src/Models/FineTunes/CreateRequest.php
  • vendor/tectalic/openai/src/Models/FineTunes/CreateResponse.php
  • vendor/tectalic/openai/src/Models/FineTunes/CreateResponseEventsItem.php
  • vendor/tectalic/openai/src/Models/FineTunes/CreateResponseHyperparams.php
  • vendor/tectalic/openai/src/Models/FineTunes/CreateResponseResultFilesItem.php
  • vendor/tectalic/openai/src/Models/FineTunes/CreateResponseResultFilesItemStatusDetails.php
  • vendor/tectalic/openai/src/Models/FineTunes/CreateResponseTrainingFilesItem.php
  • vendor/tectalic/openai/src/Models/FineTunes/CreateResponseTrainingFilesItemStatusDetails.php
  • vendor/tectalic/openai/src/Models/FineTunes/CreateResponseValidationFilesItem.php
  • vendor/tectalic/openai/src/Models/FineTunes/CreateResponseValidationFilesItemStatusDetails.php
  • vendor/tectalic/openai/src/Models/FineTunes/ListResponse.php
  • vendor/tectalic/openai/src/Models/FineTunes/ListResponseDataItem.php
  • vendor/tectalic/openai/src/Models/FineTunes/ListResponseDataItemEventsItem.php
  • vendor/tectalic/openai/src/Models/FineTunes/ListResponseDataItemHyperparams.php
  • vendor/tectalic/openai/src/Models/FineTunes/ListResponseDataItemResultFilesItem.php
  • vendor/tectalic/openai/src/Models/FineTunes/ListResponseDataItemResultFilesItemStatusDetails.php
  • vendor/tectalic/openai/src/Models/FineTunes/ListResponseDataItemTrainingFilesItem.php
  • vendor/tectalic/openai/src/Models/FineTunes/ListResponseDataItemTrainingFilesItemStatusDetails.php
  • vendor/tectalic/openai/src/Models/FineTunes/ListResponseDataItemValidationFilesItem.php
  • vendor/tectalic/openai/src/Models/FineTunes/ListResponseDataItemValidationFilesItemStatusDetails.php
  • vendor/tectalic/openai/src/Models/FineTunes/RetrieveResponse.php
  • vendor/tectalic/openai/src/Models/FineTunes/RetrieveResponseEventsItem.php
  • vendor/tectalic/openai/src/Models/FineTunes/RetrieveResponseHyperparams.php
  • vendor/tectalic/openai/src/Models/FineTunes/RetrieveResponseResultFilesItem.php
  • vendor/tectalic/openai/src/Models/FineTunes/RetrieveResponseResultFilesItemStatusDetails.php
  • vendor/tectalic/openai/src/Models/FineTunes/RetrieveResponseTrainingFilesItem.php
  • vendor/tectalic/openai/src/Models/FineTunes/RetrieveResponseTrainingFilesItemStatusDetails.php
  • vendor/tectalic/openai/src/Models/FineTunes/RetrieveResponseValidationFilesItem.php
  • vendor/tectalic/openai/src/Models/FineTunes/RetrieveResponseValidationFilesItemStatusDetails.php
  • vendor/tectalic/openai/src/Models/FineTunesCancel/CancelFineTuneResponse.php
  • vendor/tectalic/openai/src/Models/FineTunesCancel/CancelFineTuneResponseEventsItem.php
  • vendor/tectalic/openai/src/Models/FineTunesCancel/CancelFineTuneResponseHyperparams.php
  • vendor/tectalic/openai/src/Models/FineTunesCancel/CancelFineTuneResponseResultFilesItem.php
  • vendor/tectalic/openai/src/Models/FineTunesCancel/CancelFineTuneResponseResultFilesItemStatusDetails.php
  • vendor/tectalic/openai/src/Models/FineTunesCancel/CancelFineTuneResponseTrainingFilesItem.php
  • vendor/tectalic/openai/src/Models/FineTunesCancel/CancelFineTuneResponseTrainingFilesItemStatusDetails.php
  • vendor/tectalic/openai/src/Models/FineTunesCancel/CancelFineTuneResponseValidationFilesItem.php
  • vendor/tectalic/openai/src/Models/FineTunesCancel/CancelFineTuneResponseValidationFilesItemStatusDetails.php
  • vendor/tectalic/openai/src/Models/FineTunesEvents/ListFineTuneResponse.php
  • vendor/tectalic/openai/src/Models/FineTunesEvents/ListFineTuneResponseDataItem.php
  • vendor/tectalic/openai/src/Models/ImagesEdits/CreateImageRequest.php
  • vendor/tectalic/openai/src/Models/ImagesEdits/CreateImageResponse.php
  • vendor/tectalic/openai/src/Models/ImagesEdits/CreateImageResponseDataItem.php
  • vendor/tectalic/openai/src/Models/ImagesGenerations/CreateRequest.php
  • vendor/tectalic/openai/src/Models/ImagesGenerations/CreateResponse.php
  • vendor/tectalic/openai/src/Models/ImagesGenerations/CreateResponseDataItem.php
  • vendor/tectalic/openai/src/Models/ImagesVariations/CreateImageRequest.php
  • vendor/tectalic/openai/src/Models/ImagesVariations/CreateImageResponse.php
  • vendor/tectalic/openai/src/Models/ImagesVariations/CreateImageResponseDataItem.php
  • vendor/tectalic/openai/src/Models/Models/DeleteResponse.php
  • vendor/tectalic/openai/src/Models/Models/ListResponse.php
  • vendor/tectalic/openai/src/Models/Models/ListResponseDataItem.php
  • vendor/tectalic/openai/src/Models/Models/RetrieveResponse.php
  • vendor/tectalic/openai/src/Models/Moderations/CreateRequest.php
  • vendor/tectalic/openai/src/Models/Moderations/CreateResponse.php
  • vendor/tectalic/openai/src/Models/Moderations/CreateResponseResultsItem.php
  • vendor/tectalic/openai/src/Models/Moderations/CreateResponseResultsItemCategories.php
  • vendor/tectalic/openai/src/Models/Moderations/CreateResponseResultsItemCategoryScores.php
  • vendor/tectalic/openai/src/Models/UnstructuredModel.php
  • vendor/tectalic/openai/tests/AssertValidateTrait.php
  • vendor/tectalic/openai/tests/Integration/Handlers/AudioTranscriptionsTest.php
  • vendor/tectalic/openai/tests/Integration/Handlers/AudioTranslationsTest.php
  • vendor/tectalic/openai/tests/Integration/Handlers/ChatCompletionsTest.php
  • vendor/tectalic/openai/tests/Integration/Handlers/CompletionsTest.php
  • vendor/tectalic/openai/tests/Integration/Handlers/EditsTest.php
  • vendor/tectalic/openai/tests/Integration/Handlers/EmbeddingsTest.php
  • vendor/tectalic/openai/tests/Integration/Handlers/FilesContentTest.php
  • vendor/tectalic/openai/tests/Integration/Handlers/FilesTest.php
  • vendor/tectalic/openai/tests/Integration/Handlers/FineTunesCancelTest.php
  • vendor/tectalic/openai/tests/Integration/Handlers/FineTunesEventsTest.php
  • vendor/tectalic/openai/tests/Integration/Handlers/FineTunesTest.php
  • vendor/tectalic/openai/tests/Integration/Handlers/ImagesEditsTest.php
  • vendor/tectalic/openai/tests/Integration/Handlers/ImagesGenerationsTest.php
  • vendor/tectalic/openai/tests/Integration/Handlers/ImagesVariationsTest.php
  • vendor/tectalic/openai/tests/Integration/Handlers/ModelsTest.php
  • vendor/tectalic/openai/tests/Integration/Handlers/ModerationsTest.php
  • vendor/tectalic/openai/tests/MockUri.php
  • vendor/tectalic/openai/tests/NullAuth.php
  • vendor/tectalic/openai/tests/Unit/AuthenticationTest.php
  • vendor/tectalic/openai/tests/Unit/ClientTest.php
  • vendor/tectalic/openai/tests/Unit/Handlers/AudioTranscriptionsTest.php
  • vendor/tectalic/openai/tests/Unit/Handlers/AudioTranslationsTest.php
  • vendor/tectalic/openai/tests/Unit/Handlers/ChatCompletionsTest.php
  • vendor/tectalic/openai/tests/Unit/Handlers/CompletionsTest.php
  • vendor/tectalic/openai/tests/Unit/Handlers/EditsTest.php
  • vendor/tectalic/openai/tests/Unit/Handlers/EmbeddingsTest.php
  • vendor/tectalic/openai/tests/Unit/Handlers/FilesContentTest.php
  • vendor/tectalic/openai/tests/Unit/Handlers/FilesTest.php
  • vendor/tectalic/openai/tests/Unit/Handlers/FineTunesCancelTest.php
  • vendor/tectalic/openai/tests/Unit/Handlers/FineTunesEventsTest.php
  • vendor/tectalic/openai/tests/Unit/Handlers/FineTunesTest.php
  • vendor/tectalic/openai/tests/Unit/Handlers/ImagesEditsTest.php
  • vendor/tectalic/openai/tests/Unit/Handlers/ImagesGenerationsTest.php
  • vendor/tectalic/openai/tests/Unit/Handlers/ImagesVariationsTest.php
  • vendor/tectalic/openai/tests/Unit/Handlers/ModelsTest.php
  • vendor/tectalic/openai/tests/Unit/Handlers/ModerationsTest.php
  • vendor/tectalic/openai/tests/Unit/Models/AbstractModelTest.php
  • vendor/tectalic/openai/tests/openapi.yaml

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds LiteLLM proxy support: DB migration and model fillable fields, controller endpoints and generate-flow branching for LiteLLM (responses & chat), a POST route for model discovery, and UI controls + JS to configure and fetch LiteLLM models.

Changes

Cohort / File(s) Summary
Database & Model Configuration
Database/Migrations/2025_06_15_000001_add_litellm_fields_to_freescoutgpt_table.php, Entities/GPTSettings.php
Adds four LiteLLM columns to freescoutgpt (litellm_enabled, litellm_base_url, litellm_api_key, litellm_model) and exposes them in GPTSettings::$fillable.
Controller & Routing
Http/Controllers/FreeScoutGPTController.php, Http/routes.php
New getAvailableLitellmModels(Request) route (POST /freescoutgpt/litellm-models). generate() gains a LiteLLM branch (Responses API and Chat Completions modes), caching for model discovery, error handling, and persistence of LiteLLM answers. settings()/saveSettings() updated for LiteLLM fields.
Frontend UI & Logic
Public/js/settings.js, Resources/views/settings.blade.php
Adds LiteLLM settings UI (enable, base URL, API key, model select). JS enforces mutual exclusivity with Infomaniak, fetches models via new endpoint, populates dropdown, and preserves saved model selection.

Sequence Diagram

sequenceDiagram
    participant Frontend
    participant Controller
    participant Cache
    participant LiteLLM
    rect rgba(200, 150, 100, 0.5)
    Note over Frontend,Controller: Model discovery
    Frontend->>Controller: POST /freescoutgpt/litellm-models (baseUrl, apiKey)
    Controller->>Cache: lookup(baseUrl+apiKey)
    alt cache hit
        Cache-->>Controller: models
    else cache miss
        Controller->>LiteLLM: GET {baseUrl}/v1/models (Bearer apiKey?)
        LiteLLM-->>Controller: { data: [...] }
        Controller->>Cache: store(models, 10m)
    end
    Controller-->>Frontend: { data: [...] }
    Frontend->>Frontend: populate model dropdown
    end
Loading
sequenceDiagram
    participant Frontend
    participant Controller
    participant LiteLLM
    participant DB
    rect rgba(100, 150, 200, 0.5)
    Note over Frontend,Controller: Content generation (LiteLLM branch)
    Frontend->>Controller: POST /generate (query, litellm_enabled=true, settings)
    alt use_responses_api && litellm_enabled
        Controller->>LiteLLM: POST {baseUrl}/v1/responses (prompt + context)
        LiteLLM-->>Controller: { output: [...] } / error
        Controller->>Controller: extract first text output
    else litellm_enabled
        Controller->>LiteLLM: POST {baseUrl}/v1/chat/completions (messages)
        LiteLLM-->>Controller: { choices: [{ message: { content } }] } / error
        Controller->>Controller: extract content
    end
    Controller->>DB: persist answer to thread chatgpt history
    Controller-->>Frontend: { query, answer }
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Poem

🐰 In burrows of code where models hum,

I fetched the list and beat the drum.
A proxy hops, a field is new,
Models fetched and dropdowns too.
Hop! LiteLLM — the patch is through. 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add universal OpenAI-compatible gateway support' accurately reflects the main change—adding LiteLLM proxy support as an OpenAI-compatible gateway to enable various LLM providers.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
Http/Controllers/FreeScoutGPTController.php (1)

462-469: Consider validating litellm_base_url when LiteLLM is enabled.

If litellm_enabled is true but litellm_base_url is empty or null, the Guzzle request will fail with an unclear error. Consider adding a guard clause with a user-friendly error message.

💡 Proposed validation
         // LiteLLM Proxy API: use if enabled, before direct OpenAI
         if (!empty($settings->litellm_enabled)) {
             \Log::info('Using LiteLLM Proxy for answers');
             $litellmBaseUrl = rtrim($settings->litellm_base_url ?? '', '/');
+            if (empty($litellmBaseUrl)) {
+                return Response::json([
+                    'query' => $request->get('query') ?? '',
+                    'answer' => 'LiteLLM is enabled but no base URL is configured. Please configure the LiteLLM proxy URL in settings.'
+                ], 200);
+            }
             $litellmApiKey = $settings->litellm_api_key;
             $litellmModel = $settings->litellm_model;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Http/Controllers/FreeScoutGPTController.php` around lines 462 - 469, When
LiteLLM is enabled (check $settings->litellm_enabled) validate that
$settings->litellm_base_url is present and a non-empty valid URL before using
rtrim() (litellmBaseUrl); in FreeScoutGPTController add a guard clause that
returns a clear error response (or throws a BadRequest/ValidationException) if
litellm_base_url is empty or malformed, and log the validation failure so the
subsequent Guzzle request is never made with an empty base URL.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Http/Controllers/FreeScoutGPTController.php`:
- Around line 550-561: The code assigns $customerEmail inside the
FreeScoutGPTController when $settings->client_data_enabled is true but never
uses it; remove the unused assignment or incorporate it into the system message.
Update the block inside the conditional (references:
$settings->client_data_enabled, $customerEmail, $customerName,
$conversationSubject) so either (a) delete the line that sets $customerEmail =
$request->get("customer_email"); or (b) include the email in the messages[]
system content (e.g., add a "Customer email is: :email" placeholder) and pass
$customerEmail into the translation array, ensuring no unused variables remain.
- Around line 250-290: The getAvailableLitellmModels method currently trusts the
user-supplied $baseUrl and performs server-side requests; validate and sanitize
$baseUrl before using it: first ensure filter_var($baseUrl, FILTER_VALIDATE_URL)
passes and enforce scheme === 'https' (or restrict to an allowlist of domains if
required) by parsing with parse_url; then resolve the host (e.g., dns_get_record
or gethostbyname) and reject addresses in private/internal ranges (check with
ip2long and compare against RFC1918 ranges and link-local/metadata IPs like
169.254.0.0/16); if validation fails, return a 400 error and do not perform the
Guzzle request or cache the result (references: getAvailableLitellmModels,
$baseUrl, $apiKey, $cacheKey).

In `@Public/js/settings.js`:
- Around line 283-284: The code sets litellmModelSelect.innerHTML using
untrusted data.error which allows XSS; change this to create an Option element
(or set option via textContent) and assign its text/value safely instead of
using innerHTML — locate the branch that uses litellmModelSelect.innerHTML with
data.error and replace it with creating an option node (e.g., new Option or
document.createElement('option')), set its textContent (or option.text) to a
sanitized/error string and its value to an empty or escaped value, then append
that option to litellmModelSelect so the error message is rendered as plain text
not HTML.

---

Nitpick comments:
In `@Http/Controllers/FreeScoutGPTController.php`:
- Around line 462-469: When LiteLLM is enabled (check
$settings->litellm_enabled) validate that $settings->litellm_base_url is present
and a non-empty valid URL before using rtrim() (litellmBaseUrl); in
FreeScoutGPTController add a guard clause that returns a clear error response
(or throws a BadRequest/ValidationException) if litellm_base_url is empty or
malformed, and log the validation failure so the subsequent Guzzle request is
never made with an empty base URL.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c8400f9d-fbf6-4f67-90f1-e3dc775b9a70

📥 Commits

Reviewing files that changed from the base of the PR and between 03a039e and 70522e5.

📒 Files selected for processing (6)
  • Database/Migrations/2025_06_15_000001_add_litellm_fields_to_freescoutgpt_table.php
  • Entities/GPTSettings.php
  • Http/Controllers/FreeScoutGPTController.php
  • Http/routes.php
  • Public/js/settings.js
  • Resources/views/settings.blade.php

Comment thread Http/Controllers/FreeScoutGPTController.php Outdated
Comment thread Http/Controllers/FreeScoutGPTController.php Outdated
Comment thread Public/js/settings.js Outdated
… var

- Validate litellm_base_url with filter_var and scheme check to prevent SSRF
- Use textContent instead of innerHTML for error messages to prevent XSS
- Remove unused $customerEmail variable in LiteLLM chat completions path
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
Http/Controllers/FreeScoutGPTController.php (2)

590-593: Same error sanitization concern applies here.

Similar to the Responses API error handling, the Chat Completions path also exposes raw exception messages to users.

🛡️ Proposed fix
             } catch (\Exception $e) {
-                \Log::error('LiteLLM Chat Completions Error: ' . $e->getMessage());
-                $answerText = $e->getMessage();
+                \Log::error('LiteLLM Chat Completions Error: ' . $e->getMessage());
+                $answerText = __('An error occurred while processing your request. Please try again later.');
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Http/Controllers/FreeScoutGPTController.php` around lines 590 - 593, The
catch block in FreeScoutGPTController that currently logs and returns raw
exception text (the catch(\Exception $e) handling that sets $answerText =
$e->getMessage() and calls \Log::error(...)) must avoid exposing internal error
details to users: change it to log the full exception (including stack/trace) to
the server logs using the exception object, and set $answerText to a generic
sanitized message (e.g., "An internal error occurred while generating the
response") before returning; ensure the \Log::error call references the
exception (or its trace) for debugging while the user-facing $answerText
contains no raw $e->getMessage() content.

529-546: Consider sanitizing error messages before returning to users.

The exception handling exposes raw error messages (including $proxyError from the proxy response body) directly to users. This could leak internal infrastructure details such as internal hostnames, IP addresses, or configuration information from the LiteLLM proxy.

🛡️ Proposed fix to sanitize error output
                 }
-                $answerText = $proxyError ?: $errorMsg;
+                $answerText = __('An error occurred while processing your request. Please try again later.');
                 \Log::error('Error on LiteLLM Responses API Request: ' . $answerText);
+                \Log::error('LiteLLM Responses API raw error: ' . ($proxyError ?: $errorMsg));
                 return Response::json([
                     'query' => $userQuery ?? '',
                     'answer' => $answerText
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Http/Controllers/FreeScoutGPTController.php` around lines 529 - 546, The
catch block in FreeScoutGPTController currently returns raw exception details
($proxyError / $errorMsg) to users; change it to sanitize and redact sensitive
data before returning while logging the full error internally. In the catch
handler for the method that contains the current try/catch, capture and log the
full exception (\Log::error($e) or similar), then create a sanitized message by
stripping/obfuscating hostnames, IP addresses and URLs from
$proxyError/$errorMsg (use a regex to replace IPs, hostnames and http(s) URLs
with “[REDACTED]”), truncate to a safe max length, and if the result is empty
return a generic user-facing message like “An internal error occurred; please
try again.” Finally, set $answerText to this sanitized/generic value for the
JSON response while ensuring full details remain in the logs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Http/Controllers/FreeScoutGPTController.php`:
- Around line 590-593: The catch block in FreeScoutGPTController that currently
logs and returns raw exception text (the catch(\Exception $e) handling that sets
$answerText = $e->getMessage() and calls \Log::error(...)) must avoid exposing
internal error details to users: change it to log the full exception (including
stack/trace) to the server logs using the exception object, and set $answerText
to a generic sanitized message (e.g., "An internal error occurred while
generating the response") before returning; ensure the \Log::error call
references the exception (or its trace) for debugging while the user-facing
$answerText contains no raw $e->getMessage() content.
- Around line 529-546: The catch block in FreeScoutGPTController currently
returns raw exception details ($proxyError / $errorMsg) to users; change it to
sanitize and redact sensitive data before returning while logging the full error
internally. In the catch handler for the method that contains the current
try/catch, capture and log the full exception (\Log::error($e) or similar), then
create a sanitized message by stripping/obfuscating hostnames, IP addresses and
URLs from $proxyError/$errorMsg (use a regex to replace IPs, hostnames and
http(s) URLs with “[REDACTED]”), truncate to a safe max length, and if the
result is empty return a generic user-facing message like “An internal error
occurred; please try again.” Finally, set $answerText to this sanitized/generic
value for the JSON response while ensuring full details remain in the logs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e14ba61-96ca-4d77-8d91-1e6cd7364cc9

📥 Commits

Reviewing files that changed from the base of the PR and between 70522e5 and c7952ce.

📒 Files selected for processing (2)
  • Http/Controllers/FreeScoutGPTController.php
  • Public/js/settings.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • Public/js/settings.js

@bigtcze
Copy link
Copy Markdown
Author

bigtcze commented Apr 5, 2026

Any news when if this will be merged?

@presswizards
Copy link
Copy Markdown
Owner

@bigtcze A quick review has me asking, could this be used for almost any custom LLM URL? Not just litellm?

Let me review and test this more... and I can look at merging after that.

@bigtcze
Copy link
Copy Markdown
Author

bigtcze commented Apr 6, 2026

Thank you @presswizards I designed it specificaly for litellm, which is my usecase I'd like to have. I havent consirened any other custom LLM URL. I know, that litellm supports high number of providers. I currently use modern models from Google, OpenAI and Antropic, which is fine with litellm. This change basicaly delegates LLM calls from your project to deditated project, which should help you be more up-to-date with very fast changing world of AI and new LLMs appearing nearly every month.

@presswizards
Copy link
Copy Markdown
Owner

@bigtcze I am just wondering, if the options:

litellm_base_url
litellm_api_key
getAvailableLitellmModels
LiteLLM Base URL

could be named more generic since they are OpenAI-compatible I believe:

llmproxy_base_url
llmproxy_api_key
getAvailableLlmProxyModels
LLMProxy Base URL

I think it would work for others besides LiteLLM, like for OpenRouter, PortKey, BitFrost, and other OpenAI compatible AI gateways or proxies? Because I'd rather have it more open instead of trying to support each gateway or proxy as named options separately... perhaps even rework the existing OpenAI Chat Completions option to be "OpenAI-Compatible Gateway" instead, with an added URL field?

It could be as simple as using:

llm_base_url
llm_api_key

or

llm_gateway_base_url
llm_gateway_api_key

and pull in the models based on what's returned from the API Key.

@bigtcze
Copy link
Copy Markdown
Author

bigtcze commented Apr 8, 2026

Ok, give me few days, I'll update it

Replace the separate LiteLLM proxy option with a single API Base URL
field that works with any OpenAI-compatible gateway or proxy (OpenRouter,
PortKey, LiteLLM, BitFrost, etc.). When the URL is left empty, requests
go directly to OpenAI as before.

- Merge OpenAI + LiteLLM code paths into one unified Guzzle-based flow
- Add api_base_url field with DB migration
- Use 'system' role for custom gateways, 'developer'/'user' for OpenAI
- Use max_tokens (universal) for chat completions compatibility
- Make API key and Authorization header optional for local proxies
- Strip trailing /v1 from base URL to prevent path doubling
- Remove LiteLLM UI section, route, controller method, and migration
- Remove unused Tectalic OpenAI dependency
- Model dropdown now fetches from whatever endpoint is configured
@bigtcze bigtcze changed the title LiteLLM support Add universal OpenAI-compatible gateway support Apr 8, 2026
@bigtcze
Copy link
Copy Markdown
Author

bigtcze commented Apr 8, 2026

Ok there it is :)

@bigtcze
Copy link
Copy Markdown
Author

bigtcze commented Apr 23, 2026

Hello @presswizards did you have a time to check it?

@presswizards
Copy link
Copy Markdown
Owner

@bigtcze Oh very nice, sorry I missed the updated commits... let me test this out and ensure OpenAI, Infomaniak, and now the new Base URL options all work as intended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants