@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.
Files changed (133) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +82 -18
  3. package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js +370 -0
  4. package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +7 -0
  5. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +194 -0
  6. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +7 -0
  7. package/dist/modules/ai_assistant/api/ai/agents/route.js +4 -0
  8. package/dist/modules/ai_assistant/api/ai/agents/route.js.map +2 -2
  9. package/dist/modules/ai_assistant/api/ai/chat/route.js +169 -5
  10. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
  11. package/dist/modules/ai_assistant/api/route/route.js +38 -19
  12. package/dist/modules/ai_assistant/api/route/route.js.map +3 -3
  13. package/dist/modules/ai_assistant/api/settings/allowlist/route.js +195 -0
  14. package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +7 -0
  15. package/dist/modules/ai_assistant/api/settings/route.js +537 -22
  16. package/dist/modules/ai_assistant/api/settings/route.js.map +3 -3
  17. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +701 -147
  18. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
  19. package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +338 -0
  20. package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +7 -0
  21. package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.js +10 -0
  22. package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.js.map +7 -0
  23. package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.js +25 -0
  24. package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.js.map +7 -0
  25. package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js +1 -1
  26. package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js.map +2 -2
  27. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +75 -26
  28. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
  29. package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.js +10 -0
  30. package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.js.map +7 -0
  31. package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.js +25 -0
  32. package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.js.map +7 -0
  33. package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js +503 -168
  34. package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js.map +2 -2
  35. package/dist/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.js +5 -0
  36. package/dist/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.js.map +7 -0
  37. package/dist/modules/ai_assistant/data/entities/AiTenantModelAllowlist.js +5 -0
  38. package/dist/modules/ai_assistant/data/entities/AiTenantModelAllowlist.js.map +7 -0
  39. package/dist/modules/ai_assistant/data/entities.js +123 -1
  40. package/dist/modules/ai_assistant/data/entities.js.map +2 -2
  41. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +157 -0
  42. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +7 -0
  43. package/dist/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.js +77 -0
  44. package/dist/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.js.map +7 -0
  45. package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js +1 -1
  46. package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js.map +2 -2
  47. package/dist/modules/ai_assistant/i18n/de.json +90 -1
  48. package/dist/modules/ai_assistant/i18n/en.json +90 -1
  49. package/dist/modules/ai_assistant/i18n/es.json +90 -1
  50. package/dist/modules/ai_assistant/i18n/pl.json +90 -1
  51. package/dist/modules/ai_assistant/lib/agent-registry.js +17 -1
  52. package/dist/modules/ai_assistant/lib/agent-registry.js.map +2 -2
  53. package/dist/modules/ai_assistant/lib/agent-runtime.js +133 -36
  54. package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
  55. package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
  56. package/dist/modules/ai_assistant/lib/baseurl-allowlist.js +29 -0
  57. package/dist/modules/ai_assistant/lib/baseurl-allowlist.js.map +7 -0
  58. package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js +4 -1
  59. package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +2 -2
  60. package/dist/modules/ai_assistant/lib/llm-adapters/google.js +4 -1
  61. package/dist/modules/ai_assistant/lib/llm-adapters/google.js.map +2 -2
  62. package/dist/modules/ai_assistant/lib/model-allowlist.js +211 -0
  63. package/dist/modules/ai_assistant/lib/model-allowlist.js.map +7 -0
  64. package/dist/modules/ai_assistant/lib/model-factory.js +203 -31
  65. package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
  66. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +32 -1
  67. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +2 -2
  68. package/dist/modules/ai_assistant/migrations/Migration20260508140000.js +18 -0
  69. package/dist/modules/ai_assistant/migrations/Migration20260508140000.js.map +7 -0
  70. package/dist/modules/ai_assistant/migrations/Migration20260512090000.js +16 -0
  71. package/dist/modules/ai_assistant/migrations/Migration20260512090000.js.map +7 -0
  72. package/dist/modules/ai_assistant/migrations/Migration20260512130000.js +15 -0
  73. package/dist/modules/ai_assistant/migrations/Migration20260512130000.js.map +7 -0
  74. package/generated/entities/ai_agent_runtime_override/index.ts +13 -0
  75. package/generated/entities/ai_tenant_model_allowlist/index.ts +9 -0
  76. package/generated/entities.ids.generated.ts +2 -0
  77. package/generated/entity-fields-registry.ts +26 -0
  78. package/jest.config.cjs +2 -0
  79. package/package.json +4 -4
  80. package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts +477 -0
  81. package/src/modules/ai_assistant/__tests__/settings-page-logic.test.ts +116 -0
  82. package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/__tests__/route.test.ts +240 -0
  83. package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +251 -0
  84. package/src/modules/ai_assistant/api/ai/agents/route.ts +4 -0
  85. package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +273 -0
  86. package/src/modules/ai_assistant/api/ai/chat/route.ts +211 -2
  87. package/src/modules/ai_assistant/api/route/route.ts +49 -25
  88. package/src/modules/ai_assistant/api/settings/__tests__/route.test.ts +408 -0
  89. package/src/modules/ai_assistant/api/settings/allowlist/route.ts +221 -0
  90. package/src/modules/ai_assistant/api/settings/route.ts +721 -27
  91. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +858 -177
  92. package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +458 -0
  93. package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.ts +23 -0
  94. package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.tsx +12 -0
  95. package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.tsx +1 -1
  96. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +89 -12
  97. package/src/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.ts +23 -0
  98. package/src/modules/ai_assistant/backend/config/ai-assistant/settings/page.tsx +18 -0
  99. package/src/modules/ai_assistant/components/AiAssistantSettingsPageClient.tsx +617 -209
  100. package/src/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.ts +7 -0
  101. package/src/modules/ai_assistant/data/entities/AiTenantModelAllowlist.ts +2 -0
  102. package/src/modules/ai_assistant/data/entities.ts +164 -0
  103. package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +227 -0
  104. package/src/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.ts +132 -0
  105. package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentRuntimeOverrideRepository.test.ts +337 -0
  106. package/src/modules/ai_assistant/data/repositories/__tests__/AiTenantModelAllowlistRepository.test.ts +181 -0
  107. package/src/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.tsx +1 -1
  108. package/src/modules/ai_assistant/i18n/de.json +90 -1
  109. package/src/modules/ai_assistant/i18n/en.json +90 -1
  110. package/src/modules/ai_assistant/i18n/es.json +90 -1
  111. package/src/modules/ai_assistant/i18n/pl.json +90 -1
  112. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +396 -0
  113. package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +60 -6
  114. package/src/modules/ai_assistant/lib/__tests__/ai-api-operation-runner.test.ts +4 -2
  115. package/src/modules/ai_assistant/lib/__tests__/baseurl-allowlist.test.ts +75 -0
  116. package/src/modules/ai_assistant/lib/__tests__/llm-adapters-anthropic.test.ts +18 -0
  117. package/src/modules/ai_assistant/lib/__tests__/llm-adapters-google.test.ts +18 -0
  118. package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +150 -4
  119. package/src/modules/ai_assistant/lib/__tests__/model-allowlist.test.ts +290 -0
  120. package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +634 -0
  121. package/src/modules/ai_assistant/lib/agent-registry.ts +20 -1
  122. package/src/modules/ai_assistant/lib/agent-runtime.ts +220 -44
  123. package/src/modules/ai_assistant/lib/ai-agent-definition.ts +48 -0
  124. package/src/modules/ai_assistant/lib/baseurl-allowlist.ts +64 -0
  125. package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +11 -1
  126. package/src/modules/ai_assistant/lib/llm-adapters/google.ts +4 -1
  127. package/src/modules/ai_assistant/lib/model-allowlist.ts +407 -0
  128. package/src/modules/ai_assistant/lib/model-factory.ts +486 -58
  129. package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +44 -0
  130. package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +704 -235
  131. package/src/modules/ai_assistant/migrations/Migration20260508140000.ts +18 -0
  132. package/src/modules/ai_assistant/migrations/Migration20260512090000.ts +16 -0
  133. 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 providerId = resolveAiProviderIdFromEnv(process.env);
28
- const providerInfo = OPEN_CODE_PROVIDERS[providerId];
29
- const apiKeyConfigured = isOpenCodeProviderConfigured(providerId);
30
- const resolvedModel = resolveOpenCodeModel(providerId);
31
- const displayEnvKey = getOpenCodeProviderConfiguredEnvKey(providerId);
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
- return NextResponse.json({
34
- provider: {
35
- id: providerId,
36
- name: providerInfo.name,
37
- model: resolvedModel.modelWithProvider,
38
- defaultModel: providerInfo.defaultModel,
39
- envKey: displayEnvKey,
40
- configured: apiKeyConfigured
41
- },
42
- availableProviders: OPEN_CODE_PROVIDER_IDS.map((id) => {
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
- mcpKeyConfigured
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
  };