@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
|
@@ -1,22 +1,93 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
2
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";
|
|
3
6
|
import {
|
|
4
7
|
OPEN_CODE_PROVIDER_IDS,
|
|
5
8
|
OPEN_CODE_PROVIDERS,
|
|
6
9
|
getOpenCodeProviderConfiguredEnvKey,
|
|
7
|
-
isOpenCodeProviderConfigured
|
|
8
|
-
resolveAiProviderIdFromEnv,
|
|
9
|
-
resolveOpenCodeModel
|
|
10
|
+
isOpenCodeProviderConfigured
|
|
10
11
|
} from "@open-mercato/shared/lib/ai/opencode-provider";
|
|
12
|
+
import { AiAgentRuntimeOverrideRepository, AiAgentRuntimeOverrideValidationError } from "../../data/repositories/AiAgentRuntimeOverrideRepository.js";
|
|
13
|
+
import { AiTenantModelAllowlistRepository } from "../../data/repositories/AiTenantModelAllowlistRepository.js";
|
|
14
|
+
import { isBaseurlAllowlisted, readBaseurlAllowlist } from "../../lib/baseurl-allowlist.js";
|
|
15
|
+
import { loadAgentRegistry, listAgents } from "../../lib/agent-registry.js";
|
|
16
|
+
import { createModelFactory } from "../../lib/model-factory.js";
|
|
17
|
+
import {
|
|
18
|
+
agentOverrideModelAllowlistEnvVarName,
|
|
19
|
+
agentOverrideProviderAllowlistEnvVarName,
|
|
20
|
+
canonicalProviderId,
|
|
21
|
+
hasAllowlistSnapshotRestrictions,
|
|
22
|
+
intersectEffectiveAllowlistWithSnapshot,
|
|
23
|
+
intersectAllowlists,
|
|
24
|
+
isProviderAllowed,
|
|
25
|
+
isProviderAllowedInEffective,
|
|
26
|
+
isProviderModelAllowed,
|
|
27
|
+
isProviderModelAllowedInEffective,
|
|
28
|
+
modelAllowlistEnvVarName,
|
|
29
|
+
readAgentRuntimeOverrideAllowlist,
|
|
30
|
+
readAllowlistConfig
|
|
31
|
+
} from "../../lib/model-allowlist.js";
|
|
32
|
+
function modelCatalogWithAllowlistFallback(models, allowlistModelIds) {
|
|
33
|
+
if (models.length > 0) return models;
|
|
34
|
+
return (allowlistModelIds ?? []).map((id) => ({ id, name: id }));
|
|
35
|
+
}
|
|
36
|
+
const runtimeOverrideUpsertSchema = z.object({
|
|
37
|
+
providerId: z.string().min(1).max(64).nullable().optional(),
|
|
38
|
+
modelId: z.string().min(1).max(256).nullable().optional(),
|
|
39
|
+
baseURL: z.string().url().max(2048).nullable().optional(),
|
|
40
|
+
agentId: z.string().min(1).max(128).nullable().optional(),
|
|
41
|
+
allowedOverrideProviders: z.array(z.string().min(1).max(64)).nullable().optional(),
|
|
42
|
+
allowedOverrideModelsByProvider: z.record(z.string().min(1).max(64), z.array(z.string().min(1).max(256))).optional()
|
|
43
|
+
});
|
|
44
|
+
const runtimeOverrideClearSchema = z.object({
|
|
45
|
+
agentId: z.string().min(1).max(128).nullable().optional()
|
|
46
|
+
});
|
|
11
47
|
const openApi = {
|
|
12
48
|
tag: "AI Assistant",
|
|
13
49
|
summary: "AI assistant settings",
|
|
14
50
|
methods: {
|
|
15
|
-
GET: { summary: "Get AI provider configuration" }
|
|
51
|
+
GET: { summary: "Get AI provider configuration" },
|
|
52
|
+
PUT: {
|
|
53
|
+
summary: "Upsert per-tenant AI runtime override",
|
|
54
|
+
description: "Creates or updates the per-tenant AI runtime override (provider, model, baseURL). Optionally scoped to a specific agent via `agentId`. Gated by `ai_assistant.settings.manage`. baseURL must match AI_RUNTIME_BASEURL_ALLOWLIST when set.",
|
|
55
|
+
requestBody: {
|
|
56
|
+
contentType: "application/json",
|
|
57
|
+
description: "Override payload. All fields nullable/optional; null explicitly clears the axis.",
|
|
58
|
+
schema: runtimeOverrideUpsertSchema
|
|
59
|
+
},
|
|
60
|
+
responses: [
|
|
61
|
+
{ status: 200, description: "Override saved. Returns the saved row." }
|
|
62
|
+
],
|
|
63
|
+
errors: [
|
|
64
|
+
{ status: 400, description: "Validation error: unknown provider, invalid URL, or baseURL not allowlisted." },
|
|
65
|
+
{ status: 401, description: "Unauthenticated." },
|
|
66
|
+
{ status: 403, description: "Caller lacks ai_assistant.settings.manage." }
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
DELETE: {
|
|
70
|
+
summary: "Clear per-tenant AI runtime override",
|
|
71
|
+
description: "Soft-deletes the active per-tenant runtime override. Pass `agentId` to clear only the agent-specific row; omit to clear the tenant-wide default. Gated by `ai_assistant.settings.manage`. Idempotent \u2014 returns 200 with `cleared: false` when no active row existed.",
|
|
72
|
+
requestBody: {
|
|
73
|
+
contentType: "application/json",
|
|
74
|
+
description: "Optional agentId to scope the delete.",
|
|
75
|
+
schema: runtimeOverrideClearSchema
|
|
76
|
+
},
|
|
77
|
+
responses: [
|
|
78
|
+
{ status: 200, description: "Returns `{ cleared: boolean }` indicating whether a row was found and removed." }
|
|
79
|
+
],
|
|
80
|
+
errors: [
|
|
81
|
+
{ status: 401, description: "Unauthenticated." },
|
|
82
|
+
{ status: 403, description: "Caller lacks ai_assistant.settings.manage." }
|
|
83
|
+
]
|
|
84
|
+
}
|
|
16
85
|
}
|
|
17
86
|
};
|
|
18
87
|
const metadata = {
|
|
19
|
-
GET: { requireAuth: true, requireFeatures: ["ai_assistant.view"] }
|
|
88
|
+
GET: { requireAuth: true, requireFeatures: ["ai_assistant.view"] },
|
|
89
|
+
PUT: { requireAuth: true, requireFeatures: ["ai_assistant.settings.manage"] },
|
|
90
|
+
DELETE: { requireAuth: true, requireFeatures: ["ai_assistant.settings.manage"] }
|
|
20
91
|
};
|
|
21
92
|
async function GET(req) {
|
|
22
93
|
const auth = await getAuthFromRequest(req);
|
|
@@ -24,40 +95,484 @@ async function GET(req) {
|
|
|
24
95
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
25
96
|
}
|
|
26
97
|
try {
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
98
|
+
const env = process.env;
|
|
99
|
+
const configuredProviderHint = env.OM_AI_PROVIDER?.trim() || env.OPENCODE_PROVIDER?.trim() || null;
|
|
100
|
+
const registryProviders = llmProviderRegistry.list();
|
|
101
|
+
const knownProviderIdsForAllowlist = [
|
|
102
|
+
...OPEN_CODE_PROVIDER_IDS,
|
|
103
|
+
...registryProviders.map((p) => p.id).filter((id) => !OPEN_CODE_PROVIDER_IDS.includes(id))
|
|
104
|
+
];
|
|
105
|
+
const registryProviderId = configuredProviderHint ? canonicalProviderId(configuredProviderHint, registryProviders.map((provider) => provider.id)) : null;
|
|
106
|
+
const registryProvider = registryProviderId ? llmProviderRegistry.get(registryProviderId) : null;
|
|
107
|
+
const fallbackOpenCodeProviderId = (configuredProviderHint ? canonicalProviderId(configuredProviderHint, OPEN_CODE_PROVIDER_IDS) : null) ?? "openai";
|
|
108
|
+
const fallbackOpenCodeProvider = OPEN_CODE_PROVIDERS[fallbackOpenCodeProviderId];
|
|
109
|
+
const providerId = registryProvider?.id ?? fallbackOpenCodeProviderId;
|
|
110
|
+
const providerName = registryProvider?.name ?? fallbackOpenCodeProvider?.name ?? providerId;
|
|
111
|
+
const defaultProviderModel = registryProvider?.defaultModel ?? fallbackOpenCodeProvider?.defaultModel ?? "";
|
|
112
|
+
const configuredModelHint = env.OM_AI_MODEL?.trim() || env.OPENCODE_MODEL?.trim() || defaultProviderModel;
|
|
113
|
+
const fallbackModelWithProvider = `${providerId}/${configuredModelHint}`;
|
|
114
|
+
const apiKeyConfigured = registryProvider ? registryProvider.isConfigured(env) : fallbackOpenCodeProvider ? isOpenCodeProviderConfigured(fallbackOpenCodeProviderId) : false;
|
|
115
|
+
const displayEnvKey = registryProvider ? registryProvider.getConfiguredEnvKey(env) : fallbackOpenCodeProvider ? getOpenCodeProviderConfiguredEnvKey(fallbackOpenCodeProviderId) : null;
|
|
32
116
|
const mcpKeyConfigured = !!process.env.MCP_SERVER_API_KEY?.trim();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
117
|
+
let tenantOverride = null;
|
|
118
|
+
let agentResolutions = [];
|
|
119
|
+
let resolvedDefault = null;
|
|
120
|
+
let tenantAllowlistSnapshot = null;
|
|
121
|
+
try {
|
|
122
|
+
const container = await createRequestContainer();
|
|
123
|
+
const tenantId = auth.tenantId ?? null;
|
|
124
|
+
const organizationId = auth.orgId ?? null;
|
|
125
|
+
if (tenantId) {
|
|
126
|
+
const em = container.resolve("em");
|
|
127
|
+
const repo = new AiAgentRuntimeOverrideRepository(em);
|
|
128
|
+
const overrideRow = await repo.getDefault({ tenantId, organizationId, agentId: null });
|
|
129
|
+
if (overrideRow) {
|
|
130
|
+
tenantOverride = {
|
|
131
|
+
providerId: overrideRow.providerId ?? null,
|
|
132
|
+
modelId: overrideRow.modelId ?? null,
|
|
133
|
+
baseURL: overrideRow.baseUrl ?? null,
|
|
134
|
+
agentId: overrideRow.agentId ?? null,
|
|
135
|
+
updatedAt: overrideRow.updatedAt.toISOString()
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const allowlistRepo = new AiTenantModelAllowlistRepository(em);
|
|
139
|
+
tenantAllowlistSnapshot = await allowlistRepo.getSnapshot({
|
|
140
|
+
tenantId,
|
|
141
|
+
organizationId
|
|
142
|
+
});
|
|
143
|
+
const factory = createModelFactory(container);
|
|
144
|
+
const defaultResolution = factory.resolveModel({
|
|
145
|
+
tenantAllowlist: tenantAllowlistSnapshot,
|
|
146
|
+
tenantOverride: tenantOverride ? { providerId: tenantOverride.providerId, modelId: tenantOverride.modelId, baseURL: tenantOverride.baseURL } : void 0
|
|
147
|
+
});
|
|
148
|
+
resolvedDefault = {
|
|
149
|
+
providerId: defaultResolution.providerId,
|
|
150
|
+
modelId: defaultResolution.modelId,
|
|
151
|
+
baseURL: defaultResolution.baseURL ?? null,
|
|
152
|
+
source: defaultResolution.source
|
|
153
|
+
};
|
|
154
|
+
await loadAgentRegistry();
|
|
155
|
+
const agents = listAgents();
|
|
156
|
+
const agentResolutionPromises = agents.map(async (agent) => {
|
|
157
|
+
const agentOverrideRow = await repo.getExact({
|
|
158
|
+
tenantId,
|
|
159
|
+
organizationId,
|
|
160
|
+
agentId: agent.id
|
|
161
|
+
});
|
|
162
|
+
const agentTenantOverride = agentOverrideRow ? {
|
|
163
|
+
providerId: agentOverrideRow.providerId ?? null,
|
|
164
|
+
modelId: agentOverrideRow.modelId ?? null,
|
|
165
|
+
baseURL: agentOverrideRow.baseUrl ?? null
|
|
166
|
+
} : tenantOverride ?? void 0;
|
|
167
|
+
const agentResolution = factory.resolveModel({
|
|
168
|
+
moduleId: agent.moduleId,
|
|
169
|
+
agentDefaultModel: agent.defaultModel,
|
|
170
|
+
agentDefaultProvider: agent.defaultProvider,
|
|
171
|
+
agentDefaultBaseUrl: agent.defaultBaseUrl,
|
|
172
|
+
allowRuntimeModelOverride: agent.allowRuntimeModelOverride,
|
|
173
|
+
tenantOverride: agentTenantOverride,
|
|
174
|
+
tenantAllowlist: tenantAllowlistSnapshot
|
|
175
|
+
});
|
|
176
|
+
const agentEnvAllowlist = readAgentRuntimeOverrideAllowlist(
|
|
177
|
+
env,
|
|
178
|
+
agent.id,
|
|
179
|
+
knownProviderIdsForAllowlist
|
|
180
|
+
);
|
|
181
|
+
const agentTenantAllowlist = agentOverrideRow ? {
|
|
182
|
+
allowedProviders: agentOverrideRow.allowedOverrideProviders ?? null,
|
|
183
|
+
allowedModelsByProvider: agentOverrideRow.allowedOverrideModelsByProvider ?? {}
|
|
184
|
+
} : null;
|
|
185
|
+
const baseEffectiveAllowlist = intersectAllowlists(
|
|
186
|
+
env,
|
|
187
|
+
knownProviderIdsForAllowlist,
|
|
188
|
+
tenantAllowlistSnapshot
|
|
189
|
+
);
|
|
190
|
+
const agentEffectiveAllowlist = intersectEffectiveAllowlistWithSnapshot(
|
|
191
|
+
intersectEffectiveAllowlistWithSnapshot(
|
|
192
|
+
baseEffectiveAllowlist,
|
|
193
|
+
knownProviderIdsForAllowlist,
|
|
194
|
+
agentEnvAllowlist
|
|
195
|
+
),
|
|
196
|
+
knownProviderIdsForAllowlist,
|
|
197
|
+
agentTenantAllowlist
|
|
198
|
+
);
|
|
199
|
+
const agentModelEnvVars = Object.fromEntries(
|
|
200
|
+
knownProviderIdsForAllowlist.map((providerId2) => [
|
|
201
|
+
providerId2,
|
|
202
|
+
agentOverrideModelAllowlistEnvVarName(agent.id, providerId2)
|
|
203
|
+
])
|
|
204
|
+
);
|
|
205
|
+
return {
|
|
206
|
+
agentId: agent.id,
|
|
207
|
+
moduleId: agent.moduleId,
|
|
208
|
+
allowRuntimeModelOverride: agent.allowRuntimeModelOverride !== false,
|
|
209
|
+
codeDefaultProviderId: agent.defaultProvider ?? null,
|
|
210
|
+
codeDefaultModelId: agent.defaultModel ?? null,
|
|
211
|
+
override: agentOverrideRow ? {
|
|
212
|
+
providerId: agentOverrideRow.providerId ?? null,
|
|
213
|
+
modelId: agentOverrideRow.modelId ?? null,
|
|
214
|
+
baseURL: agentOverrideRow.baseUrl ?? null,
|
|
215
|
+
updatedAt: agentOverrideRow.updatedAt.toISOString()
|
|
216
|
+
} : null,
|
|
217
|
+
runtimeOverrideAllowlist: {
|
|
218
|
+
env: agentEnvAllowlist,
|
|
219
|
+
tenant: hasAllowlistSnapshotRestrictions(agentTenantAllowlist) ? agentTenantAllowlist : null,
|
|
220
|
+
effective: agentEffectiveAllowlist,
|
|
221
|
+
envVarNames: {
|
|
222
|
+
providers: agentOverrideProviderAllowlistEnvVarName(agent.id),
|
|
223
|
+
modelsByProvider: agentModelEnvVars
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
providerId: agentResolution.providerId,
|
|
227
|
+
modelId: agentResolution.modelId,
|
|
228
|
+
baseURL: agentResolution.baseURL ?? null,
|
|
229
|
+
source: agentResolution.source
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
agentResolutions = await Promise.all(agentResolutionPromises);
|
|
233
|
+
}
|
|
234
|
+
} catch (overrideError) {
|
|
235
|
+
console.warn("[AI Settings] Failed to compute Phase 4a override fields:", overrideError);
|
|
236
|
+
}
|
|
237
|
+
const allowlistConfig = readAllowlistConfig(env, knownProviderIdsForAllowlist);
|
|
238
|
+
const effectiveAllowlist = intersectAllowlists(
|
|
239
|
+
env,
|
|
240
|
+
knownProviderIdsForAllowlist,
|
|
241
|
+
tenantAllowlistSnapshot
|
|
242
|
+
);
|
|
243
|
+
const allRawProviders = [
|
|
244
|
+
...OPEN_CODE_PROVIDER_IDS.map((id) => {
|
|
43
245
|
const info = OPEN_CODE_PROVIDERS[id];
|
|
246
|
+
const registryProvider2 = llmProviderRegistry.get(id);
|
|
44
247
|
return {
|
|
45
248
|
id,
|
|
46
249
|
name: info.name,
|
|
47
250
|
defaultModel: info.defaultModel,
|
|
48
251
|
envKey: getOpenCodeProviderConfiguredEnvKey(id),
|
|
49
|
-
configured: isOpenCodeProviderConfigured(id)
|
|
252
|
+
configured: isOpenCodeProviderConfigured(id),
|
|
253
|
+
defaultModels: registryProvider2?.defaultModels ?? []
|
|
50
254
|
};
|
|
51
255
|
}),
|
|
52
|
-
|
|
256
|
+
// Also surface any llmProviderRegistry providers not in OPEN_CODE_PROVIDER_IDS
|
|
257
|
+
...llmProviderRegistry.list().filter((p) => !OPEN_CODE_PROVIDER_IDS.includes(p.id)).map((p) => ({
|
|
258
|
+
id: p.id,
|
|
259
|
+
name: p.name,
|
|
260
|
+
defaultModel: p.defaultModels[0]?.id ?? "",
|
|
261
|
+
envKey: null,
|
|
262
|
+
configured: p.isConfigured(),
|
|
263
|
+
defaultModels: p.defaultModels
|
|
264
|
+
}))
|
|
265
|
+
];
|
|
266
|
+
const availableProviders = allRawProviders.filter((p) => isProviderAllowedInEffective(effectiveAllowlist, p.id)).map((p) => {
|
|
267
|
+
const effectiveModelsList = effectiveAllowlist.modelsByProvider[p.id];
|
|
268
|
+
const catalogModels = modelCatalogWithAllowlistFallback(
|
|
269
|
+
p.defaultModels,
|
|
270
|
+
effectiveModelsList
|
|
271
|
+
);
|
|
272
|
+
const clippedDefaults = effectiveModelsList !== void 0 ? catalogModels.filter((m) => effectiveModelsList.includes(m.id)) : catalogModels;
|
|
273
|
+
return {
|
|
274
|
+
...p,
|
|
275
|
+
defaultModel: effectiveModelsList && !effectiveModelsList.includes(p.defaultModel) ? effectiveModelsList[0] ?? p.defaultModel : p.defaultModel || clippedDefaults[0]?.id || "",
|
|
276
|
+
defaultModels: clippedDefaults
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
const allowlistProviders = allRawProviders.filter((p) => isProviderAllowed(env, p.id)).map((p) => {
|
|
280
|
+
const envModelsList = allowlistConfig.modelsByProvider[p.id];
|
|
281
|
+
const catalogModels = modelCatalogWithAllowlistFallback(
|
|
282
|
+
p.defaultModels,
|
|
283
|
+
envModelsList
|
|
284
|
+
);
|
|
285
|
+
const envClippedDefaults = envModelsList !== void 0 ? catalogModels.filter((m) => envModelsList.includes(m.id)) : catalogModels;
|
|
286
|
+
return {
|
|
287
|
+
...p,
|
|
288
|
+
defaultModel: envModelsList && !envModelsList.includes(p.defaultModel) ? envModelsList[0] ?? p.defaultModel : p.defaultModel || envClippedDefaults[0]?.id || "",
|
|
289
|
+
defaultModels: envClippedDefaults
|
|
290
|
+
};
|
|
291
|
+
});
|
|
292
|
+
return NextResponse.json({
|
|
293
|
+
provider: {
|
|
294
|
+
id: providerId,
|
|
295
|
+
name: providerName,
|
|
296
|
+
model: resolvedDefault ? `${resolvedDefault.providerId}/${resolvedDefault.modelId}` : fallbackModelWithProvider,
|
|
297
|
+
defaultModel: defaultProviderModel,
|
|
298
|
+
envKey: displayEnvKey,
|
|
299
|
+
configured: apiKeyConfigured
|
|
300
|
+
},
|
|
301
|
+
availableProviders,
|
|
302
|
+
// Editable universe for the tenant allowlist page. This is clipped only
|
|
303
|
+
// by env so tenant-hidden models remain visible and can be re-enabled.
|
|
304
|
+
allowlistProviders,
|
|
305
|
+
// Snapshot of the env-driven allowlist so the UI can render hints like
|
|
306
|
+
// "limited to: openai, anthropic" without re-implementing the parser.
|
|
307
|
+
allowlist: allowlistConfig,
|
|
308
|
+
// Per-tenant allowlist snapshot (Phase 1780-6). `null` when no row has
|
|
309
|
+
// been persisted yet — the runtime then falls back to env-only
|
|
310
|
+
// enforcement. The UI uses this to drive the editable MultiSelect.
|
|
311
|
+
tenantAllowlist: tenantAllowlistSnapshot,
|
|
312
|
+
// Effective allowlist after intersecting env with tenant. The UI uses
|
|
313
|
+
// this to render the "what the runtime will actually accept" summary
|
|
314
|
+
// and to clip pickers without re-implementing the intersection.
|
|
315
|
+
effectiveAllowlist,
|
|
316
|
+
mcpKeyConfigured,
|
|
317
|
+
resolvedDefault,
|
|
318
|
+
tenantOverride,
|
|
319
|
+
agents: agentResolutions
|
|
53
320
|
});
|
|
54
321
|
} catch (error) {
|
|
55
322
|
console.error("[AI Settings] GET error:", error);
|
|
56
323
|
return NextResponse.json({ error: "Failed to fetch settings" }, { status: 500 });
|
|
57
324
|
}
|
|
58
325
|
}
|
|
326
|
+
async function PUT(req) {
|
|
327
|
+
const auth = await getAuthFromRequest(req);
|
|
328
|
+
if (!auth?.sub) {
|
|
329
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
330
|
+
}
|
|
331
|
+
let parsedBody;
|
|
332
|
+
try {
|
|
333
|
+
parsedBody = await req.json();
|
|
334
|
+
} catch {
|
|
335
|
+
return NextResponse.json({ error: "Request body must be valid JSON.", code: "validation_error" }, { status: 400 });
|
|
336
|
+
}
|
|
337
|
+
const bodyResult = runtimeOverrideUpsertSchema.safeParse(parsedBody);
|
|
338
|
+
if (!bodyResult.success) {
|
|
339
|
+
return NextResponse.json(
|
|
340
|
+
{ error: "Invalid request body.", code: "validation_error", issues: bodyResult.error.issues },
|
|
341
|
+
{ status: 400 }
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
const { providerId: requestedProviderId, modelId, baseURL, agentId } = bodyResult.data;
|
|
345
|
+
const knownProviderIdsForRequest = llmProviderRegistry.list().map((p) => p.id);
|
|
346
|
+
const providerId = requestedProviderId ? canonicalProviderId(requestedProviderId, knownProviderIdsForRequest) ?? requestedProviderId : requestedProviderId;
|
|
347
|
+
if (baseURL && baseURL.trim().length > 0) {
|
|
348
|
+
const allowlist = readBaseurlAllowlist();
|
|
349
|
+
if (!isBaseurlAllowlisted(baseURL.trim(), allowlist)) {
|
|
350
|
+
return NextResponse.json(
|
|
351
|
+
{
|
|
352
|
+
error: `baseURL "${baseURL}" is not in AI_RUNTIME_BASEURL_ALLOWLIST.`,
|
|
353
|
+
code: "baseurl_not_allowlisted"
|
|
354
|
+
},
|
|
355
|
+
{ status: 400 }
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const allowedOverrideProviders = bodyResult.data.allowedOverrideProviders === void 0 ? void 0 : bodyResult.data.allowedOverrideProviders?.map(
|
|
360
|
+
(id) => canonicalProviderId(id, knownProviderIdsForRequest) ?? id
|
|
361
|
+
) ?? null;
|
|
362
|
+
const allowedOverrideModelsByProvider = bodyResult.data.allowedOverrideModelsByProvider === void 0 ? void 0 : Object.fromEntries(
|
|
363
|
+
Object.entries(bodyResult.data.allowedOverrideModelsByProvider ?? {}).map(([id, models]) => [
|
|
364
|
+
canonicalProviderId(id, knownProviderIdsForRequest) ?? id,
|
|
365
|
+
models
|
|
366
|
+
])
|
|
367
|
+
);
|
|
368
|
+
const hasRuntimeOverrideAllowlistWrite = allowedOverrideProviders !== void 0 || allowedOverrideModelsByProvider !== void 0;
|
|
369
|
+
let putEffectiveAllowlist = null;
|
|
370
|
+
if (providerId || hasRuntimeOverrideAllowlistWrite) {
|
|
371
|
+
try {
|
|
372
|
+
const previewContainer = await createRequestContainer();
|
|
373
|
+
const knownIdsForCheck = [
|
|
374
|
+
...OPEN_CODE_PROVIDER_IDS,
|
|
375
|
+
...llmProviderRegistry.list().map((p) => p.id).filter((id) => !OPEN_CODE_PROVIDER_IDS.includes(id))
|
|
376
|
+
];
|
|
377
|
+
let snapshot = null;
|
|
378
|
+
if (auth.tenantId) {
|
|
379
|
+
try {
|
|
380
|
+
const em = previewContainer.resolve("em");
|
|
381
|
+
const allowlistRepo = new AiTenantModelAllowlistRepository(em);
|
|
382
|
+
snapshot = await allowlistRepo.getSnapshot({
|
|
383
|
+
tenantId: auth.tenantId,
|
|
384
|
+
organizationId: auth.orgId ?? null
|
|
385
|
+
});
|
|
386
|
+
} catch {
|
|
387
|
+
snapshot = null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
putEffectiveAllowlist = intersectAllowlists(
|
|
391
|
+
process.env,
|
|
392
|
+
knownIdsForCheck,
|
|
393
|
+
snapshot
|
|
394
|
+
);
|
|
395
|
+
} catch {
|
|
396
|
+
putEffectiveAllowlist = null;
|
|
397
|
+
}
|
|
398
|
+
if (providerId && putEffectiveAllowlist) {
|
|
399
|
+
if (!isProviderAllowedInEffective(putEffectiveAllowlist, providerId)) {
|
|
400
|
+
const source = putEffectiveAllowlist.tenantOverridesActive ? "the effective allowlist (env \u2229 tenant)" : "OM_AI_AVAILABLE_PROVIDERS";
|
|
401
|
+
return NextResponse.json(
|
|
402
|
+
{
|
|
403
|
+
error: `Provider "${providerId}" is not in ${source}.`,
|
|
404
|
+
code: "provider_not_allowlisted"
|
|
405
|
+
},
|
|
406
|
+
{ status: 400 }
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
if (modelId && !isProviderModelAllowedInEffective(putEffectiveAllowlist, providerId, modelId)) {
|
|
410
|
+
const source = putEffectiveAllowlist.tenantOverridesActive ? `the effective allowlist (env \u2229 tenant) for "${providerId}"` : modelAllowlistEnvVarName(providerId);
|
|
411
|
+
return NextResponse.json(
|
|
412
|
+
{
|
|
413
|
+
error: `Model "${modelId}" is not in ${source}.`,
|
|
414
|
+
code: "model_not_allowlisted"
|
|
415
|
+
},
|
|
416
|
+
{ status: 400 }
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
} else if (providerId) {
|
|
420
|
+
if (!isProviderAllowed(process.env, providerId)) {
|
|
421
|
+
return NextResponse.json(
|
|
422
|
+
{
|
|
423
|
+
error: `Provider "${requestedProviderId}" is not in OM_AI_AVAILABLE_PROVIDERS.`,
|
|
424
|
+
code: "provider_not_allowlisted"
|
|
425
|
+
},
|
|
426
|
+
{ status: 400 }
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
if (modelId && !isProviderModelAllowed(process.env, providerId, modelId)) {
|
|
430
|
+
return NextResponse.json(
|
|
431
|
+
{
|
|
432
|
+
error: `Model "${modelId}" is not in ${modelAllowlistEnvVarName(providerId)}.`,
|
|
433
|
+
code: "model_not_allowlisted"
|
|
434
|
+
},
|
|
435
|
+
{ status: 400 }
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (hasRuntimeOverrideAllowlistWrite && !agentId) {
|
|
441
|
+
return NextResponse.json(
|
|
442
|
+
{
|
|
443
|
+
error: "agentId is required when saving chat override allowlist settings.",
|
|
444
|
+
code: "agent_required"
|
|
445
|
+
},
|
|
446
|
+
{ status: 400 }
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
if (Array.isArray(allowedOverrideProviders)) {
|
|
450
|
+
for (const id of allowedOverrideProviders) {
|
|
451
|
+
if (putEffectiveAllowlist && !isProviderAllowedInEffective(putEffectiveAllowlist, id)) {
|
|
452
|
+
return NextResponse.json(
|
|
453
|
+
{
|
|
454
|
+
error: `Provider "${id}" is not in the effective tenant allowlist; per-agent chat override choices may not widen it.`,
|
|
455
|
+
code: "provider_not_allowlisted"
|
|
456
|
+
},
|
|
457
|
+
{ status: 400 }
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (allowedOverrideModelsByProvider) {
|
|
463
|
+
for (const [id, models] of Object.entries(allowedOverrideModelsByProvider)) {
|
|
464
|
+
if (putEffectiveAllowlist && !isProviderAllowedInEffective(putEffectiveAllowlist, id)) {
|
|
465
|
+
return NextResponse.json(
|
|
466
|
+
{
|
|
467
|
+
error: `Provider "${id}" is not in the effective tenant allowlist; cannot save per-agent model choices for it.`,
|
|
468
|
+
code: "provider_not_allowlisted"
|
|
469
|
+
},
|
|
470
|
+
{ status: 400 }
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
for (const allowedModelId of models) {
|
|
474
|
+
if (putEffectiveAllowlist && !isProviderModelAllowedInEffective(putEffectiveAllowlist, id, allowedModelId)) {
|
|
475
|
+
return NextResponse.json(
|
|
476
|
+
{
|
|
477
|
+
error: `Model "${allowedModelId}" is not in the effective tenant allowlist for "${id}".`,
|
|
478
|
+
code: "model_not_allowlisted"
|
|
479
|
+
},
|
|
480
|
+
{ status: 400 }
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const container = await createRequestContainer();
|
|
488
|
+
const rbacService = container.resolve("rbacService");
|
|
489
|
+
const acl = await rbacService.loadAcl(auth.sub, {
|
|
490
|
+
tenantId: auth.tenantId,
|
|
491
|
+
organizationId: auth.orgId
|
|
492
|
+
});
|
|
493
|
+
const canManage = acl.isSuperAdmin || acl.features.includes("ai_assistant.settings.manage");
|
|
494
|
+
if (!canManage) {
|
|
495
|
+
return NextResponse.json({ error: "Forbidden", code: "forbidden" }, { status: 403 });
|
|
496
|
+
}
|
|
497
|
+
const em = container.resolve("em");
|
|
498
|
+
const repo = new AiAgentRuntimeOverrideRepository(em);
|
|
499
|
+
const upsertInput = {
|
|
500
|
+
agentId: agentId ?? null,
|
|
501
|
+
...Object.prototype.hasOwnProperty.call(bodyResult.data, "providerId") ? { providerId: providerId ?? null } : {},
|
|
502
|
+
...Object.prototype.hasOwnProperty.call(bodyResult.data, "modelId") ? { modelId: modelId ?? null } : {},
|
|
503
|
+
...Object.prototype.hasOwnProperty.call(bodyResult.data, "baseURL") ? { baseURL: baseURL ?? null } : {},
|
|
504
|
+
...allowedOverrideProviders !== void 0 ? { allowedOverrideProviders } : {},
|
|
505
|
+
...allowedOverrideModelsByProvider !== void 0 ? { allowedOverrideModelsByProvider } : {}
|
|
506
|
+
};
|
|
507
|
+
const row = await repo.upsertDefault(
|
|
508
|
+
upsertInput,
|
|
509
|
+
{ tenantId: auth.tenantId ?? "", organizationId: auth.orgId ?? null, userId: auth.sub }
|
|
510
|
+
);
|
|
511
|
+
return NextResponse.json({
|
|
512
|
+
id: row.id,
|
|
513
|
+
tenantId: row.tenantId,
|
|
514
|
+
organizationId: row.organizationId,
|
|
515
|
+
agentId: row.agentId,
|
|
516
|
+
providerId: row.providerId,
|
|
517
|
+
modelId: row.modelId,
|
|
518
|
+
baseURL: row.baseUrl,
|
|
519
|
+
allowedOverrideProviders: row.allowedOverrideProviders ?? null,
|
|
520
|
+
allowedOverrideModelsByProvider: row.allowedOverrideModelsByProvider ?? {},
|
|
521
|
+
updatedAt: row.updatedAt
|
|
522
|
+
});
|
|
523
|
+
} catch (error) {
|
|
524
|
+
if (error instanceof AiAgentRuntimeOverrideValidationError) {
|
|
525
|
+
return NextResponse.json({ error: error.message, code: "provider_unknown" }, { status: 400 });
|
|
526
|
+
}
|
|
527
|
+
console.error("[AI Settings] PUT error:", error);
|
|
528
|
+
return NextResponse.json({ error: "Failed to save runtime override." }, { status: 500 });
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async function DELETE(req) {
|
|
532
|
+
const auth = await getAuthFromRequest(req);
|
|
533
|
+
if (!auth?.sub) {
|
|
534
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
535
|
+
}
|
|
536
|
+
let parsedBody = {};
|
|
537
|
+
try {
|
|
538
|
+
parsedBody = await req.json();
|
|
539
|
+
} catch {
|
|
540
|
+
}
|
|
541
|
+
const bodyResult = runtimeOverrideClearSchema.safeParse(parsedBody);
|
|
542
|
+
if (!bodyResult.success) {
|
|
543
|
+
return NextResponse.json(
|
|
544
|
+
{ error: "Invalid request body.", code: "validation_error", issues: bodyResult.error.issues },
|
|
545
|
+
{ status: 400 }
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
const container = await createRequestContainer();
|
|
550
|
+
const rbacService = container.resolve("rbacService");
|
|
551
|
+
const acl = await rbacService.loadAcl(auth.sub, {
|
|
552
|
+
tenantId: auth.tenantId,
|
|
553
|
+
organizationId: auth.orgId
|
|
554
|
+
});
|
|
555
|
+
const canManage = acl.isSuperAdmin || acl.features.includes("ai_assistant.settings.manage");
|
|
556
|
+
if (!canManage) {
|
|
557
|
+
return NextResponse.json({ error: "Forbidden", code: "forbidden" }, { status: 403 });
|
|
558
|
+
}
|
|
559
|
+
const em = container.resolve("em");
|
|
560
|
+
const repo = new AiAgentRuntimeOverrideRepository(em);
|
|
561
|
+
const cleared = await repo.clearDefault({
|
|
562
|
+
tenantId: auth.tenantId ?? "",
|
|
563
|
+
organizationId: auth.orgId ?? null,
|
|
564
|
+
agentId: bodyResult.data.agentId ?? null
|
|
565
|
+
});
|
|
566
|
+
return NextResponse.json({ cleared });
|
|
567
|
+
} catch (error) {
|
|
568
|
+
console.error("[AI Settings] DELETE error:", error);
|
|
569
|
+
return NextResponse.json({ error: "Failed to clear runtime override." }, { status: 500 });
|
|
570
|
+
}
|
|
571
|
+
}
|
|
59
572
|
export {
|
|
573
|
+
DELETE,
|
|
60
574
|
GET,
|
|
575
|
+
PUT,
|
|
61
576
|
metadata,
|
|
62
577
|
openApi
|
|
63
578
|
};
|