@open-mercato/ai-assistant 0.6.1-develop.3246.1.dbef9d7392 → 0.6.1-develop.3256.1.fe3dec2464
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +82 -18
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js +370 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +194 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/route.js +4 -0
- package/dist/modules/ai_assistant/api/ai/agents/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +169 -5
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/route/route.js +38 -19
- package/dist/modules/ai_assistant/api/route/route.js.map +3 -3
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js +195 -0
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/settings/route.js +537 -22
- package/dist/modules/ai_assistant/api/settings/route.js.map +3 -3
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +701 -147
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +338 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.js +25 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js +1 -1
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +75 -26
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.js +25 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js +503 -168
- package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities/AiTenantModelAllowlist.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiTenantModelAllowlist.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities.js +123 -1
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +157 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +7 -0
- package/dist/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.js +77 -0
- package/dist/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.js.map +7 -0
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js +1 -1
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +90 -1
- package/dist/modules/ai_assistant/i18n/en.json +90 -1
- package/dist/modules/ai_assistant/i18n/es.json +90 -1
- package/dist/modules/ai_assistant/i18n/pl.json +90 -1
- package/dist/modules/ai_assistant/lib/agent-registry.js +17 -1
- package/dist/modules/ai_assistant/lib/agent-registry.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-runtime.js +133 -36
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
- package/dist/modules/ai_assistant/lib/baseurl-allowlist.js +29 -0
- package/dist/modules/ai_assistant/lib/baseurl-allowlist.js.map +7 -0
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js +4 -1
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +2 -2
- package/dist/modules/ai_assistant/lib/llm-adapters/google.js +4 -1
- package/dist/modules/ai_assistant/lib/llm-adapters/google.js.map +2 -2
- package/dist/modules/ai_assistant/lib/model-allowlist.js +211 -0
- package/dist/modules/ai_assistant/lib/model-allowlist.js.map +7 -0
- package/dist/modules/ai_assistant/lib/model-factory.js +203 -31
- package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +32 -1
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +2 -2
- package/dist/modules/ai_assistant/migrations/Migration20260508140000.js +18 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508140000.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512090000.js +16 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512090000.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512130000.js +15 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512130000.js.map +7 -0
- package/generated/entities/ai_agent_runtime_override/index.ts +13 -0
- package/generated/entities/ai_tenant_model_allowlist/index.ts +9 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +26 -0
- package/jest.config.cjs +2 -0
- package/package.json +4 -4
- package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts +477 -0
- package/src/modules/ai_assistant/__tests__/settings-page-logic.test.ts +116 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/__tests__/route.test.ts +240 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +251 -0
- package/src/modules/ai_assistant/api/ai/agents/route.ts +4 -0
- package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +273 -0
- package/src/modules/ai_assistant/api/ai/chat/route.ts +211 -2
- package/src/modules/ai_assistant/api/route/route.ts +49 -25
- package/src/modules/ai_assistant/api/settings/__tests__/route.test.ts +408 -0
- package/src/modules/ai_assistant/api/settings/allowlist/route.ts +221 -0
- package/src/modules/ai_assistant/api/settings/route.ts +721 -27
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +858 -177
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +458 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.ts +23 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.tsx +12 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.tsx +1 -1
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +89 -12
- package/src/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.ts +23 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/settings/page.tsx +18 -0
- package/src/modules/ai_assistant/components/AiAssistantSettingsPageClient.tsx +617 -209
- package/src/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.ts +7 -0
- package/src/modules/ai_assistant/data/entities/AiTenantModelAllowlist.ts +2 -0
- package/src/modules/ai_assistant/data/entities.ts +164 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +227 -0
- package/src/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.ts +132 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentRuntimeOverrideRepository.test.ts +337 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiTenantModelAllowlistRepository.test.ts +181 -0
- package/src/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.tsx +1 -1
- package/src/modules/ai_assistant/i18n/de.json +90 -1
- package/src/modules/ai_assistant/i18n/en.json +90 -1
- package/src/modules/ai_assistant/i18n/es.json +90 -1
- package/src/modules/ai_assistant/i18n/pl.json +90 -1
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +396 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +60 -6
- package/src/modules/ai_assistant/lib/__tests__/ai-api-operation-runner.test.ts +4 -2
- package/src/modules/ai_assistant/lib/__tests__/baseurl-allowlist.test.ts +75 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-anthropic.test.ts +18 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-google.test.ts +18 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +150 -4
- package/src/modules/ai_assistant/lib/__tests__/model-allowlist.test.ts +290 -0
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +634 -0
- package/src/modules/ai_assistant/lib/agent-registry.ts +20 -1
- package/src/modules/ai_assistant/lib/agent-runtime.ts +220 -44
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +48 -0
- package/src/modules/ai_assistant/lib/baseurl-allowlist.ts +64 -0
- package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +11 -1
- package/src/modules/ai_assistant/lib/llm-adapters/google.ts +4 -1
- package/src/modules/ai_assistant/lib/model-allowlist.ts +407 -0
- package/src/modules/ai_assistant/lib/model-factory.ts +486 -58
- package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +44 -0
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +704 -235
- package/src/modules/ai_assistant/migrations/Migration20260508140000.ts +18 -0
- package/src/modules/ai_assistant/migrations/Migration20260512090000.ts +16 -0
- package/src/modules/ai_assistant/migrations/Migration20260512130000.ts +13 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
|
+
import { llmProviderRegistry } from "@open-mercato/shared/lib/ai/llm-provider-registry";
|
|
6
|
+
import { getAgent, loadAgentRegistry } from "../../../../../lib/agent-registry.js";
|
|
7
|
+
import { hasRequiredFeatures } from "../../../../../lib/auth.js";
|
|
8
|
+
import { createModelFactory } from "../../../../../lib/model-factory.js";
|
|
9
|
+
import {
|
|
10
|
+
hasAllowlistSnapshotRestrictions,
|
|
11
|
+
intersectEffectiveAllowlistWithSnapshot,
|
|
12
|
+
intersectAllowlists,
|
|
13
|
+
isModelAllowedForProviderInEffective,
|
|
14
|
+
isProviderAllowedInEffective,
|
|
15
|
+
readAgentRuntimeOverrideAllowlist
|
|
16
|
+
} from "../../../../../lib/model-allowlist.js";
|
|
17
|
+
import { AiTenantModelAllowlistRepository } from "../../../../../data/repositories/AiTenantModelAllowlistRepository.js";
|
|
18
|
+
import { AiAgentRuntimeOverrideRepository } from "../../../../../data/repositories/AiAgentRuntimeOverrideRepository.js";
|
|
19
|
+
function modelsForPicker(provider, allowedModelIds) {
|
|
20
|
+
if (provider.defaultModels.length > 0) return provider.defaultModels;
|
|
21
|
+
return (allowedModelIds ?? []).map((id) => ({ id, name: id }));
|
|
22
|
+
}
|
|
23
|
+
const agentIdPattern = /^[a-z0-9_]+\.[a-z0-9_]+$/;
|
|
24
|
+
const agentIdParamSchema = z.object({
|
|
25
|
+
agentId: z.string().regex(agentIdPattern, 'agentId must match "<module>.<agent>" (lowercase, digits, underscores only)')
|
|
26
|
+
});
|
|
27
|
+
const openApi = {
|
|
28
|
+
tag: "AI Assistant",
|
|
29
|
+
summary: "Available models for an AI agent",
|
|
30
|
+
methods: {
|
|
31
|
+
GET: {
|
|
32
|
+
operationId: "aiAssistantGetAgentModels",
|
|
33
|
+
summary: "Get the providers and curated models available for the chat-UI picker for this agent",
|
|
34
|
+
description: 'Returns all configured providers with their curated model catalogs, filtered to providers that have an API key configured in the current environment. When the agent declares `allowRuntimeModelOverride: false`, the response reflects that constraint so the UI picker can hide itself. Includes the agent\'s resolved default provider/model so the picker can render a "(default)" badge next to the right entry. RBAC: requires the same features as the agent itself (typically `ai_assistant.view`).',
|
|
35
|
+
responses: [
|
|
36
|
+
{
|
|
37
|
+
status: 200,
|
|
38
|
+
description: "Providers and curated models available for the agent picker. Empty `providers` array when `allowRuntimeModelOverride` is false."
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
errors: [
|
|
42
|
+
{ status: 401, description: "Unauthenticated." },
|
|
43
|
+
{ status: 403, description: "Caller lacks the agent's required features." },
|
|
44
|
+
{ status: 404, description: "Unknown agent id." }
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const metadata = {
|
|
50
|
+
GET: { requireAuth: true, requireFeatures: ["ai_assistant.view"] }
|
|
51
|
+
};
|
|
52
|
+
async function GET(req, { params }) {
|
|
53
|
+
const auth = await getAuthFromRequest(req);
|
|
54
|
+
if (!auth?.sub) {
|
|
55
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
56
|
+
}
|
|
57
|
+
const rawParams = await params;
|
|
58
|
+
const paramResult = agentIdParamSchema.safeParse(rawParams);
|
|
59
|
+
if (!paramResult.success) {
|
|
60
|
+
return NextResponse.json(
|
|
61
|
+
{ error: "Invalid agentId path parameter.", code: "validation_error", issues: paramResult.error.issues },
|
|
62
|
+
{ status: 400 }
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const agentId = paramResult.data.agentId;
|
|
66
|
+
try {
|
|
67
|
+
await loadAgentRegistry();
|
|
68
|
+
const container = await createRequestContainer();
|
|
69
|
+
const rbacService = container.resolve("rbacService");
|
|
70
|
+
const acl = await rbacService.loadAcl(auth.sub, {
|
|
71
|
+
tenantId: auth.tenantId,
|
|
72
|
+
organizationId: auth.orgId
|
|
73
|
+
});
|
|
74
|
+
const agent = getAgent(agentId);
|
|
75
|
+
if (!agent) {
|
|
76
|
+
return NextResponse.json({ error: `Agent "${agentId}" not found.`, code: "agent_unknown" }, { status: 404 });
|
|
77
|
+
}
|
|
78
|
+
const agentFeatures = agent.requiredFeatures ?? [];
|
|
79
|
+
if (agentFeatures.length > 0) {
|
|
80
|
+
const permitted = hasRequiredFeatures(agentFeatures, acl.features, acl.isSuperAdmin);
|
|
81
|
+
if (!permitted) {
|
|
82
|
+
return NextResponse.json(
|
|
83
|
+
{
|
|
84
|
+
error: `Access to agent "${agentId}" requires features: ${agentFeatures.join(", ")}.`,
|
|
85
|
+
code: "agent_features_denied"
|
|
86
|
+
},
|
|
87
|
+
{ status: 403 }
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const allowRuntimeModelOverride = agent.allowRuntimeModelOverride !== false;
|
|
92
|
+
let tenantAllowlistSnapshot = null;
|
|
93
|
+
let agentRuntimeOverrideAllowlist = null;
|
|
94
|
+
let tenantRuntimeOverride = null;
|
|
95
|
+
if (auth.tenantId) {
|
|
96
|
+
try {
|
|
97
|
+
const em = container.resolve("em");
|
|
98
|
+
const allowlistRepo = new AiTenantModelAllowlistRepository(em);
|
|
99
|
+
tenantAllowlistSnapshot = await allowlistRepo.getSnapshot({
|
|
100
|
+
tenantId: auth.tenantId,
|
|
101
|
+
organizationId: auth.orgId ?? null
|
|
102
|
+
});
|
|
103
|
+
const runtimeOverrideRepo = new AiAgentRuntimeOverrideRepository(em);
|
|
104
|
+
const runtimeOverrideDefaultRow = await runtimeOverrideRepo.getDefault({
|
|
105
|
+
tenantId: auth.tenantId,
|
|
106
|
+
organizationId: auth.orgId ?? null,
|
|
107
|
+
agentId
|
|
108
|
+
});
|
|
109
|
+
tenantRuntimeOverride = runtimeOverrideDefaultRow ? {
|
|
110
|
+
providerId: runtimeOverrideDefaultRow.providerId ?? null,
|
|
111
|
+
modelId: runtimeOverrideDefaultRow.modelId ?? null,
|
|
112
|
+
baseURL: runtimeOverrideDefaultRow.baseUrl ?? null
|
|
113
|
+
} : null;
|
|
114
|
+
const runtimeOverrideRow = await runtimeOverrideRepo.getExact({
|
|
115
|
+
tenantId: auth.tenantId,
|
|
116
|
+
organizationId: auth.orgId ?? null,
|
|
117
|
+
agentId
|
|
118
|
+
});
|
|
119
|
+
const tenantAgentAllowlist = runtimeOverrideRow ? {
|
|
120
|
+
allowedProviders: runtimeOverrideRow.allowedOverrideProviders ?? null,
|
|
121
|
+
allowedModelsByProvider: runtimeOverrideRow.allowedOverrideModelsByProvider ?? {}
|
|
122
|
+
} : null;
|
|
123
|
+
agentRuntimeOverrideAllowlist = hasAllowlistSnapshotRestrictions(tenantAgentAllowlist) ? tenantAgentAllowlist : null;
|
|
124
|
+
} catch (snapshotError) {
|
|
125
|
+
console.error("[AI Agents Models] Failed to load tenant allowlist:", snapshotError);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const factory = createModelFactory(container);
|
|
129
|
+
const defaultResolution = factory.resolveModel({
|
|
130
|
+
moduleId: agent.moduleId,
|
|
131
|
+
agentDefaultModel: agent.defaultModel,
|
|
132
|
+
agentDefaultProvider: agent.defaultProvider,
|
|
133
|
+
agentDefaultBaseUrl: agent.defaultBaseUrl,
|
|
134
|
+
allowRuntimeModelOverride,
|
|
135
|
+
tenantOverride: tenantRuntimeOverride ?? void 0,
|
|
136
|
+
tenantAllowlist: tenantAllowlistSnapshot
|
|
137
|
+
});
|
|
138
|
+
const defaultProviderId = defaultResolution.providerId;
|
|
139
|
+
const defaultModelId = defaultResolution.modelId;
|
|
140
|
+
const env = process.env;
|
|
141
|
+
const knownProviderIds = llmProviderRegistry.list().map((p) => p.id);
|
|
142
|
+
const baseEffectiveAllowlist = intersectAllowlists(
|
|
143
|
+
env,
|
|
144
|
+
knownProviderIds,
|
|
145
|
+
tenantAllowlistSnapshot
|
|
146
|
+
);
|
|
147
|
+
const envAgentAllowlist = readAgentRuntimeOverrideAllowlist(env, agentId, knownProviderIds);
|
|
148
|
+
const effectiveAllowlist = intersectEffectiveAllowlistWithSnapshot(
|
|
149
|
+
intersectEffectiveAllowlistWithSnapshot(
|
|
150
|
+
baseEffectiveAllowlist,
|
|
151
|
+
knownProviderIds,
|
|
152
|
+
envAgentAllowlist
|
|
153
|
+
),
|
|
154
|
+
knownProviderIds,
|
|
155
|
+
agentRuntimeOverrideAllowlist
|
|
156
|
+
);
|
|
157
|
+
const providers = allowRuntimeModelOverride ? llmProviderRegistry.list().filter((provider) => provider.isConfigured()).filter((provider) => isProviderAllowedInEffective(effectiveAllowlist, provider.id)).map((provider) => {
|
|
158
|
+
const allowedModelIds = effectiveAllowlist.modelsByProvider[provider.id];
|
|
159
|
+
const filteredModels = modelsForPicker(provider, allowedModelIds).filter(
|
|
160
|
+
(model) => isModelAllowedForProviderInEffective(effectiveAllowlist, provider.id, model.id)
|
|
161
|
+
);
|
|
162
|
+
return {
|
|
163
|
+
id: provider.id,
|
|
164
|
+
name: provider.name,
|
|
165
|
+
isDefault: provider.id === defaultProviderId,
|
|
166
|
+
models: filteredModels.map((model) => ({
|
|
167
|
+
id: model.id,
|
|
168
|
+
name: model.name,
|
|
169
|
+
contextWindow: model.contextWindow,
|
|
170
|
+
tags: model.tags,
|
|
171
|
+
isDefault: provider.id === defaultProviderId && model.id === defaultModelId
|
|
172
|
+
}))
|
|
173
|
+
};
|
|
174
|
+
}) : [];
|
|
175
|
+
return NextResponse.json({
|
|
176
|
+
agentId,
|
|
177
|
+
allowRuntimeModelOverride,
|
|
178
|
+
defaultProviderId,
|
|
179
|
+
defaultModelId,
|
|
180
|
+
defaultProviderName: llmProviderRegistry.get(defaultProviderId)?.name ?? defaultProviderId,
|
|
181
|
+
defaultModelName: llmProviderRegistry.get(defaultProviderId)?.defaultModels.find((model) => model.id === defaultModelId)?.name ?? defaultModelId,
|
|
182
|
+
providers
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error("[AI Agents Models] GET error:", error);
|
|
186
|
+
return NextResponse.json({ error: "Failed to resolve agent models." }, { status: 500 });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export {
|
|
190
|
+
GET,
|
|
191
|
+
metadata,
|
|
192
|
+
openApi
|
|
193
|
+
};
|
|
194
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../src/modules/ai_assistant/api/ai/agents/%5BagentId%5D/models/route.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { getAgent, loadAgentRegistry } from '../../../../../lib/agent-registry'\nimport { hasRequiredFeatures } from '../../../../../lib/auth'\nimport { createModelFactory } from '../../../../../lib/model-factory'\nimport {\n hasAllowlistSnapshotRestrictions,\n intersectEffectiveAllowlistWithSnapshot,\n intersectAllowlists,\n isModelAllowedForProviderInEffective,\n isProviderAllowedInEffective,\n readAgentRuntimeOverrideAllowlist,\n type TenantAllowlistSnapshot,\n} from '../../../../../lib/model-allowlist'\nimport { AiTenantModelAllowlistRepository } from '../../../../../data/repositories/AiTenantModelAllowlistRepository'\nimport { AiAgentRuntimeOverrideRepository } from '../../../../../data/repositories/AiAgentRuntimeOverrideRepository'\n\nfunction modelsForPicker(\n provider: ReturnType<typeof llmProviderRegistry.list>[number],\n allowedModelIds: string[] | undefined,\n): ReadonlyArray<{ id: string; name: string; contextWindow?: number | null; tags?: readonly string[] }> {\n if (provider.defaultModels.length > 0) return provider.defaultModels\n return (allowedModelIds ?? []).map((id) => ({ id, name: id }))\n}\n\nconst agentIdPattern = /^[a-z0-9_]+\\.[a-z0-9_]+$/\n\nconst agentIdParamSchema = z.object({\n agentId: z\n .string()\n .regex(agentIdPattern, 'agentId must match \"<module>.<agent>\" (lowercase, digits, underscores only)'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Available models for an AI agent',\n methods: {\n GET: {\n operationId: 'aiAssistantGetAgentModels',\n summary: 'Get the providers and curated models available for the chat-UI picker for this agent',\n description:\n 'Returns all configured providers with their curated model catalogs, filtered to providers ' +\n 'that have an API key configured in the current environment. When the agent declares ' +\n '`allowRuntimeModelOverride: false`, the response reflects that constraint so the ' +\n 'UI picker can hide itself. Includes the agent\\'s resolved default provider/model so ' +\n 'the picker can render a \"(default)\" badge next to the right entry. ' +\n 'RBAC: requires the same features as the agent itself (typically `ai_assistant.view`).',\n responses: [\n {\n status: 200,\n description:\n 'Providers and curated models available for the agent picker. ' +\n 'Empty `providers` array when `allowRuntimeModelOverride` is false.',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated.' },\n { status: 403, description: 'Caller lacks the agent\\'s required features.' },\n { status: 404, description: 'Unknown agent id.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nexport async function GET(\n req: NextRequest,\n { params }: { params: Promise<{ agentId: string }> },\n): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const rawParams = await params\n const paramResult = agentIdParamSchema.safeParse(rawParams)\n if (!paramResult.success) {\n return NextResponse.json(\n { error: 'Invalid agentId path parameter.', code: 'validation_error', issues: paramResult.error.issues },\n { status: 400 },\n )\n }\n const agentId = paramResult.data.agentId\n\n try {\n await loadAgentRegistry()\n\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n const agent = getAgent(agentId)\n if (!agent) {\n return NextResponse.json({ error: `Agent \"${agentId}\" not found.`, code: 'agent_unknown' }, { status: 404 })\n }\n\n const agentFeatures = agent.requiredFeatures ?? []\n if (agentFeatures.length > 0) {\n const permitted = hasRequiredFeatures(agentFeatures, acl.features, acl.isSuperAdmin)\n if (!permitted) {\n return NextResponse.json(\n {\n error: `Access to agent \"${agentId}\" requires features: ${agentFeatures.join(', ')}.`,\n code: 'agent_features_denied',\n },\n { status: 403 },\n )\n }\n }\n\n const allowRuntimeModelOverride = agent.allowRuntimeModelOverride !== false\n\n // Load the per-tenant allowlist snapshot so the picker reflects both env\n // and admin-edited tenant constraints (Phase 1780-6).\n let tenantAllowlistSnapshot: TenantAllowlistSnapshot | null = null\n let agentRuntimeOverrideAllowlist: TenantAllowlistSnapshot | null = null\n let tenantRuntimeOverride: {\n providerId: string | null\n modelId: string | null\n baseURL: string | null\n } | null = null\n if (auth.tenantId) {\n try {\n const em = container.resolve<EntityManager>('em')\n const allowlistRepo = new AiTenantModelAllowlistRepository(em)\n tenantAllowlistSnapshot = await allowlistRepo.getSnapshot({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n const runtimeOverrideRepo = new AiAgentRuntimeOverrideRepository(em)\n const runtimeOverrideDefaultRow = await runtimeOverrideRepo.getDefault({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n agentId,\n })\n tenantRuntimeOverride = runtimeOverrideDefaultRow\n ? {\n providerId: runtimeOverrideDefaultRow.providerId ?? null,\n modelId: runtimeOverrideDefaultRow.modelId ?? null,\n baseURL: runtimeOverrideDefaultRow.baseUrl ?? null,\n }\n : null\n const runtimeOverrideRow = await runtimeOverrideRepo.getExact({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n agentId,\n })\n const tenantAgentAllowlist = runtimeOverrideRow\n ? {\n allowedProviders: runtimeOverrideRow.allowedOverrideProviders ?? null,\n allowedModelsByProvider: runtimeOverrideRow.allowedOverrideModelsByProvider ?? {},\n }\n : null\n agentRuntimeOverrideAllowlist = hasAllowlistSnapshotRestrictions(tenantAgentAllowlist)\n ? tenantAgentAllowlist\n : null\n } catch (snapshotError) {\n // Picker still renders against env-only so the UI does not break, but log at\n // error level so an outage is operationally visible. The chat dispatcher\n // refuses to dispatch when this lookup fails, so writes stay safe.\n console.error('[AI Agents Models] Failed to load tenant allowlist:', snapshotError)\n }\n }\n\n // Resolve the agent's current default provider/model for the \"(default)\" badge\n const factory = createModelFactory(container)\n const defaultResolution = factory.resolveModel({\n moduleId: agent.moduleId,\n agentDefaultModel: agent.defaultModel,\n agentDefaultProvider: agent.defaultProvider,\n agentDefaultBaseUrl: agent.defaultBaseUrl,\n allowRuntimeModelOverride,\n tenantOverride: tenantRuntimeOverride ?? undefined,\n tenantAllowlist: tenantAllowlistSnapshot,\n })\n const defaultProviderId = defaultResolution.providerId\n const defaultModelId = defaultResolution.modelId\n\n // Build provider list \u2014 only configured providers, with curated model\n // catalogs, clipped to the EFFECTIVE allowlist (env \u2229 tenant) so the\n // chat-UI picker can never offer a value the runtime would refuse.\n const env = process.env as Record<string, string | undefined>\n const knownProviderIds = llmProviderRegistry.list().map((p) => p.id)\n const baseEffectiveAllowlist = intersectAllowlists(\n env,\n knownProviderIds,\n tenantAllowlistSnapshot,\n )\n const envAgentAllowlist = readAgentRuntimeOverrideAllowlist(env, agentId, knownProviderIds)\n const effectiveAllowlist = intersectEffectiveAllowlistWithSnapshot(\n intersectEffectiveAllowlistWithSnapshot(\n baseEffectiveAllowlist,\n knownProviderIds,\n envAgentAllowlist,\n ),\n knownProviderIds,\n agentRuntimeOverrideAllowlist,\n )\n const providers = allowRuntimeModelOverride\n ? llmProviderRegistry.list()\n .filter((provider) => provider.isConfigured())\n .filter((provider) => isProviderAllowedInEffective(effectiveAllowlist, provider.id))\n .map((provider) => {\n const allowedModelIds = effectiveAllowlist.modelsByProvider[provider.id]\n const filteredModels = modelsForPicker(provider, allowedModelIds).filter((model) =>\n isModelAllowedForProviderInEffective(effectiveAllowlist, provider.id, model.id),\n )\n return {\n id: provider.id,\n name: provider.name,\n isDefault: provider.id === defaultProviderId,\n models: filteredModels.map((model) => ({\n id: model.id,\n name: model.name,\n contextWindow: model.contextWindow,\n tags: model.tags,\n isDefault: provider.id === defaultProviderId && model.id === defaultModelId,\n })),\n }\n })\n : []\n\n return NextResponse.json({\n agentId,\n allowRuntimeModelOverride,\n defaultProviderId,\n defaultModelId,\n defaultProviderName: llmProviderRegistry.get(defaultProviderId)?.name ?? defaultProviderId,\n defaultModelName:\n llmProviderRegistry\n .get(defaultProviderId)\n ?.defaultModels.find((model) => model.id === defaultModelId)?.name ?? defaultModelId,\n providers,\n })\n } catch (error) {\n console.error('[AI Agents Models] GET error:', error)\n return NextResponse.json({ error: 'Failed to resolve agent models.' }, { status: 500 })\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -65,6 +65,10 @@ async function GET(req) {
|
|
|
65
65
|
description: agent.description,
|
|
66
66
|
systemPrompt: agent.systemPrompt,
|
|
67
67
|
executionMode: agent.executionMode ?? "chat",
|
|
68
|
+
defaultProvider: agent.defaultProvider ?? null,
|
|
69
|
+
defaultModel: agent.defaultModel ?? null,
|
|
70
|
+
defaultBaseUrl: agent.defaultBaseUrl ?? null,
|
|
71
|
+
allowRuntimeModelOverride: agent.allowRuntimeModelOverride !== false,
|
|
68
72
|
mutationPolicy: agent.mutationPolicy ?? "read-only",
|
|
69
73
|
readOnly: Boolean(agent.readOnly),
|
|
70
74
|
maxSteps: agent.maxSteps ?? null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/ai_assistant/api/ai/agents/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { listAgents, loadAgentRegistry } from '../../../lib/agent-registry'\nimport { hasRequiredFeatures } from '../../../lib/auth'\nimport { toolRegistry } from '../../../lib/tool-registry'\nimport type { AiToolDefinition } from '../../../lib/types'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'List AI agents the caller can invoke',\n methods: {\n GET: {\n operationId: 'aiAssistantListAgents',\n summary: 'List registered AI agents, filtered by the caller\\'s features.',\n description:\n 'Returns `{ agents: [...] }` \u2014 the subset of agents from `ai-agents.generated.ts` that the ' +\n 'authenticated caller can invoke based on each agent\\'s `requiredFeatures`. Mirrors the ' +\n '`meta.list_agents` tool handler so backoffice pages (e.g. the playground) can render an ' +\n 'agent picker without going through the MCP tool transport.',\n responses: [\n {\n status: 200,\n description: 'Accessible agent summaries.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 500, description: 'Internal failure while loading the agent registry.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized', code: 'unauthenticated' }, { status: 401 })\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n // No LLM provider configured (no API keys set). The launcher uses the\n // `aiConfigured` flag to show a setup prompt; explicit `<AiChat>` mounts\n // and playground pages still see the full registry so they can show their\n // own configuration prompts instead of silently disappearing.\n const aiConfigured = llmProviderRegistry.resolveFirstConfigured() != null\n\n await loadAgentRegistry()\n const all = listAgents()\n const accessible = all.filter((agent) =>\n hasRequiredFeatures(agent.requiredFeatures, acl.features, acl.isSuperAdmin, rbacService),\n )\n\n const agents = accessible.map((agent) => {\n const tools = agent.allowedTools.map((toolName) => {\n const tool = toolRegistry.getTool(toolName) as AiToolDefinition | undefined\n return {\n name: toolName,\n displayName: tool?.displayName ?? toolName,\n isMutation: Boolean(tool?.isMutation),\n registered: Boolean(tool),\n }\n })\n return {\n id: agent.id,\n moduleId: agent.moduleId,\n label: agent.label,\n description: agent.description,\n systemPrompt: agent.systemPrompt,\n executionMode: agent.executionMode ?? 'chat',\n mutationPolicy: agent.mutationPolicy ?? 'read-only',\n readOnly: Boolean(agent.readOnly),\n maxSteps: agent.maxSteps ?? null,\n allowedTools: agent.allowedTools,\n tools,\n requiredFeatures: agent.requiredFeatures ?? [],\n acceptedMediaTypes: agent.acceptedMediaTypes ?? [],\n keywords: agent.keywords ?? [],\n suggestions: agent.suggestions ?? [],\n hasOutputSchema: Boolean(agent.output),\n }\n })\n\n return NextResponse.json({ agents, total: agents.length, aiConfigured })\n } catch (error) {\n console.error('[AI Agents] Failed to list agents:', error)\n return NextResponse.json(\n { error: 'Failed to list agents', code: 'internal_error' },\n { status: 500 },\n )\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAsC;AAE/C,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,2BAA2B;AACpC,SAAS,YAAY,yBAAyB;AAC9C,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAGtB,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,aAAa;AAAA,MACb,SAAS;AAAA,MACT,aACE;AAAA,MAIF,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,WAAW;AAAA,QACb;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,0BAA0B;AAAA,QACtD,EAAE,QAAQ,KAAK,aAAa,qDAAqD;AAAA,MACnF;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACnE;AAEA,eAAsB,IAAI,KAAkB;AAC1C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,OAAO,gBAAgB,MAAM,kBAAkB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9F;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,cAAc,UAAU,QAAqB,aAAa;AAChE,UAAM,MAAM,MAAM,YAAY,QAAQ,KAAK,KAAK;AAAA,MAC9C,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,IACvB,CAAC;AAMD,UAAM,eAAe,oBAAoB,uBAAuB,KAAK;AAErE,UAAM,kBAAkB;AACxB,UAAM,MAAM,WAAW;AACvB,UAAM,aAAa,IAAI;AAAA,MAAO,CAAC,UAC7B,oBAAoB,MAAM,kBAAkB,IAAI,UAAU,IAAI,cAAc,WAAW;AAAA,IACzF;AAEA,UAAM,SAAS,WAAW,IAAI,CAAC,UAAU;AACvC,YAAM,QAAQ,MAAM,aAAa,IAAI,CAAC,aAAa;AACjD,cAAM,OAAO,aAAa,QAAQ,QAAQ;AAC1C,eAAO;AAAA,UACL,MAAM;AAAA,UACN,aAAa,MAAM,eAAe;AAAA,UAClC,YAAY,QAAQ,MAAM,UAAU;AAAA,UACpC,YAAY,QAAQ,IAAI;AAAA,QAC1B;AAAA,MACF,CAAC;AACD,aAAO;AAAA,QACL,IAAI,MAAM;AAAA,QACV,UAAU,MAAM;AAAA,QAChB,OAAO,MAAM;AAAA,QACb,aAAa,MAAM;AAAA,QACnB,cAAc,MAAM;AAAA,QACpB,eAAe,MAAM,iBAAiB;AAAA,QACtC,gBAAgB,MAAM,kBAAkB;AAAA,QACxC,UAAU,QAAQ,MAAM,QAAQ;AAAA,QAChC,UAAU,MAAM,YAAY;AAAA,QAC5B,cAAc,MAAM;AAAA,QACpB;AAAA,QACA,kBAAkB,MAAM,oBAAoB,CAAC;AAAA,QAC7C,oBAAoB,MAAM,sBAAsB,CAAC;AAAA,QACjD,UAAU,MAAM,YAAY,CAAC;AAAA,QAC7B,aAAa,MAAM,eAAe,CAAC;AAAA,QACnC,iBAAiB,QAAQ,MAAM,MAAM;AAAA,MACvC;AAAA,IACF,CAAC;AAED,WAAO,aAAa,KAAK,EAAE,QAAQ,OAAO,OAAO,QAAQ,aAAa,CAAC;AAAA,EACzE,SAAS,OAAO;AACd,YAAQ,MAAM,sCAAsC,KAAK;AACzD,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,yBAAyB,MAAM,iBAAiB;AAAA,MACzD,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;",
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { listAgents, loadAgentRegistry } from '../../../lib/agent-registry'\nimport { hasRequiredFeatures } from '../../../lib/auth'\nimport { toolRegistry } from '../../../lib/tool-registry'\nimport type { AiToolDefinition } from '../../../lib/types'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'List AI agents the caller can invoke',\n methods: {\n GET: {\n operationId: 'aiAssistantListAgents',\n summary: 'List registered AI agents, filtered by the caller\\'s features.',\n description:\n 'Returns `{ agents: [...] }` \u2014 the subset of agents from `ai-agents.generated.ts` that the ' +\n 'authenticated caller can invoke based on each agent\\'s `requiredFeatures`. Mirrors the ' +\n '`meta.list_agents` tool handler so backoffice pages (e.g. the playground) can render an ' +\n 'agent picker without going through the MCP tool transport.',\n responses: [\n {\n status: 200,\n description: 'Accessible agent summaries.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 500, description: 'Internal failure while loading the agent registry.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized', code: 'unauthenticated' }, { status: 401 })\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n // No LLM provider configured (no API keys set). The launcher uses the\n // `aiConfigured` flag to show a setup prompt; explicit `<AiChat>` mounts\n // and playground pages still see the full registry so they can show their\n // own configuration prompts instead of silently disappearing.\n const aiConfigured = llmProviderRegistry.resolveFirstConfigured() != null\n\n await loadAgentRegistry()\n const all = listAgents()\n const accessible = all.filter((agent) =>\n hasRequiredFeatures(agent.requiredFeatures, acl.features, acl.isSuperAdmin, rbacService),\n )\n\n const agents = accessible.map((agent) => {\n const tools = agent.allowedTools.map((toolName) => {\n const tool = toolRegistry.getTool(toolName) as AiToolDefinition | undefined\n return {\n name: toolName,\n displayName: tool?.displayName ?? toolName,\n isMutation: Boolean(tool?.isMutation),\n registered: Boolean(tool),\n }\n })\n return {\n id: agent.id,\n moduleId: agent.moduleId,\n label: agent.label,\n description: agent.description,\n systemPrompt: agent.systemPrompt,\n executionMode: agent.executionMode ?? 'chat',\n defaultProvider: agent.defaultProvider ?? null,\n defaultModel: agent.defaultModel ?? null,\n defaultBaseUrl: agent.defaultBaseUrl ?? null,\n allowRuntimeModelOverride: agent.allowRuntimeModelOverride !== false,\n mutationPolicy: agent.mutationPolicy ?? 'read-only',\n readOnly: Boolean(agent.readOnly),\n maxSteps: agent.maxSteps ?? null,\n allowedTools: agent.allowedTools,\n tools,\n requiredFeatures: agent.requiredFeatures ?? [],\n acceptedMediaTypes: agent.acceptedMediaTypes ?? [],\n keywords: agent.keywords ?? [],\n suggestions: agent.suggestions ?? [],\n hasOutputSchema: Boolean(agent.output),\n }\n })\n\n return NextResponse.json({ agents, total: agents.length, aiConfigured })\n } catch (error) {\n console.error('[AI Agents] Failed to list agents:', error)\n return NextResponse.json(\n { error: 'Failed to list agents', code: 'internal_error' },\n { status: 500 },\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,10 +2,24 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
|
+
import { llmProviderRegistry } from "@open-mercato/shared/lib/ai/llm-provider-registry";
|
|
5
6
|
import { loadAgentRegistry } from "../../../lib/agent-registry.js";
|
|
6
7
|
import { checkAgentPolicy } from "../../../lib/agent-policy.js";
|
|
7
8
|
import { runAiAgentText } from "../../../lib/agent-runtime.js";
|
|
8
9
|
import { AgentPolicyError } from "../../../lib/agent-tools.js";
|
|
10
|
+
import { readBaseurlAllowlist, isBaseurlAllowlisted } from "../../../lib/baseurl-allowlist.js";
|
|
11
|
+
import {
|
|
12
|
+
canonicalProviderId,
|
|
13
|
+
hasAllowlistSnapshotRestrictions,
|
|
14
|
+
intersectEffectiveAllowlistWithSnapshot,
|
|
15
|
+
intersectAllowlists,
|
|
16
|
+
isModelAllowedForProviderInEffective,
|
|
17
|
+
isProviderAllowedInEffective,
|
|
18
|
+
modelAllowlistEnvVarName,
|
|
19
|
+
readAgentRuntimeOverrideAllowlist
|
|
20
|
+
} from "../../../lib/model-allowlist.js";
|
|
21
|
+
import { AiTenantModelAllowlistRepository } from "../../../data/repositories/AiTenantModelAllowlistRepository.js";
|
|
22
|
+
import { AiAgentRuntimeOverrideRepository } from "../../../data/repositories/AiAgentRuntimeOverrideRepository.js";
|
|
9
23
|
const MAX_MESSAGES = 100;
|
|
10
24
|
const agentIdPattern = /^[a-z0-9_]+\.[a-z0-9_]+$/;
|
|
11
25
|
const chatMessageSchema = z.object({
|
|
@@ -31,7 +45,30 @@ const chatRequestSchema = z.object({
|
|
|
31
45
|
conversationId: z.string().min(1).max(128).optional()
|
|
32
46
|
});
|
|
33
47
|
const agentQuerySchema = z.object({
|
|
34
|
-
agent: z.string().regex(agentIdPattern, 'agent must match "<module>.<agent>" (lowercase, digits, underscores only)')
|
|
48
|
+
agent: z.string().regex(agentIdPattern, 'agent must match "<module>.<agent>" (lowercase, digits, underscores only)'),
|
|
49
|
+
/**
|
|
50
|
+
* Per-request provider override. Must match a registered + configured
|
|
51
|
+
* provider id. Validated against `llmProviderRegistry` at dispatch time.
|
|
52
|
+
* Rejected when the agent has `allowRuntimeModelOverride: false`.
|
|
53
|
+
*
|
|
54
|
+
* Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
55
|
+
*/
|
|
56
|
+
provider: z.string().optional(),
|
|
57
|
+
/**
|
|
58
|
+
* Per-request model id override. Free-form string. Logged (not rejected)
|
|
59
|
+
* when not in the provider's curated `defaultModels` catalog.
|
|
60
|
+
*
|
|
61
|
+
* Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
62
|
+
*/
|
|
63
|
+
model: z.string().optional(),
|
|
64
|
+
/**
|
|
65
|
+
* Per-request base URL override. Must parse as a URL and match
|
|
66
|
+
* `AI_RUNTIME_BASEURL_ALLOWLIST` (comma-separated host patterns). When the
|
|
67
|
+
* env var is unset or empty, any non-empty value is rejected.
|
|
68
|
+
*
|
|
69
|
+
* Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
70
|
+
*/
|
|
71
|
+
baseUrl: z.string().optional()
|
|
35
72
|
});
|
|
36
73
|
const openApi = {
|
|
37
74
|
tag: "AI Assistant",
|
|
@@ -40,7 +77,7 @@ const openApi = {
|
|
|
40
77
|
POST: {
|
|
41
78
|
operationId: "aiAssistantChatAgent",
|
|
42
79
|
summary: "Stream a chat turn for a registered AI agent",
|
|
43
|
-
description: "Dispatches a chat turn to the focused AI agent identified by `?agent=<module>.<agent>`. Enforces agent-level `requiredFeatures`, tool whitelisting, read-only / mutationPolicy, execution-mode compatibility, and attachment media-type policy. The streaming response body uses an AI SDK-compatible `text/event-stream` transport.",
|
|
80
|
+
description: "Dispatches a chat turn to the focused AI agent identified by `?agent=<module>.<agent>`. Enforces agent-level `requiredFeatures`, tool whitelisting, read-only / mutationPolicy, execution-mode compatibility, and attachment media-type policy. The streaming response body uses an AI SDK-compatible `text/event-stream` transport. Optional `?provider=`, `?model=`, and `?baseUrl=` query params let callers override the resolved provider/model/base-URL for this turn (Phase 4a). Provider must be registered and configured; baseUrl must match `AI_RUNTIME_BASEURL_ALLOWLIST` when set. Both are suppressed when the agent declares `allowRuntimeModelOverride: false`.",
|
|
44
81
|
query: agentQuerySchema,
|
|
45
82
|
requestBody: {
|
|
46
83
|
contentType: "application/json",
|
|
@@ -51,7 +88,10 @@ const openApi = {
|
|
|
51
88
|
{ status: 200, description: "Streaming text/event-stream response compatible with AI SDK chat transports.", mediaType: "text/event-stream" }
|
|
52
89
|
],
|
|
53
90
|
errors: [
|
|
54
|
-
{
|
|
91
|
+
{
|
|
92
|
+
status: 400,
|
|
93
|
+
description: "Invalid query param, malformed payload, or message count above the cap. Typed codes: `runtime_override_disabled` (agent has allowRuntimeModelOverride:false), `provider_unknown` (provider id not registered), `provider_not_configured` (provider registered but no API key in env), `baseurl_not_allowlisted` (baseUrl not in AI_RUNTIME_BASEURL_ALLOWLIST)."
|
|
94
|
+
},
|
|
55
95
|
{ status: 401, description: "Unauthenticated caller." },
|
|
56
96
|
{ status: 403, description: "Caller lacks agent-level or tool-level required features." },
|
|
57
97
|
{ status: 404, description: "Unknown agent id." },
|
|
@@ -93,7 +133,10 @@ async function POST(req) {
|
|
|
93
133
|
}
|
|
94
134
|
const requestUrl = new URL(req.url);
|
|
95
135
|
const queryResult = agentQuerySchema.safeParse({
|
|
96
|
-
agent: requestUrl.searchParams.get("agent") ?? void 0
|
|
136
|
+
agent: requestUrl.searchParams.get("agent") ?? void 0,
|
|
137
|
+
provider: requestUrl.searchParams.get("provider") ?? void 0,
|
|
138
|
+
model: requestUrl.searchParams.get("model") ?? void 0,
|
|
139
|
+
baseUrl: requestUrl.searchParams.get("baseUrl") ?? void 0
|
|
97
140
|
});
|
|
98
141
|
if (!queryResult.success) {
|
|
99
142
|
return jsonError(400, 'Invalid or missing "agent" query parameter.', "validation_error", {
|
|
@@ -101,6 +144,9 @@ async function POST(req) {
|
|
|
101
144
|
});
|
|
102
145
|
}
|
|
103
146
|
const agentId = queryResult.data.agent;
|
|
147
|
+
const rawProvider = queryResult.data.provider;
|
|
148
|
+
const rawModel = queryResult.data.model;
|
|
149
|
+
const rawBaseUrl = queryResult.data.baseUrl;
|
|
104
150
|
let parsedBody;
|
|
105
151
|
try {
|
|
106
152
|
parsedBody = await req.json();
|
|
@@ -137,6 +183,123 @@ async function POST(req) {
|
|
|
137
183
|
if (!decision.ok) {
|
|
138
184
|
return jsonError(statusForDenyCode(decision.code), decision.message, decision.code);
|
|
139
185
|
}
|
|
186
|
+
const agentDef = decision.agent;
|
|
187
|
+
const hasRuntimeOverride = rawProvider && rawProvider.trim().length > 0 || rawModel && rawModel.trim().length > 0 || rawBaseUrl && rawBaseUrl.trim().length > 0;
|
|
188
|
+
if (hasRuntimeOverride) {
|
|
189
|
+
if (agentDef.allowRuntimeModelOverride === false) {
|
|
190
|
+
return jsonError(
|
|
191
|
+
400,
|
|
192
|
+
`Agent "${agentId}" has runtime model override disabled (allowRuntimeModelOverride: false).`,
|
|
193
|
+
"runtime_override_disabled"
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
let tenantAllowlistSnapshot = null;
|
|
198
|
+
let agentRuntimeOverrideAllowlist = null;
|
|
199
|
+
if (auth.tenantId) {
|
|
200
|
+
try {
|
|
201
|
+
const em = container.resolve("em");
|
|
202
|
+
const allowlistRepo = new AiTenantModelAllowlistRepository(em);
|
|
203
|
+
tenantAllowlistSnapshot = await allowlistRepo.getSnapshot({
|
|
204
|
+
tenantId: auth.tenantId,
|
|
205
|
+
organizationId: auth.orgId ?? null
|
|
206
|
+
});
|
|
207
|
+
const runtimeOverrideRepo = new AiAgentRuntimeOverrideRepository(em);
|
|
208
|
+
const agentRuntimeOverrideRow = await runtimeOverrideRepo.getExact({
|
|
209
|
+
tenantId: auth.tenantId,
|
|
210
|
+
organizationId: auth.orgId ?? null,
|
|
211
|
+
agentId
|
|
212
|
+
});
|
|
213
|
+
const tenantAgentAllowlist = agentRuntimeOverrideRow ? {
|
|
214
|
+
allowedProviders: agentRuntimeOverrideRow.allowedOverrideProviders ?? null,
|
|
215
|
+
allowedModelsByProvider: agentRuntimeOverrideRow.allowedOverrideModelsByProvider ?? {}
|
|
216
|
+
} : null;
|
|
217
|
+
agentRuntimeOverrideAllowlist = hasAllowlistSnapshotRestrictions(tenantAgentAllowlist) ? tenantAgentAllowlist : null;
|
|
218
|
+
} catch (snapshotError) {
|
|
219
|
+
console.error(
|
|
220
|
+
"[AI Chat Agent] Tenant allowlist lookup failed; refusing to dispatch:",
|
|
221
|
+
snapshotError
|
|
222
|
+
);
|
|
223
|
+
return jsonError(
|
|
224
|
+
503,
|
|
225
|
+
"Tenant allowlist is temporarily unavailable. Try again shortly.",
|
|
226
|
+
"tenant_allowlist_unavailable"
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const knownProviderIds = llmProviderRegistry.list().map((p) => p.id);
|
|
231
|
+
const baseEffectiveAllowlist = intersectAllowlists(
|
|
232
|
+
process.env,
|
|
233
|
+
knownProviderIds,
|
|
234
|
+
tenantAllowlistSnapshot
|
|
235
|
+
);
|
|
236
|
+
const envAgentAllowlist = readAgentRuntimeOverrideAllowlist(
|
|
237
|
+
process.env,
|
|
238
|
+
agentId,
|
|
239
|
+
knownProviderIds
|
|
240
|
+
);
|
|
241
|
+
const effectiveAllowlist = intersectEffectiveAllowlistWithSnapshot(
|
|
242
|
+
intersectEffectiveAllowlistWithSnapshot(
|
|
243
|
+
baseEffectiveAllowlist,
|
|
244
|
+
knownProviderIds,
|
|
245
|
+
envAgentAllowlist
|
|
246
|
+
),
|
|
247
|
+
knownProviderIds,
|
|
248
|
+
agentRuntimeOverrideAllowlist
|
|
249
|
+
);
|
|
250
|
+
const normalizedProvider = rawProvider && rawProvider.trim().length > 0 ? canonicalProviderId(rawProvider.trim(), llmProviderRegistry.list().map((p) => p.id)) : null;
|
|
251
|
+
if (rawProvider && rawProvider.trim().length > 0) {
|
|
252
|
+
const providerEntry = normalizedProvider ? llmProviderRegistry.get(normalizedProvider) : null;
|
|
253
|
+
if (!providerEntry) {
|
|
254
|
+
return jsonError(
|
|
255
|
+
400,
|
|
256
|
+
`Provider "${rawProvider}" is not registered. Registered provider ids: ${llmProviderRegistry.list().map((p) => p.id).join(", ")}.`,
|
|
257
|
+
"provider_unknown"
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (!providerEntry.isConfigured()) {
|
|
261
|
+
return jsonError(
|
|
262
|
+
400,
|
|
263
|
+
`Provider "${rawProvider}" is registered but not configured in this environment (missing API key).`,
|
|
264
|
+
"provider_not_configured"
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
if (!isProviderAllowedInEffective(effectiveAllowlist, normalizedProvider)) {
|
|
268
|
+
const source = effectiveAllowlist.tenantOverridesActive ? "the effective allowlist (env \u2229 tenant)" : "OM_AI_AVAILABLE_PROVIDERS";
|
|
269
|
+
return jsonError(
|
|
270
|
+
400,
|
|
271
|
+
`Provider "${rawProvider}" is not in ${source}.`,
|
|
272
|
+
"provider_not_allowlisted"
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (rawModel && rawModel.trim().length > 0 && !isModelAllowedForProviderInEffective(
|
|
276
|
+
effectiveAllowlist,
|
|
277
|
+
normalizedProvider,
|
|
278
|
+
rawModel.trim()
|
|
279
|
+
)) {
|
|
280
|
+
const source = effectiveAllowlist.tenantOverridesActive ? `the effective allowlist (env \u2229 tenant) for "${normalizedProvider}"` : modelAllowlistEnvVarName(normalizedProvider);
|
|
281
|
+
return jsonError(
|
|
282
|
+
400,
|
|
283
|
+
`Model "${rawModel}" is not in ${source}.`,
|
|
284
|
+
"model_not_allowlisted"
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (rawBaseUrl && rawBaseUrl.trim().length > 0) {
|
|
289
|
+
const allowlist = readBaseurlAllowlist();
|
|
290
|
+
if (!isBaseurlAllowlisted(rawBaseUrl.trim(), allowlist)) {
|
|
291
|
+
return jsonError(
|
|
292
|
+
400,
|
|
293
|
+
`baseUrl "${rawBaseUrl}" is not in the AI_RUNTIME_BASEURL_ALLOWLIST. Set that env var to a comma-separated list of allowed host patterns to enable per-request baseUrl overrides.`,
|
|
294
|
+
"baseurl_not_allowlisted"
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const requestOverride = hasRuntimeOverride ? {
|
|
299
|
+
providerId: normalizedProvider,
|
|
300
|
+
modelId: rawModel && rawModel.trim().length > 0 ? rawModel.trim() : null,
|
|
301
|
+
baseURL: rawBaseUrl && rawBaseUrl.trim().length > 0 ? rawBaseUrl.trim() : null
|
|
302
|
+
} : void 0;
|
|
140
303
|
return await runAiAgentText({
|
|
141
304
|
agentId,
|
|
142
305
|
messages: bodyResult.data.messages,
|
|
@@ -151,7 +314,8 @@ async function POST(req) {
|
|
|
151
314
|
features: acl.features,
|
|
152
315
|
isSuperAdmin: acl.isSuperAdmin
|
|
153
316
|
},
|
|
154
|
-
container
|
|
317
|
+
container,
|
|
318
|
+
requestOverride
|
|
155
319
|
});
|
|
156
320
|
} catch (error) {
|
|
157
321
|
if (error instanceof AgentPolicyError) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/ai_assistant/api/ai/chat/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { UIMessage } from 'ai'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { loadAgentRegistry } from '../../../lib/agent-registry'\nimport { checkAgentPolicy, type AgentPolicyDenyCode } from '../../../lib/agent-policy'\nimport { runAiAgentText } from '../../../lib/agent-runtime'\nimport { AgentPolicyError } from '../../../lib/agent-tools'\n\nconst MAX_MESSAGES = 100\n\nconst agentIdPattern = /^[a-z0-9_]+\\.[a-z0-9_]+$/\n\nconst chatMessageSchema = z.object({\n role: z.enum(['user', 'assistant', 'system']),\n content: z.string(),\n})\n\nconst pageContextSchema = z\n .object({\n pageId: z.string().nullable().optional(),\n entityType: z.string().nullable().optional(),\n recordId: z.string().nullable().optional(),\n })\n .passthrough()\n\nconst chatRequestSchema = z.object({\n messages: z\n .array(chatMessageSchema)\n .min(1, 'messages must contain at least one message')\n .max(MAX_MESSAGES, `messages must contain at most ${MAX_MESSAGES} entries`),\n attachmentIds: z.array(z.string()).optional(),\n debug: z.boolean().optional(),\n pageContext: pageContextSchema.optional(),\n /**\n * Optional stable conversation id forwarded from `<AiChat>`. Bridged into\n * the Step 5.6 `prepareMutation` idempotency hash so repeated turns within\n * the same chat collapse onto the same pending action. Additive; omitted\n * bodies continue to work as before.\n */\n conversationId: z.string().min(1).max(128).optional(),\n})\n\nexport type AiChatRequest = z.infer<typeof chatRequestSchema>\n\nconst agentQuerySchema = z.object({\n agent: z\n .string()\n .regex(agentIdPattern, 'agent must match \"<module>.<agent>\" (lowercase, digits, underscores only)'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI agent dispatcher',\n methods: {\n POST: {\n operationId: 'aiAssistantChatAgent',\n summary: 'Stream a chat turn for a registered AI agent',\n description:\n 'Dispatches a chat turn to the focused AI agent identified by `?agent=<module>.<agent>`. ' +\n 'Enforces agent-level `requiredFeatures`, tool whitelisting, read-only / mutationPolicy, ' +\n 'execution-mode compatibility, and attachment media-type policy. The streaming response ' +\n 'body uses an AI SDK-compatible `text/event-stream` transport.',\n query: agentQuerySchema,\n requestBody: {\n contentType: 'application/json',\n description: 'Chat turn payload. `messages` is required; `attachmentIds`, `debug`, and `pageContext` are optional.',\n schema: chatRequestSchema,\n },\n responses: [\n { status: 200, description: 'Streaming text/event-stream response compatible with AI SDK chat transports.', mediaType: 'text/event-stream' },\n ],\n errors: [\n { status: 400, description: 'Invalid query param, malformed payload, or message count above the cap.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks agent-level or tool-level required features.' },\n { status: 404, description: 'Unknown agent id.' },\n { status: 409, description: 'Agent/tool/execution-mode policy violation.' },\n { status: 500, description: 'Internal runtime failure.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nfunction statusForDenyCode(code: AgentPolicyDenyCode): number {\n switch (code) {\n case 'agent_unknown':\n return 404\n case 'agent_features_denied':\n case 'tool_features_denied':\n return 403\n case 'tool_not_whitelisted':\n case 'tool_unknown':\n case 'mutation_blocked_by_readonly':\n case 'mutation_blocked_by_policy':\n case 'execution_mode_not_supported':\n return 409\n case 'attachment_type_not_accepted':\n return 400\n default:\n return 409\n }\n}\n\nexport async function POST(req: NextRequest): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n\n const requestUrl = new URL(req.url)\n const queryResult = agentQuerySchema.safeParse({\n agent: requestUrl.searchParams.get('agent') ?? undefined,\n })\n if (!queryResult.success) {\n return jsonError(400, 'Invalid or missing \"agent\" query parameter.', 'validation_error', {\n issues: queryResult.error.issues,\n })\n }\n const agentId = queryResult.data.agent\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n\n const bodyResult = chatRequestSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return jsonError(400, 'Invalid request body.', 'validation_error', {\n issues: bodyResult.error.issues,\n })\n }\n\n try {\n await loadAgentRegistry()\n\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n const decision = checkAgentPolicy({\n agentId,\n authContext: {\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n },\n requestedExecutionMode: 'chat',\n // TODO(step-3.7): resolve attachmentIds -> media types via attachment-bridge\n // once the attachment-to-model conversion bridge lands. Until then the\n // policy gate skips attachment-type validation because media types are\n // not known at dispatch time.\n attachmentMediaTypes: undefined,\n })\n\n if (!decision.ok) {\n return jsonError(statusForDenyCode(decision.code), decision.message, decision.code)\n }\n\n return await runAiAgentText({\n agentId,\n messages: bodyResult.data.messages as unknown as UIMessage[],\n attachmentIds: bodyResult.data.attachmentIds,\n pageContext: bodyResult.data.pageContext,\n debug: bodyResult.data.debug,\n conversationId: bodyResult.data.conversationId ?? null,\n authContext: {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n features: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n },\n container,\n })\n } catch (error) {\n if (error instanceof AgentPolicyError) {\n return jsonError(statusForDenyCode(error.code), error.message, error.code)\n }\n console.error('[AI Chat Agent] Dispatch failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Agent dispatch failed.',\n 'internal_error',\n )\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAsC;AAE/C,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,yBAAyB;AAClC,SAAS,wBAAkD;AAC3D,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { UIMessage } from 'ai'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { loadAgentRegistry } from '../../../lib/agent-registry'\nimport { checkAgentPolicy, type AgentPolicyDenyCode } from '../../../lib/agent-policy'\nimport { runAiAgentText } from '../../../lib/agent-runtime'\nimport { AgentPolicyError } from '../../../lib/agent-tools'\nimport { readBaseurlAllowlist, isBaseurlAllowlisted } from '../../../lib/baseurl-allowlist'\nimport {\n canonicalProviderId,\n hasAllowlistSnapshotRestrictions,\n intersectEffectiveAllowlistWithSnapshot,\n intersectAllowlists,\n isModelAllowedForProviderInEffective,\n isProviderAllowedInEffective,\n modelAllowlistEnvVarName,\n readAgentRuntimeOverrideAllowlist,\n type TenantAllowlistSnapshot,\n} from '../../../lib/model-allowlist'\nimport { AiTenantModelAllowlistRepository } from '../../../data/repositories/AiTenantModelAllowlistRepository'\nimport { AiAgentRuntimeOverrideRepository } from '../../../data/repositories/AiAgentRuntimeOverrideRepository'\nimport type { EntityManager } from '@mikro-orm/postgresql'\n\nconst MAX_MESSAGES = 100\n\nconst agentIdPattern = /^[a-z0-9_]+\\.[a-z0-9_]+$/\n\nconst chatMessageSchema = z.object({\n role: z.enum(['user', 'assistant', 'system']),\n content: z.string(),\n})\n\nconst pageContextSchema = z\n .object({\n pageId: z.string().nullable().optional(),\n entityType: z.string().nullable().optional(),\n recordId: z.string().nullable().optional(),\n })\n .passthrough()\n\nconst chatRequestSchema = z.object({\n messages: z\n .array(chatMessageSchema)\n .min(1, 'messages must contain at least one message')\n .max(MAX_MESSAGES, `messages must contain at most ${MAX_MESSAGES} entries`),\n attachmentIds: z.array(z.string()).optional(),\n debug: z.boolean().optional(),\n pageContext: pageContextSchema.optional(),\n /**\n * Optional stable conversation id forwarded from `<AiChat>`. Bridged into\n * the Step 5.6 `prepareMutation` idempotency hash so repeated turns within\n * the same chat collapse onto the same pending action. Additive; omitted\n * bodies continue to work as before.\n */\n conversationId: z.string().min(1).max(128).optional(),\n})\n\nexport type AiChatRequest = z.infer<typeof chatRequestSchema>\n\nconst agentQuerySchema = z.object({\n agent: z\n .string()\n .regex(agentIdPattern, 'agent must match \"<module>.<agent>\" (lowercase, digits, underscores only)'),\n /**\n * Per-request provider override. Must match a registered + configured\n * provider id. Validated against `llmProviderRegistry` at dispatch time.\n * Rejected when the agent has `allowRuntimeModelOverride: false`.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n provider: z.string().optional(),\n /**\n * Per-request model id override. Free-form string. Logged (not rejected)\n * when not in the provider's curated `defaultModels` catalog.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n model: z.string().optional(),\n /**\n * Per-request base URL override. Must parse as a URL and match\n * `AI_RUNTIME_BASEURL_ALLOWLIST` (comma-separated host patterns). When the\n * env var is unset or empty, any non-empty value is rejected.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n baseUrl: z.string().optional(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI agent dispatcher',\n methods: {\n POST: {\n operationId: 'aiAssistantChatAgent',\n summary: 'Stream a chat turn for a registered AI agent',\n description:\n 'Dispatches a chat turn to the focused AI agent identified by `?agent=<module>.<agent>`. ' +\n 'Enforces agent-level `requiredFeatures`, tool whitelisting, read-only / mutationPolicy, ' +\n 'execution-mode compatibility, and attachment media-type policy. The streaming response ' +\n 'body uses an AI SDK-compatible `text/event-stream` transport. ' +\n 'Optional `?provider=`, `?model=`, and `?baseUrl=` query params let callers ' +\n 'override the resolved provider/model/base-URL for this turn (Phase 4a). ' +\n 'Provider must be registered and configured; baseUrl must match ' +\n '`AI_RUNTIME_BASEURL_ALLOWLIST` when set. Both are suppressed when the ' +\n 'agent declares `allowRuntimeModelOverride: false`.',\n query: agentQuerySchema,\n requestBody: {\n contentType: 'application/json',\n description: 'Chat turn payload. `messages` is required; `attachmentIds`, `debug`, and `pageContext` are optional.',\n schema: chatRequestSchema,\n },\n responses: [\n { status: 200, description: 'Streaming text/event-stream response compatible with AI SDK chat transports.', mediaType: 'text/event-stream' },\n ],\n errors: [\n {\n status: 400,\n description:\n 'Invalid query param, malformed payload, or message count above the cap. ' +\n 'Typed codes: `runtime_override_disabled` (agent has allowRuntimeModelOverride:false), ' +\n '`provider_unknown` (provider id not registered), ' +\n '`provider_not_configured` (provider registered but no API key in env), ' +\n '`baseurl_not_allowlisted` (baseUrl not in AI_RUNTIME_BASEURL_ALLOWLIST).',\n },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks agent-level or tool-level required features.' },\n { status: 404, description: 'Unknown agent id.' },\n { status: 409, description: 'Agent/tool/execution-mode policy violation.' },\n { status: 500, description: 'Internal runtime failure.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nfunction statusForDenyCode(code: AgentPolicyDenyCode): number {\n switch (code) {\n case 'agent_unknown':\n return 404\n case 'agent_features_denied':\n case 'tool_features_denied':\n return 403\n case 'tool_not_whitelisted':\n case 'tool_unknown':\n case 'mutation_blocked_by_readonly':\n case 'mutation_blocked_by_policy':\n case 'execution_mode_not_supported':\n return 409\n case 'attachment_type_not_accepted':\n return 400\n default:\n return 409\n }\n}\n\nexport async function POST(req: NextRequest): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n\n const requestUrl = new URL(req.url)\n const queryResult = agentQuerySchema.safeParse({\n agent: requestUrl.searchParams.get('agent') ?? undefined,\n provider: requestUrl.searchParams.get('provider') ?? undefined,\n model: requestUrl.searchParams.get('model') ?? undefined,\n baseUrl: requestUrl.searchParams.get('baseUrl') ?? undefined,\n })\n if (!queryResult.success) {\n return jsonError(400, 'Invalid or missing \"agent\" query parameter.', 'validation_error', {\n issues: queryResult.error.issues,\n })\n }\n const agentId = queryResult.data.agent\n const rawProvider = queryResult.data.provider\n const rawModel = queryResult.data.model\n const rawBaseUrl = queryResult.data.baseUrl\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n\n const bodyResult = chatRequestSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return jsonError(400, 'Invalid request body.', 'validation_error', {\n issues: bodyResult.error.issues,\n })\n }\n\n try {\n await loadAgentRegistry()\n\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n const decision = checkAgentPolicy({\n agentId,\n authContext: {\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n },\n requestedExecutionMode: 'chat',\n // TODO(step-3.7): resolve attachmentIds -> media types via attachment-bridge\n // once the attachment-to-model conversion bridge lands. Until then the\n // policy gate skips attachment-type validation because media types are\n // not known at dispatch time.\n attachmentMediaTypes: undefined,\n })\n\n if (!decision.ok) {\n return jsonError(statusForDenyCode(decision.code), decision.message, decision.code)\n }\n\n const agentDef = decision.agent\n\n // --- Phase 4a: validate runtime override query params ---\n const hasRuntimeOverride =\n (rawProvider && rawProvider.trim().length > 0) ||\n (rawModel && rawModel.trim().length > 0) ||\n (rawBaseUrl && rawBaseUrl.trim().length > 0)\n\n if (hasRuntimeOverride) {\n if (agentDef.allowRuntimeModelOverride === false) {\n return jsonError(\n 400,\n `Agent \"${agentId}\" has runtime model override disabled (allowRuntimeModelOverride: false).`,\n 'runtime_override_disabled',\n )\n }\n }\n\n let tenantAllowlistSnapshot: TenantAllowlistSnapshot | null = null\n let agentRuntimeOverrideAllowlist: TenantAllowlistSnapshot | null = null\n if (auth.tenantId) {\n try {\n const em = container.resolve<EntityManager>('em')\n const allowlistRepo = new AiTenantModelAllowlistRepository(em)\n tenantAllowlistSnapshot = await allowlistRepo.getSnapshot({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n const runtimeOverrideRepo = new AiAgentRuntimeOverrideRepository(em)\n const agentRuntimeOverrideRow = await runtimeOverrideRepo.getExact({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n agentId,\n })\n const tenantAgentAllowlist = agentRuntimeOverrideRow\n ? {\n allowedProviders: agentRuntimeOverrideRow.allowedOverrideProviders ?? null,\n allowedModelsByProvider: agentRuntimeOverrideRow.allowedOverrideModelsByProvider ?? {},\n }\n : null\n agentRuntimeOverrideAllowlist = hasAllowlistSnapshotRestrictions(tenantAgentAllowlist)\n ? tenantAgentAllowlist\n : null\n } catch (snapshotError) {\n // Fail closed: refuse to dispatch if we cannot confirm the tenant allowlist.\n // Silently falling back to env-only would widen the effective allowlist when\n // the DB is unavailable, which is the opposite of what an admin intends.\n console.error(\n '[AI Chat Agent] Tenant allowlist lookup failed; refusing to dispatch:',\n snapshotError,\n )\n return jsonError(\n 503,\n 'Tenant allowlist is temporarily unavailable. Try again shortly.',\n 'tenant_allowlist_unavailable',\n )\n }\n }\n const knownProviderIds = llmProviderRegistry.list().map((p) => p.id)\n const baseEffectiveAllowlist = intersectAllowlists(\n process.env as Record<string, string | undefined>,\n knownProviderIds,\n tenantAllowlistSnapshot,\n )\n const envAgentAllowlist = readAgentRuntimeOverrideAllowlist(\n process.env as Record<string, string | undefined>,\n agentId,\n knownProviderIds,\n )\n const effectiveAllowlist = intersectEffectiveAllowlistWithSnapshot(\n intersectEffectiveAllowlistWithSnapshot(\n baseEffectiveAllowlist,\n knownProviderIds,\n envAgentAllowlist,\n ),\n knownProviderIds,\n agentRuntimeOverrideAllowlist,\n )\n\n const normalizedProvider = rawProvider && rawProvider.trim().length > 0\n ? canonicalProviderId(rawProvider.trim(), llmProviderRegistry.list().map((p) => p.id))\n : null\n\n if (rawProvider && rawProvider.trim().length > 0) {\n const providerEntry = normalizedProvider ? llmProviderRegistry.get(normalizedProvider) : null\n if (!providerEntry) {\n return jsonError(\n 400,\n `Provider \"${rawProvider}\" is not registered. Registered provider ids: ${llmProviderRegistry.list().map((p) => p.id).join(', ')}.`,\n 'provider_unknown',\n )\n }\n if (!providerEntry.isConfigured()) {\n return jsonError(\n 400,\n `Provider \"${rawProvider}\" is registered but not configured in this environment (missing API key).`,\n 'provider_not_configured',\n )\n }\n if (!isProviderAllowedInEffective(effectiveAllowlist, normalizedProvider!)) {\n const source = effectiveAllowlist.tenantOverridesActive\n ? 'the effective allowlist (env \u2229 tenant)'\n : 'OM_AI_AVAILABLE_PROVIDERS'\n return jsonError(\n 400,\n `Provider \"${rawProvider}\" is not in ${source}.`,\n 'provider_not_allowlisted',\n )\n }\n if (\n rawModel\n && rawModel.trim().length > 0\n && !isModelAllowedForProviderInEffective(\n effectiveAllowlist,\n normalizedProvider!,\n rawModel.trim(),\n )\n ) {\n const source = effectiveAllowlist.tenantOverridesActive\n ? `the effective allowlist (env \u2229 tenant) for \"${normalizedProvider}\"`\n : modelAllowlistEnvVarName(normalizedProvider!)\n return jsonError(\n 400,\n `Model \"${rawModel}\" is not in ${source}.`,\n 'model_not_allowlisted',\n )\n }\n }\n\n if (rawBaseUrl && rawBaseUrl.trim().length > 0) {\n const allowlist = readBaseurlAllowlist()\n if (!isBaseurlAllowlisted(rawBaseUrl.trim(), allowlist)) {\n return jsonError(\n 400,\n `baseUrl \"${rawBaseUrl}\" is not in the AI_RUNTIME_BASEURL_ALLOWLIST. Set that env var to a comma-separated list of allowed host patterns to enable per-request baseUrl overrides.`,\n 'baseurl_not_allowlisted',\n )\n }\n }\n // --- end Phase 4a validation ---\n\n const requestOverride =\n hasRuntimeOverride\n ? {\n providerId: normalizedProvider,\n modelId: rawModel && rawModel.trim().length > 0 ? rawModel.trim() : null,\n baseURL: rawBaseUrl && rawBaseUrl.trim().length > 0 ? rawBaseUrl.trim() : null,\n }\n : undefined\n\n return await runAiAgentText({\n agentId,\n messages: bodyResult.data.messages as unknown as UIMessage[],\n attachmentIds: bodyResult.data.attachmentIds,\n pageContext: bodyResult.data.pageContext,\n debug: bodyResult.data.debug,\n conversationId: bodyResult.data.conversationId ?? null,\n authContext: {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n features: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n },\n container,\n requestOverride,\n })\n } catch (error) {\n if (error instanceof AgentPolicyError) {\n return jsonError(statusForDenyCode(error.code), error.message, error.code)\n }\n console.error('[AI Chat Agent] Dispatch failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Agent dispatch failed.',\n 'internal_error',\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|