Skip to content

[AI-3] Add the LLM connection administration page - #24880

Open
tangopium wants to merge 1 commit into
feature/66020-pr01-connection-recordfrom
feature/66020-pr02-connection-admin-page
Open

[AI-3] Add the LLM connection administration page#24880
tangopium wants to merge 1 commit into
feature/66020-pr01-connection-recordfrom
feature/66020-pr02-connection-admin-page

Conversation

@tangopium

@tangopium tangopium commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Ticket

AI-3

What are you trying to accomplish?

PR 2 of 11 in the AI-3 stack. This part adds the admin page itself: the "LLM settings" menu entry under Artificial Intelligence (AI), the connection form (enabled, API format, base URL, write-only API key) and the Contract/Service write path. The update contract probes the server when credentials change, so a failed Connect persists nothing, and probing is guarded on changed attributes so a form render never fires an outbound request. Inline errors distinguish an unreachable host, rejected credentials, a non-OpenAI endpoint and an SSRF-filtered address, the latter naming the allowlist environment variable.

Merge checklist

  • Added/updated tests
  • Behind the llm_connection feature flag

Stacked on #24879.

@github-actions

Copy link
Copy Markdown

Caution

The provided work package version does not match the core version

Details:

Please make sure that:

  • The work package version OR your pull request target branch is correct

@github-actions

Copy link
Copy Markdown

Caution

The Enterprise plan field is not set on the work package

Details:

Please make sure that:

  • The work package Enterprise plan field is set

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fcf7e8fc1c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread config/routes.rb
Comment thread app/validators/llm_server_validator.rb Outdated
@tangopium
tangopium force-pushed the feature/66020-pr02-connection-admin-page branch from fcf7e8f to 9ae73a8 Compare August 22, 2026 15:05
@tangopium tangopium changed the title [#66020] Add the LLM connection administration page [AI-3] Add the LLM connection administration page Aug 22, 2026
@github-actions

Copy link
Copy Markdown

Warning

Flaky specs

  • rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:1]
  • rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:2]
🤖 Ask Copilot to investigate

Copy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer.

@copilot The following spec(s) are flaky in CI (first seen on PR #24880, linked for reference only):

- `rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:1]`
- `rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:2]`

Treat this as a standalone task, unrelated to PR #24880. Create a new branch from origin/dev and open a new pull request targeting dev — do not stack it on PR #24880 or reuse that branch.

Follow the playbook in docs/development/testing/handling-flaky-tests/README.md to find the root cause and fix the underlying race — do not skip, delete, or weaken the spec to make it pass; disabling is a last resort per the playbook, and only with a bug ticket. Verify the fix by running the spec(s) repeatedly (e.g. `script/bulk_run_rspec --run-count 10`).

If you cannot reproduce the flake or are not confident in a fix after reasonable investigation, do not fabricate a change or skip the spec to force CI green. Instead, leave the pull request in draft and document what you tried, the suspected cause, and any leads in its description, then assign @tangopium to take over.

Once the fix is verified, title the PR after the spec(s) it fixes, and use the PR description to explain the root cause, how the change resolves it, and the before/after results. Label the PR `flaky-spec`, assign @tangopium, and request a review from @tangopium.
On every commit, set @tangopium as the sole co-author with a `Co-authored-by:` trailer (use their GitHub no-reply email so it links to their account), so it is traceable who dispatched the fix.

@tangopium
tangopium force-pushed the feature/66020-pr02-connection-admin-page branch from 9ae73a8 to 3dd92bc Compare August 22, 2026 15:51
Administration, Artificial Intelligence (AI), LLM settings: API format, base
URL and API key, behind the llm_connection feature flag. Saving is connecting.
The contract probes the server before anything is written, following the
storages precedent where the Nextcloud credentials validator adds a contract
error on 401 and stops the write, so a failed connect persists nothing.

The probe fires only when base URL, API key or API format changed. Without
that guard every unrelated save, and every form render that builds a model
through SetAttributesService, would hit the server. Registry-backed formats
skip it entirely: there is nothing at their base URL to probe.

The API key is write-only. The stored value is never sent to the browser, a
blank submission keeps the current key, and the Stimulus controller wipes the
field on turbo:before-cache so the back button cannot restore a typed secret.
The base URL is normalised only by trimming whitespace and a trailing slash;
the version segment is never rewritten, because silently editing an
administrator's URL makes the eventual failure harder to diagnose.

A 404 for the model list is not a failure: a server can speak chat perfectly
while offering no list, so the save succeeds and later parts of this stack
give the administrator ways to name models by hand.

Part 2 of the AI-3 stack.

https://community.openproject.org/work_packages/66020
@github-actions

Copy link
Copy Markdown

Warning

Flaky specs

  • rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:2]
