diff --git a/ee/app/policies/ee/global_policy.rb b/ee/app/policies/ee/global_policy.rb index d0b23811f2b000f0696b4fa335ed7bbdadde54de..3e9396cdcfe91bbc1acfae01975067dce4632403 100644 --- a/ee/app/policies/ee/global_policy.rb +++ b/ee/app/policies/ee/global_policy.rb @@ -73,7 +73,9 @@ module GlobalPolicy next true if ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) next false unless ::License.feature_available?(:ai_chat) - if duo_chat_free_access_was_cut_off? + if duo_chat_self_hosted? + self_hosted_models_available_for?(@user) + elsif duo_chat_free_access_was_cut_off? duo_chat.allowed_for?(@user) else # Before service start date ::Gitlab::CurrentSettings.duo_features_enabled? @@ -84,7 +86,9 @@ module GlobalPolicy next false unless @user next true unless ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) - if duo_chat_free_access_was_cut_off? + if duo_chat_self_hosted? + self_hosted_models_available_for?(@user) + elsif duo_chat_free_access_was_cut_off? duo_chat.allowed_for?(@user) else @user.any_group_with_ai_chat_available? @@ -217,5 +221,17 @@ def duo_chat_free_access_was_cut_off_for_sm? def duo_chat CloudConnector::AvailableServices.find_by_name(:duo_chat) end + + # Check whether a user is allowed to use Duo Chat powered by self-hosted models + def duo_chat_self_hosted? + ::Ai::FeatureSetting.find_by_feature(:duo_chat)&.self_hosted? + end + + def self_hosted_models_available_for?(user) + service = CloudConnector::AvailableServices.find_by_name(:self_hosted_models) + return false unless service + + service.free_access? || service.allowed_for?(user) + end end end diff --git a/ee/lib/gitlab/llm/ai_gateway/docs_client.rb b/ee/lib/gitlab/llm/ai_gateway/docs_client.rb index ab96c2397d7892753ed7162703573af951ff4264..7c2ae2275d5985a5c73e8de33a4e6497466bcbf2 100644 --- a/ee/lib/gitlab/llm/ai_gateway/docs_client.rb +++ b/ee/lib/gitlab/llm/ai_gateway/docs_client.rb @@ -50,7 +50,10 @@ def enabled? end def access_token - ::CloudConnector::AvailableServices.find_by_name(:duo_chat).access_token(user) + chat_feature_setting = ::Ai::FeatureSetting.find_by_feature(:duo_chat) + feature_name = chat_feature_setting&.self_hosted? ? :self_hosted_models : :duo_chat + + ::CloudConnector::AvailableServices.find_by_name(feature_name).access_token(user) end strong_memoize_attr :access_token diff --git a/ee/lib/gitlab/llm/chain/requests/ai_gateway.rb b/ee/lib/gitlab/llm/chain/requests/ai_gateway.rb index 97e8a82692613a8217b1fa3bc23f09f45c7f4d18..055501e0eaf4173219ae14ef083fc37cc234ef16 100644 --- a/ee/lib/gitlab/llm/chain/requests/ai_gateway.rb +++ b/ee/lib/gitlab/llm/chain/requests/ai_gateway.rb @@ -25,7 +25,7 @@ class AiGateway < Base def initialize(user, service_name: :duo_chat, tracking_context: {}) @user = user @tracking_context = tracking_context - @ai_client = ::Gitlab::Llm::AiGateway::Client.new(user, service_name: service_name, + @ai_client = ::Gitlab::Llm::AiGateway::Client.new(user, service_name: processed_service_name(service_name), tracking_context: tracking_context) @logger = Gitlab::Llm::Logger.build end @@ -171,6 +171,13 @@ def tracking_class_name(provider) def chat_feature_setting ::Ai::FeatureSetting.find_by_feature(:duo_chat) end + + def processed_service_name(service_name) + return service_name unless service_name == :duo_chat + return service_name unless chat_feature_setting&.self_hosted? + + :self_hosted_models + end end end end diff --git a/ee/spec/lib/gitlab/llm/ai_gateway/docs_client_spec.rb b/ee/spec/lib/gitlab/llm/ai_gateway/docs_client_spec.rb index 4b6220eecea6c5845dfa2e48ffafc0e3c9a1629e..3a15540e7a4c7c3e7c41957fe2a057276c677193 100644 --- a/ee/spec/lib/gitlab/llm/ai_gateway/docs_client_spec.rb +++ b/ee/spec/lib/gitlab/llm/ai_gateway/docs_client_spec.rb @@ -90,5 +90,22 @@ expect(result).to eq(nil) end end + + context 'when duo chat model is self-hosted' do + let_it_be(:feature_setting) { create(:ai_feature_setting, feature: :duo_chat) } + + it 'returns access token for self-hosted-models service' do + service = instance_double('::CloudConnector::SelfSigned::AvailableServiceData') + expect(::CloudConnector::AvailableServices).to receive(:find_by_name) + .with(:self_hosted_models).and_return(service) + allow(service).to receive(:access_token).and_return(expected_access_token) + + expect(Gitlab::HTTP).to receive(:post).with( + anything, + hash_including(timeout: described_class::DEFAULT_TIMEOUT) + ).and_call_original + expect(result.parsed_response).to eq(expected_response) + end + end end end diff --git a/ee/spec/lib/gitlab/llm/chain/requests/ai_gateway_spec.rb b/ee/spec/lib/gitlab/llm/chain/requests/ai_gateway_spec.rb index 7818ac61e45a6894720ed9f868ec687ec6ea22e1..cb0881c20d901c19ec2567f6f68f88f92d4b8265 100644 --- a/ee/spec/lib/gitlab/llm/chain/requests/ai_gateway_spec.rb +++ b/ee/spec/lib/gitlab/llm/chain/requests/ai_gateway_spec.rb @@ -24,6 +24,20 @@ described_class.new(user, service_name: :alternative, tracking_context: tracking_context) end end + + context 'when duo chat is self-hosted' do + let_it_be(:feature_setting) { create(:ai_feature_setting, feature: :duo_chat, provider: :self_hosted) } + + it 'creates ai gateway client with self-hosted-models service name' do + expect(::Gitlab::Llm::AiGateway::Client).to receive(:new).with( + user, + service_name: :self_hosted_models, + tracking_context: tracking_context + ) + + described_class.new(user, service_name: :duo_chat, tracking_context: tracking_context) + end + end end describe '#request' do diff --git a/ee/spec/policies/global_policy_spec.rb b/ee/spec/policies/global_policy_spec.rb index 208d418b62699f3af5a313bf79152b4ef296d8ee..b6b82283277e942d208d27b814d1c3ddeb68af2e 100644 --- a/ee/spec/policies/global_policy_spec.rb +++ b/ee/spec/policies/global_policy_spec.rb @@ -641,15 +641,19 @@ context 'when on .org or .com', :saas do where(:group_with_ai_membership, :duo_pro_seat_assigned, :requires_licensed_seat, - :duo_chat_enabled_for_user) do - false | false | false | be_disallowed(policy) - false | true | false | be_disallowed(policy) - true | false | false | be_allowed(policy) - true | true | false | be_allowed(policy) + :self_hosted_duo_chat_enabled, :self_hosted_duo_chat_available, :duo_chat_enabled_for_user) do + false | false | false | false | false | be_disallowed(policy) + false | true | false | false | false | be_disallowed(policy) + true | false | false | false | false | be_allowed(policy) + true | true | false | false | false | be_allowed(policy) # When Group actor belongs to a group which requires licensed seat for chat - true | false | true | be_disallowed(policy) - true | true | true | be_allowed(policy) + true | false | true | false | false | be_disallowed(policy) + true | true | true | false | false | be_allowed(policy) + + # When Duo chat is self-hosted + false | false | false | true | false | be_disallowed(policy) + false | false | false | true | true | be_allowed(policy) end with_them do @@ -661,6 +665,16 @@ allow(duo_chat_service_data).to receive(:allowed_for?).with(current_user).and_return(duo_pro_seat_assigned) allow(current_user).to receive(:belongs_to_group_requires_licensed_seat_for_chat?) .and_return(requires_licensed_seat) + + allow(::Ai::FeatureSetting).to receive_message_chain(:find_by_feature, + :self_hosted?).and_return(self_hosted_duo_chat_enabled) + self_hosted_service_data = instance_double(CloudConnector::SelfSigned::AvailableServiceData) + allow(CloudConnector::AvailableServices).to receive(:find_by_name).with(:self_hosted_models) + .and_return(self_hosted_service_data) + allow(self_hosted_service_data).to receive(:allowed_for?) + .with(current_user).and_return(self_hosted_duo_chat_available) + allow(self_hosted_service_data).to receive(:free_access?) + .and_return(self_hosted_duo_chat_available) end it { is_expected.to duo_chat_enabled_for_user } @@ -672,19 +686,22 @@ let_it_be(:yesterday) { Time.current - 1.day } where(:licensed, :duo_features_enabled, :duo_chat_cut_off_date, :duo_pro_seat_assigned, - :requires_licensed_seat_sm, :duo_chat_enabled_for_user) do - true | false | ref(:tomorrow) | false | false | be_disallowed(policy) - true | true | ref(:tomorrow) | false | false | be_allowed(policy) - true | true | ref(:tomorrow) | false | true | be_disallowed(policy) - false | false | ref(:tomorrow) | false | false | be_disallowed(policy) - false | true | ref(:tomorrow) | false | false | be_disallowed(policy) - false | true | ref(:tomorrow) | true | false | be_disallowed(policy) - false | true | ref(:yesterday) | false | false | be_disallowed(policy) - false | true | ref(:yesterday) | true | false | be_disallowed(policy) - false | false | ref(:yesterday) | true | false | be_disallowed(policy) - true | false | ref(:yesterday) | true | false | be_allowed(policy) - true | false | ref(:yesterday) | true | true | be_allowed(policy) - true | true | ref(:yesterday) | false | false | be_disallowed(policy) + :requires_licensed_seat_sm, :self_hosted_duo_chat_enabled, :self_hosted_duo_chat_available, + :duo_chat_enabled_for_user) do + true | false | ref(:tomorrow) | false | false | false | false | be_disallowed(policy) + true | true | ref(:tomorrow) | false | false | false | false | be_allowed(policy) + true | true | ref(:tomorrow) | false | true | false | false | be_disallowed(policy) + false | false | ref(:tomorrow) | false | false | false | false | be_disallowed(policy) + false | true | ref(:tomorrow) | false | false | false | false | be_disallowed(policy) + false | true | ref(:tomorrow) | true | false | false | false | be_disallowed(policy) + false | true | ref(:yesterday) | false | false | false | false | be_disallowed(policy) + false | true | ref(:yesterday) | true | false | false | false | be_disallowed(policy) + false | false | ref(:yesterday) | true | false | false | false | be_disallowed(policy) + true | false | ref(:yesterday) | true | false | false | false | be_allowed(policy) + true | false | ref(:yesterday) | true | true | false | false | be_allowed(policy) + true | true | ref(:yesterday) | false | false | false | false | be_disallowed(policy) + true | true | ref(:yesterday) | false | false | true | false | be_disallowed(policy) + true | true | ref(:yesterday) | false | false | true | true | be_allowed(policy) end with_them do @@ -699,6 +716,16 @@ allow(CloudConnector::AvailableServices).to receive(:find_by_name) .with(:duo_chat).and_return(duo_chat_service_data) allow(duo_chat_service_data).to receive(:allowed_for?).with(current_user).and_return(duo_pro_seat_assigned) + + allow(::Ai::FeatureSetting).to receive_message_chain(:find_by_feature, + :self_hosted?).and_return(self_hosted_duo_chat_enabled) + self_hosted_service_data = instance_double(CloudConnector::SelfSigned::AvailableServiceData) + allow(CloudConnector::AvailableServices).to receive(:find_by_name).with(:self_hosted_models) + .and_return(self_hosted_service_data) + allow(self_hosted_service_data).to receive(:allowed_for?) + .with(current_user).and_return(self_hosted_duo_chat_available) + allow(self_hosted_service_data).to receive(:free_access?) + .and_return(self_hosted_duo_chat_available) end it { is_expected.to duo_chat_enabled_for_user }