diff --git a/db/migrate/20251104183810_add_ai_gateway_timeout_seconds_to_ai_settings.rb b/db/migrate/20251104183810_add_ai_gateway_timeout_seconds_to_ai_settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f9c82f50774ace239c93f81f236410587984bb08
--- /dev/null
+++ b/db/migrate/20251104183810_add_ai_gateway_timeout_seconds_to_ai_settings.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddAiGatewayTimeoutSecondsToAiSettings < Gitlab::Database::Migration[2.3]
+ disable_ddl_transaction!
+
+ milestone '18.6'
+
+ def up
+ add_column :ai_settings, :ai_gateway_timeout_seconds, :integer, default: 60, if_not_exists: true
+ end
+
+ def down
+ remove_column :ai_settings, :ai_gateway_timeout_seconds, if_exists: true
+ end
+end
diff --git a/db/schema_migrations/20251104183810 b/db/schema_migrations/20251104183810
new file mode 100644
index 0000000000000000000000000000000000000000..b4513fe49c1de871e3d3ddc6598b3f42abaf832a
--- /dev/null
+++ b/db/schema_migrations/20251104183810
@@ -0,0 +1 @@
+e5e2689cc3d6a184d65f2a35582b990c8b88f168cabf0d8db6a9b761475b3a90
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 1e4fb5ed8ec2e479674b9dabefe4fbd456510253..bf171a4138872ecca45404a9b1207fd645a34668 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -10602,6 +10602,7 @@ CREATE TABLE ai_settings (
duo_agent_platform_service_url text,
duo_agent_platform_request_count integer DEFAULT 0 NOT NULL,
foundational_agents_default_enabled boolean DEFAULT true,
+ ai_gateway_timeout_seconds integer DEFAULT 60,
CONSTRAINT check_3cf9826589 CHECK ((char_length(ai_gateway_url) <= 2048)),
CONSTRAINT check_900d7a89b3 CHECK ((char_length(duo_agent_platform_service_url) <= 2048)),
CONSTRAINT check_a02bd8868c CHECK ((char_length(amazon_q_role_arn) <= 2048)),
diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index 43a7cddc118ea1a0ff0589767f93e10b30acac13..c8c35a5d64254fa46852e851d5afca776e536f19 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -6712,6 +6712,7 @@ Input type: `DuoSettingsUpdateInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
+| `aiGatewayTimeoutSeconds` | [`Int`](#int) | Timeout for AI gateway request. |
| `aiGatewayUrl` | [`String`](#string) | URL for local AI gateway server. |
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| `duoAgentPlatformServiceUrl` | [`String`](#string) | URL for the local Duo Agent Platform service. |
@@ -29977,6 +29978,7 @@ GitLab Duo settings.
| Name | Type | Description |
| ---- | ---- | ----------- |
+| `aiGatewayTimeoutSeconds` | [`Int`](#int) | Timeout in seconds for requests to the AI gateway server. |
| `aiGatewayUrl` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 17.9. **Status**: Experiment. URL for local AI gateway server. |
| `duoAgentPlatformServiceUrl` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 18.4. **Status**: Experiment. URL for local Duo Agent Platform service. |
| `duoCoreFeaturesEnabled` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Introduced** in GitLab 18.0. **Status**: Experiment. Indicates whether GitLab Duo Core features are enabled. |
diff --git a/ee/app/graphql/mutations/ai/duo_settings/update.rb b/ee/app/graphql/mutations/ai/duo_settings/update.rb
index a692beb868d4f1d3634aa170e4e7ab32af483de5..5c0d14098706252e2cd47c72efb08f87d5aeff3e 100644
--- a/ee/app/graphql/mutations/ai/duo_settings/update.rb
+++ b/ee/app/graphql/mutations/ai/duo_settings/update.rb
@@ -11,6 +11,10 @@ class Update < BaseMutation
required: false,
description: 'URL for local AI gateway server.'
+ argument :ai_gateway_timeout_seconds, GraphQL::Types::Int,
+ required: false,
+ description: "Timeout for AI gateway request."
+
argument :duo_agent_platform_service_url, String,
required: false,
description: 'URL for the local Duo Agent Platform service.'
@@ -46,13 +50,10 @@ def resolve(**args)
private
def check_feature_available!(args)
- raise_resource_not_available_error!(:ai_gateway_url) if args.key?(:ai_gateway_url) &&
- !allowed_to_update?(:manage_self_hosted_models_settings)
-
- if args.key?(:duo_agent_platform_service_url) &&
- !allowed_to_update?(:manage_self_hosted_models_settings)
-
- raise_resource_not_available_error!(:duo_agent_platform_service_url)
+ [:ai_gateway_url, :duo_agent_platform_service_url, :ai_gateway_timeout_seconds].each do |setting|
+ if args.key?(setting) && !allowed_to_update?(:manage_self_hosted_models_settings)
+ raise_resource_not_available_error!(setting)
+ end
end
raise_resource_not_available_error!(:duo_core_features_enabled) if args.key?(:duo_core_features_enabled) &&
diff --git a/ee/app/graphql/types/ai/duo_settings/duo_settings_type.rb b/ee/app/graphql/types/ai/duo_settings/duo_settings_type.rb
index e2004b5a0c9f338cb0faadc2780724dcea4c4ca8..299e5561b087b6259a2d04c5f87156b74cf1fdba 100644
--- a/ee/app/graphql/types/ai/duo_settings/duo_settings_type.rb
+++ b/ee/app/graphql/types/ai/duo_settings/duo_settings_type.rb
@@ -7,12 +7,19 @@ class DuoSettingsType < ::Types::BaseObject # rubocop:disable Graphql/AuthorizeT
graphql_name 'DuoSettings'
description 'GitLab Duo settings'
+ # rubocop: disable GraphQL/ExtractType -- no value for now
field :ai_gateway_url, String,
null: true,
description: 'URL for local AI gateway server.',
authorize: :read_self_hosted_models_settings,
experiment: { milestone: '17.9' }
+ field :ai_gateway_timeout_seconds, GraphQL::Types::Int,
+ null: true,
+ description: 'Timeout in seconds for requests to the AI gateway server.',
+ authorize: :read_self_hosted_models_settings
+ # rubocop: enable GraphQL/ExtractType
+
field :duo_agent_platform_service_url, String,
null: true,
description: 'URL for local Duo Agent Platform service.',
diff --git a/ee/app/models/ai/setting.rb b/ee/app/models/ai/setting.rb
index 8efca32ca0ad05688c3592d1b65abcf86692c143..8ab8e719c30e187c6a5681c8a3818d216d0175ab 100644
--- a/ee/app/models/ai/setting.rb
+++ b/ee/app/models/ai/setting.rb
@@ -13,6 +13,11 @@ class Setting < ApplicationRecord
validates :amazon_q_role_arn, length: { maximum: 2048 }, allow_nil: true
validate :validate_ai_gateway_url
+ validates :ai_gateway_timeout_seconds,
+ numericality: {
+ greater_than_or_equal_to: 60,
+ less_than_or_equal_to: 600
+ }
validates :duo_core_features_enabled,
inclusion: { in: [true, false] },
diff --git a/ee/spec/models/ai/setting_spec.rb b/ee/spec/models/ai/setting_spec.rb
index 024a76288505e1a8dc1161a86a226e9419c52963..011457a207f26c4331a251920556d82d082b1646 100644
--- a/ee/spec/models/ai/setting_spec.rb
+++ b/ee/spec/models/ai/setting_spec.rb
@@ -183,6 +183,12 @@
end
end
+ it 'validates ai_gateway_timeout_seconds is between 60 and 600' do
+ expect(described_class.instance).to validate_numericality_of(:ai_gateway_timeout_seconds)
+ .is_greater_than_or_equal_to(60)
+ .is_less_than_or_equal_to(600)
+ end
+
it { is_expected.to validate_length_of(:amazon_q_role_arn).is_at_most(2048).allow_nil }
end
diff --git a/ee/spec/requests/api/graphql/ai/duo_settings/update_spec.rb b/ee/spec/requests/api/graphql/ai/duo_settings/update_spec.rb
index 255f9ba670467e0fdf48d2b57faaa48be95718e9..8bbb0229909f991fd7668367a218994eda20e826 100644
--- a/ee/spec/requests/api/graphql/ai/duo_settings/update_spec.rb
+++ b/ee/spec/requests/api/graphql/ai/duo_settings/update_spec.rb
@@ -18,7 +18,8 @@
{
ai_gateway_url: "http://new-ai-gateway-url",
duo_core_features_enabled: true,
- duo_agent_platform_service_url: "new-duo-agent-platform-url:50052"
+ duo_agent_platform_service_url: "new-duo-agent-platform-url:50052",
+ ai_gateway_timeout_seconds: 100
}
end
@@ -37,8 +38,9 @@
end
context 'when the user does not have write access' do
+ let(:current_user) { create(:user) }
+
context 'when attempting to update ai_gateway_url' do
- let(:current_user) { create(:user) }
let(:mutation_params) { { ai_gateway_url: "http://new-ai-gateway-url" } }
it_behaves_like 'performs the right authorization'
@@ -54,7 +56,6 @@
end
context 'when attempting to update duo_agent_platform_service_url' do
- let(:current_user) { create(:user) }
let(:mutation_params) { { duo_agent_platform_service_url: "new-duo-agent-platform-url:50052" } }
it_behaves_like 'performs the right authorization'
@@ -69,6 +70,21 @@
end
end
+ context 'when attempting to update ai_gateway_timeout_seconds' do
+ let(:mutation_params) { { ai_gateway_timeout_seconds: 100 } }
+
+ it_behaves_like 'performs the right authorization'
+
+ it 'returns an error about the missing permission' do
+ request
+
+ expect(graphql_errors).to be_present
+ expect(graphql_errors.pluck('message')).to match_array(
+ "You don't have permission to update the setting ai_gateway_timeout_seconds."
+ )
+ end
+ end
+
context 'when attempting to update duo_core_features_enabled' do
let(:mutation_params) { { duo_core_features_enabled: true } }
@@ -146,13 +162,15 @@
expect(result['duoSettings']).to include(
"aiGatewayUrl" => "http://new-ai-gateway-url",
- "duoCoreFeaturesEnabled" => true
+ "duoCoreFeaturesEnabled" => true,
+ "aiGatewayTimeoutSeconds" => 100
)
expect(result['errors']).to eq([])
expect { duo_settings.reload }.to change { duo_settings.ai_gateway_url }.to("http://new-ai-gateway-url")
.and change { duo_settings.duo_core_features_enabled }.to(true)
.and change { duo_settings.duo_agent_platform_service_url }.to("new-duo-agent-platform-url:50052")
+ .and change { duo_settings.ai_gateway_timeout_seconds }.to(100)
end
context 'when ai_gateway_url arg is a blank string' do
diff --git a/ee/spec/services/ai/duo_settings/update_service_spec.rb b/ee/spec/services/ai/duo_settings/update_service_spec.rb
index c3f3c81acef1bd2afd52564b619318be228eb7be..3f85385cd4dfbb487e91bfff867945d087b1448e 100644
--- a/ee/spec/services/ai/duo_settings/update_service_spec.rb
+++ b/ee/spec/services/ai/duo_settings/update_service_spec.rb
@@ -6,7 +6,7 @@
let_it_be(:user) { create(:user) }
let_it_be(:duo_settings) { create(:ai_settings) }
- let(:params) { { ai_gateway_url: "http://new-ai-gateway-url", duo_core_features_enabled: true } }
+ let(:params) { { ai_gateway_url: "http://new-ai-gateway-url", duo_core_features_enabled: true, ai_gateway_timeout_seconds: 100 } }
subject(:service_result) { described_class.new(params).execute }
@@ -15,6 +15,7 @@
it 'returns a success response' do
expect { service_result }.to change { duo_settings.reload.ai_gateway_url }.to("http://new-ai-gateway-url")
.and change { duo_settings.reload.duo_core_features_enabled }.to(true)
+ .and change { duo_settings.reload.ai_gateway_timeout_seconds }.to(100)
expect(service_result).to be_success
expect(service_result.payload).to eq(duo_settings)