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)