diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index 4abf36ac582f61dbce75eeb6b2fd82b15019c780..e3d5de80f68f8e218c7ef94d1a3e8774ca10e674 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -334,6 +334,7 @@ Returns [`AvailableModels`](#availablemodels).
| Name | Type | Description |
| ---- | ---- | ----------- |
+| `projectId` | [`ProjectID`](#projectid) | Global ID of the project the user is acting on. |
| `rootNamespaceId` | [`GroupID!`](#groupid) | Global ID of the namespace the user is acting on. |
### `Query.aiChatContextPresets`
diff --git a/ee/app/assets/javascripts/ai/duo_agentic_chat/components/duo_agentic_chat.vue b/ee/app/assets/javascripts/ai/duo_agentic_chat/components/duo_agentic_chat.vue
index 4b468abd92a5d9bcd60be505e5e41f221b51978b..8af978c34aae5c2e62f569c9ad2fa29c35a98169 100644
--- a/ee/app/assets/javascripts/ai/duo_agentic_chat/components/duo_agentic_chat.vue
+++ b/ee/app/assets/javascripts/ai/duo_agentic_chat/components/duo_agentic_chat.vue
@@ -159,6 +159,7 @@ export default {
variables() {
return {
rootNamespaceId: this.rootNamespaceId,
+ projectId: this.projectId,
};
},
update(data) {
diff --git a/ee/app/assets/javascripts/ai/graphql/get_ai_chat_available_models.query.graphql b/ee/app/assets/javascripts/ai/graphql/get_ai_chat_available_models.query.graphql
index def23d46b46020f4dc1815ba8213a9f3bde77b5e..289af54d2bed74d2a8ceb607bc13e3aa62daba69 100644
--- a/ee/app/assets/javascripts/ai/graphql/get_ai_chat_available_models.query.graphql
+++ b/ee/app/assets/javascripts/ai/graphql/get_ai_chat_available_models.query.graphql
@@ -1,5 +1,5 @@
-query getAiChatAvailableModels($rootNamespaceId: GroupID!) {
- aiChatAvailableModels(rootNamespaceId: $rootNamespaceId) {
+query getAiChatAvailableModels($rootNamespaceId: GroupID!, $projectId: ProjectID) {
+ aiChatAvailableModels(rootNamespaceId: $rootNamespaceId, projectId: $projectId) {
defaultModel {
name
ref
diff --git a/ee/app/graphql/resolvers/ai/chat/available_models_resolver.rb b/ee/app/graphql/resolvers/ai/chat/available_models_resolver.rb
index 2650f8c4ba80eeccc0809dd512d31114dbb07c2f..681ce4e7e51fc218a985adb813becbbe634ffa58 100644
--- a/ee/app/graphql/resolvers/ai/chat/available_models_resolver.rb
+++ b/ee/app/graphql/resolvers/ai/chat/available_models_resolver.rb
@@ -15,8 +15,18 @@ class AvailableModelsResolver < BaseResolver
required: true,
description: 'Global ID of the namespace the user is acting on.'
- def resolve(root_namespace_id:)
- namespace = authorized_find!(id: root_namespace_id)
+ argument :project_id,
+ ::Types::GlobalIDType[::Project],
+ required: false,
+ description: 'Global ID of the project the user is acting on.'
+
+ def resolve(root_namespace_id:, project_id: nil)
+ if project_id
+ project = authorized_find!(id: project_id)
+ namespace = project.root_namespace
+ else
+ namespace = authorized_find!(id: root_namespace_id)
+ end
result = ::Ai::ModelSelection::FetchModelDefinitionsService
.new(current_user, model_selection_scope: namespace)
diff --git a/ee/spec/graphql/resolvers/ai/chat/available_models_resolver_spec.rb b/ee/spec/graphql/resolvers/ai/chat/available_models_resolver_spec.rb
index fbe71301fe6a8c122ccae217ad685035bc438bd2..8894acdf5e069362d16d2a6cb4b7640e29aff0ca 100644
--- a/ee/spec/graphql/resolvers/ai/chat/available_models_resolver_spec.rb
+++ b/ee/spec/graphql/resolvers/ai/chat/available_models_resolver_spec.rb
@@ -21,6 +21,61 @@
.and_return(true)
end
+ shared_examples "returns model selection data" do
+ it "returns the correct structure with default and selectable models" do
+ expect(resolver).to eq(expected_result)
+ end
+ end
+
+ shared_examples "returns a ResourceNotAvailable error" do
+ it "generates an error" do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
+ resolver
+ end
+ end
+ end
+
+ describe "the resource is not authorized" do
+ context "when the resource is a root namespace" do
+ context "when the namespace does not have duo agentic chat enabled" do
+ before do
+ allow(Ability).to receive(:allowed?)
+ .with(current_user, :access_duo_agentic_chat, group)
+ .and_return(false)
+ end
+
+ include_examples "returns a ResourceNotAvailable error"
+ end
+ end
+
+ context "when the resource is a project" do
+ context "when the namespace has duo agentic chat enabled" do
+ before do
+ allow(Ability).to receive(:allowed?)
+ .with(current_user, :access_duo_agentic_chat, group)
+ .and_return(true)
+ end
+
+ context "when the child project does not have duo agentic chat enabled" do
+ let_it_be(:project) { create(:project, group: group) }
+
+ let(:args) do
+ { root_namespace_id: GitlabSchema.id_from_object(group),
+ project_id: GitlabSchema.id_from_object(project) }
+ end
+
+ before do
+ allow(Ability).to receive(:allowed?)
+ .with(current_user, :access_duo_agentic_chat, project)
+ .and_return(false)
+ end
+
+ include_examples "returns a ResourceNotAvailable error"
+ end
+ end
+ end
+ end
+
context "when service returns successful result" do
let(:service_result) do
ServiceResponse.success(payload: {
@@ -41,21 +96,54 @@
})
end
- before do
- allow_next_instance_of(::Ai::ModelSelection::FetchModelDefinitionsService) do |service|
- allow(service).to receive(:execute).and_return(service_result)
- end
- end
-
- it "returns the correct structure with default and selectable models" do
- expect(resolver).to eq({
+ let(:expected_result) do
+ {
default_model: { name: "Claude Sonnet 4.0 - Anthropic", ref: "claude_sonnet_4_20250514" },
selectable_models: [
{ name: "Claude Sonnet 4.0 - Anthropic", ref: "claude_sonnet_4_20250514" },
{ name: "Claude Sonnet 3.7 - Anthropic", ref: "claude_sonnet_3_7_20250219" }
],
pinned_model: nil
- })
+ }
+ end
+
+ before do
+ allow_next_instance_of(::Ai::ModelSelection::FetchModelDefinitionsService) do |service|
+ allow(service).to receive(:execute).and_return(service_result)
+ end
+ end
+
+ context "when the resource is a root namespace" do
+ context "when the namespace has duo agentic chat enabled" do
+ include_examples "returns model selection data"
+ end
+ end
+
+ context "when the resource is a project" do
+ context "when the namespace does not have duo agentic chat enabled" do
+ before do
+ allow(Ability).to receive(:allowed?)
+ .with(current_user, :access_duo_agentic_chat, group)
+ .and_return(false)
+ end
+
+ context "when the child project has duo agentic chat enabled" do
+ let_it_be(:project) { create(:project, group: group) }
+
+ let(:args) do
+ { root_namespace_id: GitlabSchema.id_from_object(group),
+ project_id: GitlabSchema.id_from_object(project) }
+ end
+
+ before do
+ allow(Ability).to receive(:allowed?)
+ .with(current_user, :access_duo_agentic_chat, project)
+ .and_return(true)
+ end
+
+ include_examples "returns model selection data"
+ end
+ end
end
context "when there is a pinned model" do