@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,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Env-driven provider/model allowlist (Phase 1780-5).
|
|
3
|
+
*
|
|
4
|
+
* Two env variables govern which AI providers/models the runtime accepts. They
|
|
5
|
+
* are the ULTIMATE constraint — settings UI, per-tenant overrides, per-agent
|
|
6
|
+
* defaults, and request-time overrides are all clipped against this list. When
|
|
7
|
+
* a caller asks for a provider/model outside the allowlist the runtime emits a
|
|
8
|
+
* warning and falls back to the agent's default (or to the first allowlisted
|
|
9
|
+
* pair when even the default is rejected).
|
|
10
|
+
*
|
|
11
|
+
* - `OM_AI_AVAILABLE_PROVIDERS=openai,anthropic`
|
|
12
|
+
* Comma-separated provider ids. Unset/empty → no provider restriction.
|
|
13
|
+
* Whitespace-tolerant; case-insensitive comparison against the registry.
|
|
14
|
+
*
|
|
15
|
+
* - `OM_AI_AVAILABLE_MODELS_<PROVIDER>=gpt-5-mini,gpt-5`
|
|
16
|
+
* Per-provider comma-separated model id list. Unset/empty → no model
|
|
17
|
+
* restriction for that provider. PROVIDER is uppercased from the registry
|
|
18
|
+
* id (e.g. `openai` → `OM_AI_AVAILABLE_MODELS_OPENAI`).
|
|
19
|
+
*
|
|
20
|
+
* Both vars are read fresh on every call so hot-reload and test overrides
|
|
21
|
+
* work without re-creating the factory.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export type EnvLookup = Record<string, string | undefined>
|
|
25
|
+
|
|
26
|
+
const PROVIDERS_ENV = 'OM_AI_AVAILABLE_PROVIDERS'
|
|
27
|
+
|
|
28
|
+
function envProvidersVarName(): string {
|
|
29
|
+
return PROVIDERS_ENV
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function envModelsVarName(providerId: string): string {
|
|
33
|
+
const envSafeProviderId = providerId.toUpperCase().replace(/[^A-Z0-9]/g, '_')
|
|
34
|
+
return `OM_AI_AVAILABLE_MODELS_${envSafeProviderId}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function envSafeId(value: string): string {
|
|
38
|
+
return value.toUpperCase().replace(/[^A-Z0-9]/g, '_')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function agentOverrideProvidersVarName(agentId: string): string {
|
|
42
|
+
return `OM_AI_AGENT_${envSafeId(agentId)}_AVAILABLE_PROVIDERS`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function agentOverrideModelsVarName(agentId: string, providerId: string): string {
|
|
46
|
+
return `OM_AI_AGENT_${envSafeId(agentId)}_AVAILABLE_MODELS_${envSafeId(providerId)}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function normalizeProviderId(value: string | null | undefined): string {
|
|
50
|
+
return value?.trim().toLowerCase().replace(/_/g, '-') ?? ''
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function providerIdAliases(providerId: string): string[] {
|
|
54
|
+
const normalized = normalizeProviderId(providerId)
|
|
55
|
+
if (!normalized) return []
|
|
56
|
+
return Array.from(new Set([normalized, normalized.replace(/-/g, '_')]))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function canonicalProviderId(
|
|
60
|
+
providerId: string,
|
|
61
|
+
knownProviderIds: readonly string[],
|
|
62
|
+
): string | null {
|
|
63
|
+
const normalized = normalizeProviderId(providerId)
|
|
64
|
+
return knownProviderIds.find((id) => normalizeProviderId(id) === normalized) ?? null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function canonicalizeProviderList(
|
|
68
|
+
providerIds: string[] | null,
|
|
69
|
+
knownProviderIds: readonly string[],
|
|
70
|
+
): string[] | null {
|
|
71
|
+
if (providerIds === null) return null
|
|
72
|
+
const result: string[] = []
|
|
73
|
+
const seen = new Set<string>()
|
|
74
|
+
for (const providerId of providerIds) {
|
|
75
|
+
const canonical = canonicalProviderId(providerId, knownProviderIds) ?? normalizeProviderId(providerId)
|
|
76
|
+
const key = normalizeProviderId(canonical)
|
|
77
|
+
if (seen.has(key)) continue
|
|
78
|
+
seen.add(key)
|
|
79
|
+
result.push(canonical)
|
|
80
|
+
}
|
|
81
|
+
return result
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseList(raw: string | undefined): string[] | null {
|
|
85
|
+
if (raw === undefined) return null
|
|
86
|
+
const trimmed = raw.trim()
|
|
87
|
+
if (trimmed.length === 0) return null
|
|
88
|
+
const items = trimmed
|
|
89
|
+
.split(',')
|
|
90
|
+
.map((entry) => entry.trim())
|
|
91
|
+
.filter((entry) => entry.length > 0)
|
|
92
|
+
return items.length > 0 ? items : null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Reads the configured provider allowlist. Returns `null` when the env var is
|
|
97
|
+
* unset or empty (no restriction). Otherwise returns the parsed list of
|
|
98
|
+
* provider ids (preserving caller order; case preserved as-written).
|
|
99
|
+
*/
|
|
100
|
+
export function readAllowedProviders(env: EnvLookup = process.env): string[] | null {
|
|
101
|
+
return parseList(env[PROVIDERS_ENV])
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reads the configured per-provider model allowlist. Returns `null` when the
|
|
106
|
+
* env var is unset or empty (no restriction for that provider). Otherwise
|
|
107
|
+
* returns the parsed model id list.
|
|
108
|
+
*/
|
|
109
|
+
export function readAllowedModels(
|
|
110
|
+
env: EnvLookup,
|
|
111
|
+
providerId: string,
|
|
112
|
+
): string[] | null {
|
|
113
|
+
return parseList(env[envModelsVarName(providerId)])
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Snapshot of the env-driven allowlist suitable for the settings GET response.
|
|
118
|
+
* `hasRestrictions` is `true` when at least one of the env vars is set.
|
|
119
|
+
*/
|
|
120
|
+
export interface AllowlistConfig {
|
|
121
|
+
providers: string[] | null
|
|
122
|
+
modelsByProvider: Record<string, string[]>
|
|
123
|
+
hasRestrictions: boolean
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Reads the full allowlist for every provider in `knownProviderIds` and
|
|
128
|
+
* returns a snapshot. Use the settings response to feed UI dropdowns so the
|
|
129
|
+
* picker can only offer values the runtime would accept.
|
|
130
|
+
*/
|
|
131
|
+
export function readAllowlistConfig(
|
|
132
|
+
env: EnvLookup = process.env,
|
|
133
|
+
knownProviderIds: string[] = [],
|
|
134
|
+
): AllowlistConfig {
|
|
135
|
+
const providers = readAllowedProviders(env)
|
|
136
|
+
const modelsByProvider: Record<string, string[]> = {}
|
|
137
|
+
for (const providerId of knownProviderIds) {
|
|
138
|
+
const list = readAllowedModels(env, providerId)
|
|
139
|
+
if (list !== null) modelsByProvider[providerId] = list
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
providers,
|
|
143
|
+
modelsByProvider,
|
|
144
|
+
hasRestrictions: providers !== null || Object.keys(modelsByProvider).length > 0,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Returns `true` when the provider is permitted by the allowlist (or when no
|
|
150
|
+
* provider restriction is configured). Case-insensitive id comparison.
|
|
151
|
+
*/
|
|
152
|
+
export function isProviderAllowed(
|
|
153
|
+
env: EnvLookup,
|
|
154
|
+
providerId: string,
|
|
155
|
+
): boolean {
|
|
156
|
+
const allowed = readAllowedProviders(env)
|
|
157
|
+
if (allowed === null) return true
|
|
158
|
+
const needle = normalizeProviderId(providerId)
|
|
159
|
+
return allowed.some((id) => normalizeProviderId(id) === needle)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Returns `true` when the model is permitted for the provider by the
|
|
164
|
+
* allowlist (or when no per-provider model restriction is configured).
|
|
165
|
+
* Case-sensitive model id comparison — model ids are vendor-specified strings
|
|
166
|
+
* (e.g. `gpt-5-mini`, `claude-opus-4-20250514`).
|
|
167
|
+
*/
|
|
168
|
+
export function isModelAllowedForProvider(
|
|
169
|
+
env: EnvLookup,
|
|
170
|
+
providerId: string,
|
|
171
|
+
modelId: string,
|
|
172
|
+
): boolean {
|
|
173
|
+
const allowed = readAllowedModels(env, providerId)
|
|
174
|
+
if (allowed === null) return true
|
|
175
|
+
return allowed.includes(modelId)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Returns `true` when the (provider, model) pair satisfies BOTH the provider
|
|
180
|
+
* allowlist and the per-provider model allowlist. Convenience helper for
|
|
181
|
+
* settings PUT validators.
|
|
182
|
+
*/
|
|
183
|
+
export function isProviderModelAllowed(
|
|
184
|
+
env: EnvLookup,
|
|
185
|
+
providerId: string,
|
|
186
|
+
modelId: string,
|
|
187
|
+
): boolean {
|
|
188
|
+
return isProviderAllowed(env, providerId) && isModelAllowedForProvider(env, providerId, modelId)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Public version of {@link envModelsVarName} for docs/UI hints.
|
|
193
|
+
*/
|
|
194
|
+
export function modelAllowlistEnvVarName(providerId: string): string {
|
|
195
|
+
return envModelsVarName(providerId)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Public version of {@link envProvidersVarName}.
|
|
200
|
+
*/
|
|
201
|
+
export function providerAllowlistEnvVarName(): string {
|
|
202
|
+
return envProvidersVarName()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function agentOverrideProviderAllowlistEnvVarName(agentId: string): string {
|
|
206
|
+
return agentOverrideProvidersVarName(agentId)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function agentOverrideModelAllowlistEnvVarName(
|
|
210
|
+
agentId: string,
|
|
211
|
+
providerId: string,
|
|
212
|
+
): string {
|
|
213
|
+
return agentOverrideModelsVarName(agentId, providerId)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Tenant-scoped allowlist snapshot (Phase 1780-6). Persisted by the
|
|
218
|
+
* `ai_tenant_model_allowlists` table and edited from the AI assistant
|
|
219
|
+
* settings page. The runtime intersects this with the env allowlist before
|
|
220
|
+
* picking which provider/model is permitted.
|
|
221
|
+
*
|
|
222
|
+
* `allowedProviders === null` means "inherit env" (no tenant-level provider
|
|
223
|
+
* restriction). For `allowedModelsByProvider`, a missing key means "inherit
|
|
224
|
+
* env" for that provider; an empty array means "no models permitted for this
|
|
225
|
+
* provider" — the runtime will refuse all picks for that provider.
|
|
226
|
+
*/
|
|
227
|
+
export interface TenantAllowlistSnapshot {
|
|
228
|
+
allowedProviders: string[] | null
|
|
229
|
+
allowedModelsByProvider: Record<string, string[]>
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function hasAllowlistSnapshotRestrictions(snapshot: TenantAllowlistSnapshot | null): boolean {
|
|
233
|
+
return Boolean(
|
|
234
|
+
snapshot &&
|
|
235
|
+
(snapshot.allowedProviders !== null ||
|
|
236
|
+
Object.keys(snapshot.allowedModelsByProvider ?? {}).length > 0),
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function readAgentRuntimeOverrideAllowlist(
|
|
241
|
+
env: EnvLookup,
|
|
242
|
+
agentId: string,
|
|
243
|
+
knownProviderIds: string[],
|
|
244
|
+
): TenantAllowlistSnapshot | null {
|
|
245
|
+
const providers = parseList(env[agentOverrideProvidersVarName(agentId)])
|
|
246
|
+
const modelsByProvider: Record<string, string[]> = {}
|
|
247
|
+
for (const providerId of knownProviderIds) {
|
|
248
|
+
const list = parseList(env[agentOverrideModelsVarName(agentId, providerId)])
|
|
249
|
+
if (list !== null) modelsByProvider[providerId] = list
|
|
250
|
+
}
|
|
251
|
+
const snapshot = {
|
|
252
|
+
allowedProviders: providers,
|
|
253
|
+
allowedModelsByProvider: modelsByProvider,
|
|
254
|
+
}
|
|
255
|
+
return hasAllowlistSnapshotRestrictions(snapshot) ? snapshot : null
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Effective allowlist after intersecting env with tenant. Both axes are
|
|
260
|
+
* `null` when neither side imposes a restriction — semantically equivalent to
|
|
261
|
+
* `readAllowlistConfig` with no tenant snapshot.
|
|
262
|
+
*/
|
|
263
|
+
export interface EffectiveAllowlist {
|
|
264
|
+
providers: string[] | null
|
|
265
|
+
modelsByProvider: Record<string, string[]>
|
|
266
|
+
hasRestrictions: boolean
|
|
267
|
+
/**
|
|
268
|
+
* `true` when the tenant snapshot contributes any narrowing on top of the
|
|
269
|
+
* env allowlist. Useful for telling the UI "this tenant has its own picks"
|
|
270
|
+
* vs "we are showing env-only restrictions".
|
|
271
|
+
*/
|
|
272
|
+
tenantOverridesActive: boolean
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function intersectIdLists(
|
|
276
|
+
outer: string[] | null,
|
|
277
|
+
inner: string[] | null,
|
|
278
|
+
caseInsensitive: boolean,
|
|
279
|
+
): string[] | null {
|
|
280
|
+
if (outer === null && inner === null) return null
|
|
281
|
+
if (outer === null) return inner
|
|
282
|
+
if (inner === null) return outer
|
|
283
|
+
const outerSet = new Set(
|
|
284
|
+
caseInsensitive ? outer.map((id) => normalizeProviderId(id)) : outer,
|
|
285
|
+
)
|
|
286
|
+
const result: string[] = []
|
|
287
|
+
for (const id of inner) {
|
|
288
|
+
const needle = caseInsensitive ? normalizeProviderId(id) : id
|
|
289
|
+
if (outerSet.has(needle)) result.push(id)
|
|
290
|
+
}
|
|
291
|
+
return result
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Intersects the env-driven allowlist with an optional tenant snapshot. The
|
|
296
|
+
* tenant allowlist may NEVER widen the env allowlist; values outside the env
|
|
297
|
+
* are silently dropped. Returns the effective shape the settings UI and
|
|
298
|
+
* model-factory should clip against.
|
|
299
|
+
*/
|
|
300
|
+
export function intersectAllowlists(
|
|
301
|
+
env: EnvLookup,
|
|
302
|
+
knownProviderIds: string[],
|
|
303
|
+
tenant: TenantAllowlistSnapshot | null,
|
|
304
|
+
): EffectiveAllowlist {
|
|
305
|
+
const envProviders = canonicalizeProviderList(readAllowedProviders(env), knownProviderIds)
|
|
306
|
+
const envModelsByProvider: Record<string, string[]> = {}
|
|
307
|
+
for (const providerId of knownProviderIds) {
|
|
308
|
+
const list = readAllowedModels(env, providerId)
|
|
309
|
+
if (list !== null) envModelsByProvider[providerId] = list
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const tenantProviders = canonicalizeProviderList(tenant?.allowedProviders ?? null, knownProviderIds)
|
|
313
|
+
const tenantModelsByProvider = tenant?.allowedModelsByProvider ?? {}
|
|
314
|
+
|
|
315
|
+
const providers = intersectIdLists(envProviders, tenantProviders, true)
|
|
316
|
+
|
|
317
|
+
const modelsByProvider: Record<string, string[]> = { ...envModelsByProvider }
|
|
318
|
+
for (const rawProviderId of Object.keys(tenantModelsByProvider)) {
|
|
319
|
+
const providerId = canonicalProviderId(rawProviderId, knownProviderIds) ?? normalizeProviderId(rawProviderId)
|
|
320
|
+
const tenantList = tenantModelsByProvider[rawProviderId] ?? []
|
|
321
|
+
const envList = modelsByProvider[providerId] ?? null
|
|
322
|
+
const intersection = intersectIdLists(envList, tenantList, false)
|
|
323
|
+
if (intersection !== null) {
|
|
324
|
+
modelsByProvider[providerId] = intersection
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const tenantOverridesActive =
|
|
329
|
+
tenantProviders !== null || Object.keys(tenantModelsByProvider).length > 0
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
providers,
|
|
333
|
+
modelsByProvider,
|
|
334
|
+
hasRestrictions:
|
|
335
|
+
providers !== null || Object.keys(modelsByProvider).length > 0,
|
|
336
|
+
tenantOverridesActive,
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function intersectEffectiveAllowlistWithSnapshot(
|
|
341
|
+
outer: EffectiveAllowlist,
|
|
342
|
+
knownProviderIds: string[],
|
|
343
|
+
inner: TenantAllowlistSnapshot | null,
|
|
344
|
+
): EffectiveAllowlist {
|
|
345
|
+
if (!hasAllowlistSnapshotRestrictions(inner)) return outer
|
|
346
|
+
|
|
347
|
+
const innerProviders = canonicalizeProviderList(inner?.allowedProviders ?? null, knownProviderIds)
|
|
348
|
+
const providers = intersectIdLists(outer.providers, innerProviders, true)
|
|
349
|
+
|
|
350
|
+
const modelsByProvider: Record<string, string[]> = { ...outer.modelsByProvider }
|
|
351
|
+
for (const rawProviderId of Object.keys(inner?.allowedModelsByProvider ?? {})) {
|
|
352
|
+
const providerId = canonicalProviderId(rawProviderId, knownProviderIds) ?? normalizeProviderId(rawProviderId)
|
|
353
|
+
const innerList = inner?.allowedModelsByProvider[rawProviderId] ?? []
|
|
354
|
+
const outerList = modelsByProvider[providerId] ?? null
|
|
355
|
+
const intersection = intersectIdLists(outerList, innerList, false)
|
|
356
|
+
if (intersection !== null) {
|
|
357
|
+
modelsByProvider[providerId] = intersection
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
providers,
|
|
363
|
+
modelsByProvider,
|
|
364
|
+
hasRestrictions: true,
|
|
365
|
+
tenantOverridesActive: outer.tenantOverridesActive || hasAllowlistSnapshotRestrictions(inner),
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Effective-allowlist version of `isProviderAllowed`.
|
|
371
|
+
*/
|
|
372
|
+
export function isProviderAllowedInEffective(
|
|
373
|
+
effective: EffectiveAllowlist,
|
|
374
|
+
providerId: string,
|
|
375
|
+
): boolean {
|
|
376
|
+
if (effective.providers === null) return true
|
|
377
|
+
const needle = normalizeProviderId(providerId)
|
|
378
|
+
return effective.providers.some((id) => normalizeProviderId(id) === needle)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Effective-allowlist version of `isModelAllowedForProvider`.
|
|
383
|
+
*/
|
|
384
|
+
export function isModelAllowedForProviderInEffective(
|
|
385
|
+
effective: EffectiveAllowlist,
|
|
386
|
+
providerId: string,
|
|
387
|
+
modelId: string,
|
|
388
|
+
): boolean {
|
|
389
|
+
const list = effective.modelsByProvider[providerId] ?? effective.modelsByProvider[normalizeProviderId(providerId)]
|
|
390
|
+
if (list === undefined) return true
|
|
391
|
+
return list.includes(modelId)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Returns `true` when the (provider, model) pair satisfies the effective
|
|
396
|
+
* allowlist (both env and tenant constraints).
|
|
397
|
+
*/
|
|
398
|
+
export function isProviderModelAllowedInEffective(
|
|
399
|
+
effective: EffectiveAllowlist,
|
|
400
|
+
providerId: string,
|
|
401
|
+
modelId: string,
|
|
402
|
+
): boolean {
|
|
403
|
+
return (
|
|
404
|
+
isProviderAllowedInEffective(effective, providerId) &&
|
|
405
|
+
isModelAllowedForProviderInEffective(effective, providerId, modelId)
|
|
406
|
+
)
|
|
407
|
+
}
|