Refuse chat and completions requests that need the cloud connector
What does this MR do and why?
This is the second of three merge requests, split from one change to limit blast radius. It moves the chat and completions callers onto the resolver.
Depends on !260773 (merged), the resolver MR, which adds Gitlab::AiGateway.request_url! and NoCloudConnectorTokenError.
For reviewers: the Code Suggestions MR, !265184, will stack on this MR. The merge order is !260773 (merged), then this MR, then that one.
On an offline cloud license, a GitLab Duo request can reach cloud.gitlab.com when Local AI Gateway URL is nil or a feature uses a GitLab-managed model. It carries no credential that host issued.
On an offline cloud license, the resolver now follows the token decision. It refuses a request, naming the feature setting, unless the token would be self-signed.
This covers a GitLab-managed or missing Chat row, which previously failed with a 401 and A9999.
Each caller here now resolves its URL through request_url! or access_token_url!, and passes the unit primitive its token headers use.
When the resolver refuses, the caller stops before the request, in the failure shape its own callers already handle. The message names the setting to change.
- Classic Duo Chat's agent step, tools and slash commands let
NoCloudConnectorTokenErrorpropagate to their existing rescues, and Chat answers with a translated message and error code A1001, which names only the missing setting. - Duo Chat documentation search returns
nil. - AI Gateway completions return
nil, or adetailnaming the setting. Vulnerability Resolution publishes the same message as the error of its answer. - Code embeddings raise
EmbeddingsGenerationError. - Third-party agent token requests return a 503.
- The Anthropic and Vertex AI proxy clients raise it to their caller, though no production code builds them today.
Where each request resolves its URL, and how it reports the refusal
| Request | Resolved at | Result on an offline cloud license |
|---|---|---|
| Classic Duo Chat agent step | ee/lib/gitlab/duo/chat/step_executor.rb, through #base_url |
NoCloudConnectorTokenError, a ConnectionError, which Chat answers with a translated message and A1001 (ee/lib/gitlab/duo/chat/react_executor.rb) |
| Classic Duo Chat tools and slash commands | ee/lib/gitlab/llm/chain/requests/ai_gateway.rb, in #endpoint |
the same error, to each tool's existing rescue. Slash commands keep their generic M4000 error, and llm.log names the setting |
| Duo Chat documentation search | ee/lib/gitlab/llm/ai_gateway/docs_client.rb |
nil, which its only caller reads as no results. It reads the raw General Chat row, so a GitLab-managed setting there refuses even with the URL set |
AI Gateway completions: Completions::Base features, Vulnerability Resolution, the git command, evaluators |
ee/lib/gitlab/llm/concerns/ai_gateway_client_concern.rb. Completions::Base resolves first (ee/lib/gitlab/llm/ai_gateway/completions/base.rb), and so does Vulnerability Resolution in its own request!, which overrides Base's (ee/lib/gitlab/llm/ai_gateway/completions/resolve_vulnerability.rb) |
For the git command and evaluators, nil, the concern's failed-request value. A Completions::Base feature returns a detail naming the setting, as a failed request returns. Vulnerability Resolution publishes the same message as the error of its answer |
| Code embeddings | ee/lib/gitlab/llm/embeddings/model_definition.rb, rescued in ee/lib/gitlab/llm/embeddings/client.rb |
EmbeddingsGenerationError, as a failed response raises, which indexing logs and retries |
Third-party agent token, POST /api/v4/ai/third_party_agents/direct_access |
ee/lib/ai/third_party_agents/token_service.rb |
the error a failed token request returns, which the API answers with a 503 |
| Anthropic and Vertex AI proxy clients | ee/lib/gitlab/llm/anthropic/client.rb, ee/lib/gitlab/llm/vertex_ai/model_configurations/base.rb |
NoCloudConnectorTokenError to the caller. Outside specs, the clients are built only in ee/lib/gitlab/llm/chain/requests/anthropic.rb and vertex_ai.rb, and those requests are built nowhere |
Behaviour changes to rule on
- Third-party agents are always refused offline, since no setting can make their token self-signed. The refusal is not sent to error tracking.
- Embeddings with a self-hosted ActiveContext model are refused unless the Code Embeddings setting is self-hosted, because the token reads that row. An instance whose local gateway bypasses auth worked before, and Rails cannot see the bypass.
- Offline and with no setting, the gate and the token each look up the feature setting: one extra query per request.
Refusal path
flowchart LR
R["a Duo request<br/>builds its URL"] --> RU["Gitlab::AiGateway.request_url!<br/>or access_token_url!<br/>with the caller's unit primitive"]
RU --> L{"offline<br/>cloud license?"}
L -->|no| S["URL returned<br/>request sent"]
L -->|yes| U{"Local AI Gateway<br/>URL set?"}
U -->|no| E["raise NoCloudConnectorTokenError<br/>naming the setting<br/>no request sent"]
U -->|yes| T{"would the token<br/>be self-signed?"}
T -->|yes| S
T -->|no| E
E --> X["each caller handles it<br/>in its own failure shape"]
X --> CH["StepExecutor, Requests::AiGateway<br/>propagate to Chat's A1001 answer and tool rescues"]
X --> DC["DocsClient, AiGatewayClientConcern<br/>log and nil"]
X --> BA["Completions::Base<br/>detail naming the setting"]
X --> EM["Embeddings::Client<br/>EmbeddingsGenerationError"]
X --> TP["ThirdPartyAgents::TokenService<br/>error, API 503"]
X --> AV["Anthropic::Client, VertexAi model configurations<br/>raised to the caller"]An AI coding assistant wrote this change and ran the checks below in a simulated environment: a GitLab source checkout and a 19.4.1 test instance.
References
- Relates to #629912. The Code Suggestions MR closes it once all three have landed.
- Split from !260773 (merged) at the request of its reviewer, @wanpol.
- Depends on !260773 (merged), the resolver MR.
Screenshots or screen recordings
After this change, with Local AI Gateway URL unset, Duo Chat answers that the instance needs a URL and a self-hosted model:
How to set up and validate locally
Steps, and what they showed on 19.4.1
- With an offline cloud license, assign self-hosted models to General Chat, Code Explanation and GitLab Duo for CLI, and clear Local AI Gateway URL.
- Ask Duo Chat a question (the
aiActionmutation withchat), send/explainon a selected snippet,POST /api/v4/ai/llm/git_commandandPOST /api/v4/ai/third_party_agents/direct_access. Throughgitlab-rails runnercallGitlab::Llm::AiGateway::DocsClient.new(user).search(query: 'How do I create an issue?'), since a Chat question with the URL cleared, or with General Chat GitLab-managed, stops at the agent step first. Expected: each answers as in the table below. - Set the URL and repeat. Expected: the local AI gateway serves each request, except the third-party agent token, which is refused.
- With the URL set, make General Chat GitLab-managed, or remove its setting, and ask Duo Chat a question. Expected: no request, and A1001 naming General Chat.
- Set General Chat to
vendoredwithAi::FeatureSettings::UpdateServicein a Rails console and repeat the search. Expected: no request,nil, andllm.lognames General Chat. bundle exec rspecon the changed spec files.
| On 19.4.1 with an offline cloud license | Before | With this change |
|---|---|---|
| Duo Chat question, URL cleared | reached cloud.gitlab.com, 401, and Chat answered I'm sorry, I can't generate a response. Please try again. (A9999) |
no request, and at once the translated answer (A1001) |
/explain on a snippet, URL cleared |
reached cloud.gitlab.com, 401, and the same answer (M4000) |
no request, and the same answer (M4000) |
POST /api/v4/ai/llm/git_command, URL cleared |
reached cloud.gitlab.com, 401, and the API returned 201 with a null body |
no request, and 201 with a null body |
POST /api/v4/ai/third_party_agents/direct_access, URL cleared |
reached cloud.gitlab.com, and the API returned 503 Token creation failed |
no request, and a 503 naming Local AI Gateway URL |
DocsClient#search, URL cleared |
reached cloud.gitlab.com, 401 |
no request, and nil |
DocsClient#search, URL set, General Chat GitLab-managed |
reached cloud.gitlab.com, 401 |
no request, nil, and llm.log names General Chat |
These instance results come from the combined change in the original merge request, before it was split. Each held with internet access and without it, with no connection attempt.
Code embeddings and the Anthropic and Vertex AI clients rest on their specs alone, since they need an ActiveContext vector store or have no production caller.
- On a test VM with this MR applied on top of the resolver MR, 12 of the 14 changed spec files pass with 470 examples and no failures. With the Code Suggestions MR on top, all 14 pass with 569 examples and no failures.
- Before the last commit, which changes only the third-party agent token service and its spec, the changed and 11 related spec files passed with 779 examples, and no changed line lacked coverage.
- On a 19.5.3 test instance with the URL set, a GitLab-managed or missing General Chat setting answers A1001 with no gateway call and no cloud connection. A self-hosted setting still answers.
- RuboCop is clean on the 30 changed Ruby files.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
