[AI-3] Add the LLM connection administration page - #24880
Conversation
|
Caution The provided work package version does not match the core version Details:
Please make sure that:
|
|
Caution The Enterprise plan field is not set on the work package Details:
Please make sure that:
|
There was a problem hiding this comment.
💡 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".
fcf7e8f to
9ae73a8
Compare
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy 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. |
9ae73a8 to
3dd92bc
Compare
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
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy 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. |
3dd92bc to
0b2833b
Compare
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy 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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
🤖 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 |
There was a problem hiding this comment.
🤖 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 |
There was a problem hiding this comment.
🤖 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']; |
There was a problem hiding this comment.
🤖 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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Let's move this next to the other one :)
There was a problem hiding this comment.
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| |
There was a problem hiding this comment.
Following our Lookbook, let's wrap this in a fieldset_group.
| 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" |
There was a problem hiding this comment.
These are not relevant in this PR yet, shall we do a move?
| module LlmConnections | ||
| class ConnectionForm < ApplicationForm | ||
| form do |f| | ||
| f.check_box( |
There was a problem hiding this comment.
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.
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
llm_connectionfeature flagStacked on #24879.