🤖 Ask Copilot to investigate

Copy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer.

@copilot The following spec(s) are flaky in CI (first seen on PR #24880, linked for reference only):

- `rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:2]`

Treat this as a standalone task, unrelated to PR #24880. Create a new branch from origin/dev and open a new pull request targeting dev — do not stack it on PR #24880 or reuse that branch.

Follow the playbook in docs/development/testing/handling-flaky-tests/README.md to find the root cause and fix the underlying race — do not skip, delete, or weaken the spec to make it pass; disabling is a last resort per the playbook, and only with a bug ticket. Verify the fix by running the spec(s) repeatedly (e.g. `script/bulk_run_rspec --run-count 10`).

If you cannot reproduce the flake or are not confident in a fix after reasonable investigation, do not fabricate a change or skip the spec to force CI green. Instead, leave the pull request in draft and document what you tried, the suspected cause, and any leads in its description, then assign @tangopium to take over.

Once the fix is verified, title the PR after the spec(s) it fixes, and use the PR description to explain the root cause, how the change resolves it, and the before/after results. Label the PR `flaky-spec`, assign @tangopium, and request a review from @tangopium.
On every commit, set @tangopium as the sole co-author with a `Co-authored-by:` trailer (use their GitHub no-reply email so it links to their account), so it is traceable who dispatched the fix.

@tangopium
tangopium force-pushed the feature/66020-pr02-connection-admin-page branch from 3dd92bc to 0b2833b Compare August 22, 2026 16:16
@tangopium
tangopium requested a review from thykel August 22, 2026 21:39
@tangopium tangopium self-assigned this Aug 22, 2026
@github-actions

Copy link
Copy Markdown

Warning

Flaky specs

  • rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:1]
🤖 Ask Copilot to investigate

Copy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer.

