From a0dabdaa2198cc279e05bf26f3ad844ebae21a41 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:09:25 +0800 Subject: [PATCH] feat(service-ai): registerRoutes option to suppress host route mounting AIServicePluginOptions gains `registerRoutes?: boolean` (default true). When false, the plugin still caches route defs on the kernel (`__aiRoutes`) but does NOT fire the `ai:routes` hook, so its concrete routes are not mounted on the shared HTTP server. This is for a host/routing-shell AIService in a multi-tenant runtime: mounting concrete AI routes on the host shadows the dispatcher's `/ai/*` wildcard, which resolves the environment and dispatches to the per-env kernel's AIService. Single-env runtimes are unaffected (default true). Co-Authored-By: Claude Opus 4.8 --- packages/services/service-ai/src/plugin.ts | 27 ++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/services/service-ai/src/plugin.ts b/packages/services/service-ai/src/plugin.ts index 92bceb321..5c263aeb7 100644 --- a/packages/services/service-ai/src/plugin.ts +++ b/packages/services/service-ai/src/plugin.ts @@ -56,6 +56,16 @@ export interface AIServicePluginOptions { * behavior). Pairs with the gateway adapter only; ignored by other providers. */ gatewayModel?: string; + /** + * Whether to mount this plugin's HTTP routes (fire the `ai:routes` hook). + * Defaults to `true`. Set `false` for an AIService on a host/routing-shell + * kernel in a multi-tenant runtime: the host should NOT serve concrete AI + * routes (they would shadow the dispatcher's `/ai/*` wildcard, which resolves + * the request's environment and dispatches to the per-environment kernel's + * AIService). Route definitions are still cached on the kernel (`__aiRoutes`) + * so the dispatcher can match them per-env. Single-env runtimes leave this on. + */ + registerRoutes?: boolean; /** * Explicit trace recorder override. When set, auto-detection * of {@link ObjectQLTraceRecorder} is skipped. @@ -906,15 +916,24 @@ export class AIServicePlugin implements Plugin { ctx.logger.debug('[AI] Metadata service not available, skipping agent and assistant routes'); } - // Trigger hook so HTTP server plugins can mount these routes - await ctx.trigger('ai:routes', routes); - - // Cache routes on the kernel so HttpDispatcher can find them + // Cache routes on the kernel so HttpDispatcher can find them (always — the + // per-env dispatch path reads `__aiRoutes` to match routes for this kernel). const kernel = ctx.getKernel(); if (kernel) { (kernel as any).__aiRoutes = routes; } + // Trigger hook so HTTP server plugins can MOUNT these routes on the shared + // server. Skipped when `registerRoutes === false` (a host/routing-shell + // AIService in multi-tenant mode): mounting concrete routes there would + // shadow the dispatcher's `/ai/*` wildcard and serve every tenant's chat + // from the host instead of its own per-environment kernel. + if (this.options.registerRoutes !== false) { + await ctx.trigger('ai:routes', routes); + } else { + ctx.logger.info('[AI] registerRoutes=false — not mounting AI routes on this kernel (multi-tenant host)'); + } + ctx.logger.info( `[AI] Service started — adapter="${this.service.adapterName}", ` + `tools=${this.service.toolRegistry.size}, ` +