diff --git a/web/src/components/settings/index.vue b/web/src/components/settings/index.vue
index 5ba587d2e6..4999d197ef 100644
--- a/web/src/components/settings/index.vue
+++ b/web/src/components/settings/index.vue
@@ -326,7 +326,7 @@ export default defineComponent({
description: "LLM providers for online evaluations",
icon: "smart-toy",
to: { name: "llmProviders", query: { org_identifier: org } },
- visible: !!z.online_evals_enabled,
+ visible: (isEnt || isCloud) && !!z.online_evals_enabled,
dataTest: "llm-providers-tab",
group: "Data & AI",
},
diff --git a/web/src/composables/router.spec.ts b/web/src/composables/router.spec.ts
index d9523dc56d..c701e44cc1 100644
--- a/web/src/composables/router.spec.ts
+++ b/web/src/composables/router.spec.ts
@@ -24,10 +24,6 @@ vi.mock("./shared/useIngestionRoutes", () => ({
default: vi.fn(() => mockIngestionRoutes),
}));
-vi.mock("./shared/useAIRoutes", () => ({
- default: vi.fn(() => ({ homeChildRoutes: [] })),
-}));
-
import useOSRoutes from "./router";
import useIngestionRoutes from "./shared/useIngestionRoutes";
diff --git a/web/src/composables/router.ts b/web/src/composables/router.ts
index 11f7287eee..1b1895208e 100644
--- a/web/src/composables/router.ts
+++ b/web/src/composables/router.ts
@@ -14,17 +14,11 @@
// along with this program. If not, see .
import useIngestionRoutes from "./shared/useIngestionRoutes";
-import useAIRoutes from "./shared/useAIRoutes";
const useOSRoutes = () => {
const parentRoutes: any = [];
- const ai = useAIRoutes();
-
- const homeChildRoutes: any[] = [
- ...useIngestionRoutes(),
- ...ai.homeChildRoutes,
- ];
+ const homeChildRoutes: any[] = [...useIngestionRoutes()];
return { parentRoutes, homeChildRoutes };
};
diff --git a/web/src/composables/shared/useAIRoutes.ts b/web/src/composables/shared/useAIRoutes.ts
deleted file mode 100644
index 91114e501e..0000000000
--- a/web/src/composables/shared/useAIRoutes.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright 2026 OpenObserve Inc.
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-// AI Observability / Online Evals routes. Shared by both the open-source
-// router (@/composables/router) and the enterprise router
-// (@/enterprise/composables/router) so the `/ai` feature works in every build.
-// Visibility of the sidebar entry is still gated on the `online_evals_enabled`
-// zoConfig flag in MainLayout.vue — these routes only ensure the pages exist.
-
-import OnlineEvals from "@/enterprise/components/OnlineEvals.vue";
-import { routeGuard } from "@/utils/zincutils";
-
-const AIObservabilityShell = () =>
- import("@/enterprise/views/AIObservability/Index.vue");
-const AILLMInsightsPage = () =>
- import("@/enterprise/views/AIObservability/LLMInsightsPage.vue");
-const AISessionsPage = () =>
- import("@/enterprise/views/AIObservability/SessionsPage.vue");
-
-const useAIRoutes = () => {
- // Mounted directly under the MainLayout (home) children.
- const homeChildRoutes: any[] = [
- {
- path: "ai",
- component: AIObservabilityShell,
- beforeEnter(to: any, from: any, next: any) {
- routeGuard(to, from, next);
- },
- meta: {
- title: "AI Monitoring",
- keepAlive: false,
- },
- children: [
- {
- path: "",
- name: "aiObservability",
- redirect: { name: "aiLLMInsights" },
- },
- {
- path: "llm-insights",
- name: "aiLLMInsights",
- component: AILLMInsightsPage,
- meta: { title: "LLM Insights", keepAlive: false },
- },
- {
- path: "sessions",
- name: "aiSessions",
- component: AISessionsPage,
- meta: { title: "Sessions", keepAlive: false },
- },
- {
- path: "evaluations",
- name: "aiEvaluations",
- component: OnlineEvals,
- props: { hideTabBar: true },
- meta: { title: "Evaluations", keepAlive: false },
- },
- ],
- },
- {
- // Legacy URL — keep saved/bookmarked links working
- path: "online-evals",
- redirect: { name: "aiEvaluations" },
- },
- ];
-
- return { homeChildRoutes };
-};
-
-export default useAIRoutes;
diff --git a/web/src/composables/shared/useManagementRoutes.spec.ts b/web/src/composables/shared/useManagementRoutes.spec.ts
index 946aa62e5d..f63769af85 100644
--- a/web/src/composables/shared/useManagementRoutes.spec.ts
+++ b/web/src/composables/shared/useManagementRoutes.spec.ts
@@ -145,6 +145,11 @@ describe("useManagementRoutes", () => {
expect(templateRoute.path).toBe("templates");
});
+ it("should NOT have llmProviders route in OSS builds", () => {
+ const llmRoute = routes[0].children.find((child: any) => child.name === "llmProviders");
+ expect(llmRoute).toBeUndefined();
+ });
+
it("should have beforeEnter hook for general route", () => {
const generalRoute = routes[0].children.find((child: any) => child.name === "general");
expect(typeof generalRoute.beforeEnter).toBe("function");
@@ -502,63 +507,63 @@ describe("useManagementRoutes", () => {
config.isEnterprise = "false";
config.isCloud = "false";
const routes = useManagementRoutes();
- expect(routes[0].children).toHaveLength(7); // Only base routes
+ expect(routes[0].children).toHaveLength(6); // Only base routes (llm_providers is enterprise/cloud-only)
});
it("should handle isCloud as string 'false'", () => {
config.isEnterprise = "false";
config.isCloud = "false";
const routes = useManagementRoutes();
- expect(routes[0].children).toHaveLength(7); // Only base routes
+ expect(routes[0].children).toHaveLength(6); // Only base routes (llm_providers is enterprise/cloud-only)
});
it("should handle isEnterprise as undefined", () => {
(config as any).isEnterprise = undefined;
config.isCloud = "false";
const routes = useManagementRoutes();
- expect(routes[0].children).toHaveLength(7); // Only base routes
+ expect(routes[0].children).toHaveLength(6); // Only base routes (llm_providers is enterprise/cloud-only)
});
it("should handle isCloud as undefined", () => {
config.isEnterprise = "false";
(config as any).isCloud = undefined;
const routes = useManagementRoutes();
- expect(routes[0].children).toHaveLength(7); // Only base routes
+ expect(routes[0].children).toHaveLength(6); // Only base routes (llm_providers is enterprise/cloud-only)
});
it("should handle both config values as undefined", () => {
(config as any).isEnterprise = undefined;
(config as any).isCloud = undefined;
const routes = useManagementRoutes();
- expect(routes[0].children).toHaveLength(7); // Only base routes
+ expect(routes[0].children).toHaveLength(6); // Only base routes (llm_providers is enterprise/cloud-only)
});
it("should handle isEnterprise as non-string truthy value", () => {
(config as any).isEnterprise = true;
config.isCloud = "false";
const routes = useManagementRoutes();
- expect(routes[0].children).toHaveLength(7); // Only base routes, since config comparison is strict "true"
+ expect(routes[0].children).toHaveLength(6); // Only base routes (llm_providers is enterprise/cloud-only), since config comparison is strict "true"
});
it("should handle isCloud as non-string truthy value", () => {
config.isEnterprise = "false";
(config as any).isCloud = true;
const routes = useManagementRoutes();
- expect(routes[0].children).toHaveLength(7); // Only base routes, since config comparison is strict "true"
+ expect(routes[0].children).toHaveLength(6); // Only base routes (llm_providers is enterprise/cloud-only), since config comparison is strict "true"
});
it("should handle empty string for isEnterprise", () => {
config.isEnterprise = "";
config.isCloud = "false";
const routes = useManagementRoutes();
- expect(routes[0].children).toHaveLength(7); // Only base routes
+ expect(routes[0].children).toHaveLength(6); // Only base routes (llm_providers is enterprise/cloud-only)
});
it("should handle empty string for isCloud", () => {
config.isEnterprise = "false";
config.isCloud = "";
const routes = useManagementRoutes();
- expect(routes[0].children).toHaveLength(7); // Only base routes
+ expect(routes[0].children).toHaveLength(6); // Only base routes (llm_providers is enterprise/cloud-only)
});
it("should return the same structure on multiple calls", () => {
diff --git a/web/src/composables/shared/useManagementRoutes.ts b/web/src/composables/shared/useManagementRoutes.ts
index 0d15148416..7aaf0a193c 100644
--- a/web/src/composables/shared/useManagementRoutes.ts
+++ b/web/src/composables/shared/useManagementRoutes.ts
@@ -89,23 +89,25 @@ const useManagementRoutes = () => {
routeGuard(to, from, next);
},
},
- {
- // Available in all builds (used by the AI Observability / Online
- // Evals flows), not just enterprise.
- path: "llm_providers",
- name: "llmProviders",
- component: () =>
- import("@/components/settings/LlmProvidersSettings.vue"),
- meta: {
- title: "LLM Providers",
- },
- beforeEnter(to: any, from: any, next: any) {
- routeGuard(to, from, next);
- },
- },
],
},
];
+ // LLM Providers (used by the AI Observability / Online Evals flows) is an
+ // enterprise/cloud-only feature — not available in OSS builds.
+ if (config.isEnterprise == "true" || config.isCloud == "true") {
+ routes[0].children.push({
+ path: "llm_providers",
+ name: "llmProviders",
+ component: () =>
+ import("@/components/settings/LlmProvidersSettings.vue"),
+ meta: {
+ title: "LLM Providers",
+ },
+ beforeEnter(to: any, from: any, next: any) {
+ routeGuard(to, from, next);
+ },
+ });
+ }
if (config.isEnterprise == "true") {
routes[0].children.push(
...[
diff --git a/web/src/enterprise/composables/router.ts b/web/src/enterprise/composables/router.ts
index d83fa899fe..0fd663f637 100644
--- a/web/src/enterprise/composables/router.ts
+++ b/web/src/enterprise/composables/router.ts
@@ -20,13 +20,17 @@ import Usage from "@/enterprise/components/billings/usage.vue";
import BillingGroup from "@/enterprise/components/billings/BillingGroup.vue";
import AzureMarketplaceSetup from "@/views/AzureMarketplaceSetup.vue";
import AwsMarketplaceSetup from "@/views/AwsMarketplaceSetup.vue";
-import useAIRoutes from "@/composables/shared/useAIRoutes";
+import OnlineEvals from "@/enterprise/components/OnlineEvals.vue";
+import { routeGuard } from "@/utils/zincutils";
+
+const AIObservabilityShell = () =>
+ import("@/enterprise/views/AIObservability/Index.vue");
+const AILLMInsightsPage = () =>
+ import("@/enterprise/views/AIObservability/LLMInsightsPage.vue");
+const AISessionsPage = () =>
+ import("@/enterprise/views/AIObservability/SessionsPage.vue");
const useEnvRoutes = () => {
- // AI Observability / Online Evals routes are shared with the open-source
- // router so the feature is available in every build.
- const ai = useAIRoutes();
-
// Note: AWS Marketplace registration is handled by backend at POST /api/aws-marketplace/register
// The backend sets a cookie and redirects to Dex login
const parentRoutes: any = [
@@ -57,7 +61,48 @@ const useEnvRoutes = () => {
];
const homeChildRoutes = [
- ...ai.homeChildRoutes,
+ {
+ path: "ai",
+ component: AIObservabilityShell,
+ beforeEnter(to: any, from: any, next: any) {
+ routeGuard(to, from, next);
+ },
+ meta: {
+ title: "AI Monitoring",
+ keepAlive: false,
+ },
+ children: [
+ {
+ path: "",
+ name: "aiObservability",
+ redirect: { name: "aiLLMInsights" },
+ },
+ {
+ path: "llm-insights",
+ name: "aiLLMInsights",
+ component: AILLMInsightsPage,
+ meta: { title: "LLM Insights", keepAlive: false },
+ },
+ {
+ path: "sessions",
+ name: "aiSessions",
+ component: AISessionsPage,
+ meta: { title: "Sessions", keepAlive: false },
+ },
+ {
+ path: "evaluations",
+ name: "aiEvaluations",
+ component: OnlineEvals,
+ props: { hideTabBar: true },
+ meta: { title: "Evaluations", keepAlive: false },
+ },
+ ],
+ },
+ {
+ // Legacy URL — keep saved/bookmarked links working
+ path: "online-evals",
+ redirect: { name: "aiEvaluations" },
+ },
{
path: "billings",
name: "billings",
diff --git a/web/src/layouts/MainLayout.vue b/web/src/layouts/MainLayout.vue
index d80ca3eaad..a46a4864fc 100644
--- a/web/src/layouts/MainLayout.vue
+++ b/web/src/layouts/MainLayout.vue
@@ -333,7 +333,10 @@ export default defineComponent({
// of whether the config response arrived before or after this component
// mounted.
const isOnlineEvalsEnabled = computed(() => {
- return Boolean(store.state.zoConfig?.online_evals_enabled);
+ return (
+ (config.isEnterprise == "true" || config.isCloud == "true") &&
+ Boolean(store.state.zoConfig?.online_evals_enabled)
+ );
});
const orgOptions = ref([{ label: Number, value: String }]);