@copilot The following spec(s) are flaky in CI (first seen on PR #24880, linked for reference only):

- `rspec ./spec/features/work_packages/table/switch_types_spec.rb[1:1:1]`

Treat this as a standalone task, unrelated to PR #24880. Create a new branch from origin/dev and open a new pull request targeting dev — do not stack it on PR #24880 or reuse that branch.

Follow the playbook in docs/development/testing/handling-flaky-tests/README.md to find the root cause and fix the underlying race — do not skip, delete, or weaken the spec to make it pass; disabling is a last resort per the playbook, and only with a bug ticket. Verify the fix by running the spec(s) repeatedly (e.g. `script/bulk_run_rspec --run-count 10`).

If you cannot reproduce the flake or are not confident in a fix after reasonable investigation, do not fabricate a change or skip the spec to force CI green. Instead, leave the pull request in draft and document what you tried, the suspected cause, and any leads in its description, then assign @tangopium to take over.

Once the fix is verified, title the PR after the spec(s) it fixes, and use the PR description to explain the root cause, how the change resolves it, and the before/after results. Label the PR `flaky-spec`, assign @tangopium, and request a review from @tangopium.
On every commit, set @tangopium as the sole co-author with a `Co-authored-by:` trailer (use their GitHub no-reply email so it links to their account), so it is traceable who dispatched the fix.

@thykel thykel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

First pass done 🚀

One more thing that comes to mind: These new Admin pages are likely gonna be gated by a subscription tier. The current consensus is that they will be available in Professional and above.

That topic is too big to roll into this PR, so we should definitely establish a separate PR on for it. [Lookbook resource]

I18n.t("admin.llm_connections.form.api_key_caption#{'_stored' if key}")
end

def api_key_placeholder

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So, when we come to this form for the first time, the field is empty.

Then, if the form submission fails (because of a bogus URL), the "API key" field gets cleared and suddenly displays the asterisk placeholder.

Shall we fix this?

  • For starters, that placeholder should only be displayed if there is an actual value stored already.
  • However, we probably also want to retain the API key value even once the error hits -- the rest of the form is also retained anyway.

module Admin
class LlmConnectionsController < ApplicationController
include OpTurbo::ComponentStream
include PaginationHelper

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 PaginationHelper is not used anywhere in this PR — show is empty and nothing paginates until the model table arrives in #24883. Worth moving to the PR that needs it.


def render_form_with_errors
update_via_turbo_stream(component: ::LlmConnections::FormComponent.new(@connection))
# The HTML fallback re-renders the whole page, which needs everything the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 This comment describes a show that assigns several things, but in this PR show is def show; end. The comment becomes accurate in #24883. Either trim it here or move it with the code it describes.

contract.errors.add(:api_key, :invalid_api_key)
when Llm::Client::TimeoutError
contract.errors.add(attribute, :request_timed_out)
when Llm::Client::ConnectionError

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 Faraday::SSLError and HTTPX's TLS failures both reach this branch as ConnectionError, so a certificate problem is reported as "could not be reached. Please ensure the LLM server is running and reachable from OpenProject."

The acceptance criteria ask for a distinct SSL error under Base URL. The field placement is already right; only the message is missing. Given that plain http is deliberately allowed for on-premise servers, a TLS failure against an https URL is a case an administrator will hit, and "the server is not running" sends them the wrong way.

If this is deferred on the grounds that the ticket calls its own list incomplete, that is reasonable — worth saying so on the ticket rather than leaving it implicit.

// gives the administrator feedback while that happens, and keeps the typed API
// key out of Turbo's page cache.
export default class extends Controller {
static targets = ['submitButton', 'progressBanner', 'secretInput'];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 The acceptance criteria include: "When LLM settings checkbox is disabled the form underneath and the LLM panel are hidden."

This controller handles the progress banner and secret clearing, and nothing keys off the Enabled checkbox — the form and (from #24883) the model panel stay visible when it is unchecked. The flag works correctly at runtime, so this is presentation only, but it is an unimplemented criterion rather than a deliberate deviation as far as I can tell.


validates :base_url, presence: true
validates :api_format, inclusion: { in: Llm::Adapters::FORMATS }
# Bedrock and Vertex AI can be discovered against, but not called: they need

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Duplicated comment, let's keep only the one at the source.

attribute :default_chat_model_id
attribute :default_embedding_model_id

validates :base_url, presence: true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's move this next to the other one :)

Comment thread config/locales/en.yml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ouch, what happened with the newline purge? Let's keep this lean, as we use an automated linter.


module LlmConnections
class ConnectionForm < ApplicationForm
form do |f|

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Following our Lookbook, let's wrap this in a fieldset_group.

Comment thread config/locales/en.yml
Comment on lines +1704 to +1707
add_model_description: "If the server does not expose a model list, enter the model name exactly as the server expects it. Manually added models are kept when the list is refreshed."
add_model_submit: "Add model"
description: "Connect OpenProject to a server that speaks the OpenAI API, so AI features can use it."
refresh_models: "Refresh models"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These are not relevant in this PR yet, shall we do a move?

module LlmConnections
class ConnectionForm < ApplicationForm
form do |f|
f.check_box(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Based on our design principles (and the original Figma designs), we only want to expose the rest of the form once "Enable LLMs for this instance" is ticked as enabled.

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants