@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
|
@@ -98,6 +98,46 @@ describe('OpenAIAdapter (OpenAI-compatible provider factory)', () => {
|
|
|
98
98
|
const provider = createOpenAICompatibleProvider(azurePreset!)
|
|
99
99
|
expect(provider.id).toBe('azure')
|
|
100
100
|
})
|
|
101
|
+
|
|
102
|
+
it('preset env baseURL override beats preset default for openai', () => {
|
|
103
|
+
const openaiPreset = OPENAI_COMPATIBLE_PRESETS.find((p) => p.id === 'openai')!
|
|
104
|
+
const provider = createOpenAICompatibleProvider(openaiPreset)
|
|
105
|
+
// Without override the preset baseURL is undefined (uses AI SDK default).
|
|
106
|
+
const modelDefault = provider.createModel({ apiKey: 'key', modelId: 'gpt-4o-mini' })
|
|
107
|
+
expect(modelDefault).toBeDefined()
|
|
108
|
+
// With a per-call baseURL override the adapter must not crash.
|
|
109
|
+
const modelOverride = provider.createModel({
|
|
110
|
+
apiKey: 'key',
|
|
111
|
+
modelId: 'gpt-4o-mini',
|
|
112
|
+
baseURL: 'https://custom-proxy.example.com/v1',
|
|
113
|
+
})
|
|
114
|
+
expect(modelOverride).toBeDefined()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('openai preset declares OPENAI_BASE_URL in baseURLEnvKeys', () => {
|
|
118
|
+
const openaiPreset = OPENAI_COMPATIBLE_PRESETS.find((p) => p.id === 'openai')!
|
|
119
|
+
expect(openaiPreset.baseURLEnvKeys).toContain('OPENAI_BASE_URL')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('deepinfra preset declares DEEPINFRA_BASE_URL in baseURLEnvKeys', () => {
|
|
123
|
+
const preset = OPENAI_COMPATIBLE_PRESETS.find((p) => p.id === 'deepinfra')!
|
|
124
|
+
expect(preset.baseURLEnvKeys).toContain('DEEPINFRA_BASE_URL')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('groq preset declares GROQ_BASE_URL in baseURLEnvKeys', () => {
|
|
128
|
+
const preset = OPENAI_COMPATIBLE_PRESETS.find((p) => p.id === 'groq')!
|
|
129
|
+
expect(preset.baseURLEnvKeys).toContain('GROQ_BASE_URL')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('together preset declares TOGETHER_BASE_URL in baseURLEnvKeys', () => {
|
|
133
|
+
const preset = OPENAI_COMPATIBLE_PRESETS.find((p) => p.id === 'together')!
|
|
134
|
+
expect(preset.baseURLEnvKeys).toContain('TOGETHER_BASE_URL')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('fireworks preset declares FIREWORKS_BASE_URL in baseURLEnvKeys', () => {
|
|
138
|
+
const preset = OPENAI_COMPATIBLE_PRESETS.find((p) => p.id === 'fireworks')!
|
|
139
|
+
expect(preset.baseURLEnvKeys).toContain('FIREWORKS_BASE_URL')
|
|
140
|
+
})
|
|
101
141
|
})
|
|
102
142
|
|
|
103
143
|
describe('OpenAI preset OPENCODE_* fallback env keys', () => {
|
|
@@ -124,8 +164,8 @@ describe('OpenAI preset OPENCODE_* fallback env keys', () => {
|
|
|
124
164
|
})
|
|
125
165
|
|
|
126
166
|
describe('OPENAI_COMPATIBLE_PRESETS built-in catalog', () => {
|
|
127
|
-
it('ships at least
|
|
128
|
-
expect(OPENAI_COMPATIBLE_PRESETS.length).toBeGreaterThanOrEqual(
|
|
167
|
+
it('ships at least 10 built-in presets including openai, openrouter, and lm-studio', () => {
|
|
168
|
+
expect(OPENAI_COMPATIBLE_PRESETS.length).toBeGreaterThanOrEqual(10)
|
|
129
169
|
const ids = OPENAI_COMPATIBLE_PRESETS.map((p) => p.id)
|
|
130
170
|
expect(ids).toContain('openai')
|
|
131
171
|
expect(ids).toContain('deepinfra')
|
|
@@ -135,18 +175,33 @@ describe('OPENAI_COMPATIBLE_PRESETS built-in catalog', () => {
|
|
|
135
175
|
expect(ids).toContain('azure')
|
|
136
176
|
expect(ids).toContain('litellm')
|
|
137
177
|
expect(ids).toContain('ollama')
|
|
178
|
+
expect(ids).toContain('openrouter')
|
|
179
|
+
expect(ids).toContain('lm-studio')
|
|
138
180
|
})
|
|
139
181
|
|
|
140
|
-
it('every preset has at least one
|
|
182
|
+
it('every preset has at least one env key', () => {
|
|
141
183
|
for (const preset of OPENAI_COMPATIBLE_PRESETS) {
|
|
142
184
|
expect(preset.envKeys.length).toBeGreaterThan(0)
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('non-auto-detect presets have at least one model and a non-empty defaultModel', () => {
|
|
189
|
+
for (const preset of OPENAI_COMPATIBLE_PRESETS) {
|
|
190
|
+
if (preset.id === 'lm-studio') {
|
|
191
|
+
// LM Studio deliberately uses empty defaultModel — auto-detects the
|
|
192
|
+
// loaded model from the request body's model field.
|
|
193
|
+
expect(preset.defaultModel).toBe('')
|
|
194
|
+
expect(preset.defaultModels.length).toBe(0)
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
143
197
|
expect(preset.defaultModels.length).toBeGreaterThan(0)
|
|
144
198
|
expect(preset.defaultModel.length).toBeGreaterThan(0)
|
|
145
199
|
}
|
|
146
200
|
})
|
|
147
201
|
|
|
148
|
-
it('every preset defaultModel exists in its defaultModels array', () => {
|
|
202
|
+
it('every non-auto-detect preset defaultModel exists in its defaultModels array', () => {
|
|
149
203
|
for (const preset of OPENAI_COMPATIBLE_PRESETS) {
|
|
204
|
+
if (preset.id === 'lm-studio') continue
|
|
150
205
|
const ids = preset.defaultModels.map((m) => m.id)
|
|
151
206
|
expect(ids).toContain(preset.defaultModel)
|
|
152
207
|
}
|
|
@@ -158,3 +213,94 @@ describe('OPENAI_COMPATIBLE_PRESETS built-in catalog', () => {
|
|
|
158
213
|
expect(unique.size).toBe(ids.length)
|
|
159
214
|
})
|
|
160
215
|
})
|
|
216
|
+
|
|
217
|
+
describe('OpenRouter preset', () => {
|
|
218
|
+
const openrouterPreset = OPENAI_COMPATIBLE_PRESETS.find((p) => p.id === 'openrouter')!
|
|
219
|
+
|
|
220
|
+
it('has expected shape', () => {
|
|
221
|
+
expect(openrouterPreset).toBeDefined()
|
|
222
|
+
expect(openrouterPreset.baseURL).toBe('https://openrouter.ai/api/v1')
|
|
223
|
+
expect(openrouterPreset.baseURLEnvKeys).toContain('OPENROUTER_BASE_URL')
|
|
224
|
+
expect(openrouterPreset.envKeys).toContain('OPENROUTER_API_KEY')
|
|
225
|
+
expect(openrouterPreset.defaultModel).toBe('meta-llama/llama-3.3-70b-instruct')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('isConfigured returns true only when OPENROUTER_API_KEY is set', () => {
|
|
229
|
+
const provider = createOpenAICompatibleProvider(openrouterPreset)
|
|
230
|
+
expect(provider.isConfigured({ OPENROUTER_API_KEY: 'or-key' })).toBe(true)
|
|
231
|
+
expect(provider.isConfigured({ OPENAI_API_KEY: 'unrelated' })).toBe(false)
|
|
232
|
+
expect(provider.isConfigured({})).toBe(false)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('getConfiguredEnvKey returns OPENROUTER_API_KEY', () => {
|
|
236
|
+
const provider = createOpenAICompatibleProvider(openrouterPreset)
|
|
237
|
+
expect(provider.getConfiguredEnvKey({ OPENROUTER_API_KEY: 'key' })).toBe('OPENROUTER_API_KEY')
|
|
238
|
+
expect(provider.getConfiguredEnvKey({})).toBe('OPENROUTER_API_KEY')
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('createModel with default baseURL does not throw', () => {
|
|
242
|
+
const provider = createOpenAICompatibleProvider(openrouterPreset)
|
|
243
|
+
const model = provider.createModel({ apiKey: 'or-key', modelId: openrouterPreset.defaultModel })
|
|
244
|
+
expect(model).toBeDefined()
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('createModel with per-call baseURL override does not throw', () => {
|
|
248
|
+
const provider = createOpenAICompatibleProvider(openrouterPreset)
|
|
249
|
+
const model = provider.createModel({
|
|
250
|
+
apiKey: 'or-key',
|
|
251
|
+
modelId: openrouterPreset.defaultModel,
|
|
252
|
+
baseURL: 'https://my-openrouter-proxy.example.com/v1',
|
|
253
|
+
})
|
|
254
|
+
expect(model).toBeDefined()
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('OPENROUTER_BASE_URL env override beats preset default', () => {
|
|
258
|
+
const provider = createOpenAICompatibleProvider(openrouterPreset)
|
|
259
|
+
const model = provider.createModel({
|
|
260
|
+
apiKey: 'or-key',
|
|
261
|
+
modelId: openrouterPreset.defaultModel,
|
|
262
|
+
baseURL: 'https://overridden.example.com/v1',
|
|
263
|
+
})
|
|
264
|
+
expect(model).toBeDefined()
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
describe('LM Studio preset', () => {
|
|
269
|
+
const lmStudioPreset = OPENAI_COMPATIBLE_PRESETS.find((p) => p.id === 'lm-studio')!
|
|
270
|
+
|
|
271
|
+
it('has expected shape with empty defaultModel', () => {
|
|
272
|
+
expect(lmStudioPreset).toBeDefined()
|
|
273
|
+
expect(lmStudioPreset.baseURL).toBe('http://localhost:1234/v1')
|
|
274
|
+
expect(lmStudioPreset.baseURLEnvKeys).toContain('LM_STUDIO_BASE_URL')
|
|
275
|
+
expect(lmStudioPreset.envKeys).toContain('LM_STUDIO_API_KEY')
|
|
276
|
+
expect(lmStudioPreset.defaultModel).toBe('')
|
|
277
|
+
expect(lmStudioPreset.defaultModels).toHaveLength(0)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('isConfigured returns true when LM_STUDIO_API_KEY is set', () => {
|
|
281
|
+
const provider = createOpenAICompatibleProvider(lmStudioPreset)
|
|
282
|
+
expect(provider.isConfigured({ LM_STUDIO_API_KEY: 'lm-key' })).toBe(true)
|
|
283
|
+
expect(provider.isConfigured({})).toBe(false)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('getConfiguredEnvKey returns LM_STUDIO_API_KEY', () => {
|
|
287
|
+
const provider = createOpenAICompatibleProvider(lmStudioPreset)
|
|
288
|
+
expect(provider.getConfiguredEnvKey({})).toBe('LM_STUDIO_API_KEY')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('createModel with empty modelId does not throw', () => {
|
|
292
|
+
const provider = createOpenAICompatibleProvider(lmStudioPreset)
|
|
293
|
+
const model = provider.createModel({ apiKey: 'lm-key', modelId: '' })
|
|
294
|
+
expect(model).toBeDefined()
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('createModel with per-call baseURL override does not throw', () => {
|
|
298
|
+
const provider = createOpenAICompatibleProvider(lmStudioPreset)
|
|
299
|
+
const model = provider.createModel({
|
|
300
|
+
apiKey: 'lm-key',
|
|
301
|
+
modelId: '',
|
|
302
|
+
baseURL: 'http://192.168.1.100:1234/v1',
|
|
303
|
+
})
|
|
304
|
+
expect(model).toBeDefined()
|
|
305
|
+
})
|
|
306
|
+
})
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import {
|
|
2
|
+
agentOverrideModelAllowlistEnvVarName,
|
|
3
|
+
agentOverrideProviderAllowlistEnvVarName,
|
|
4
|
+
intersectEffectiveAllowlistWithSnapshot,
|
|
5
|
+
intersectAllowlists,
|
|
6
|
+
isModelAllowedForProvider,
|
|
7
|
+
isModelAllowedForProviderInEffective,
|
|
8
|
+
isProviderAllowed,
|
|
9
|
+
isProviderAllowedInEffective,
|
|
10
|
+
isProviderModelAllowed,
|
|
11
|
+
isProviderModelAllowedInEffective,
|
|
12
|
+
modelAllowlistEnvVarName,
|
|
13
|
+
providerAllowlistEnvVarName,
|
|
14
|
+
readAgentRuntimeOverrideAllowlist,
|
|
15
|
+
readAllowedModels,
|
|
16
|
+
readAllowedProviders,
|
|
17
|
+
readAllowlistConfig,
|
|
18
|
+
} from '../model-allowlist'
|
|
19
|
+
|
|
20
|
+
describe('model-allowlist', () => {
|
|
21
|
+
describe('readAllowedProviders', () => {
|
|
22
|
+
it('returns null when OM_AI_AVAILABLE_PROVIDERS is unset', () => {
|
|
23
|
+
expect(readAllowedProviders({})).toBeNull()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('returns null when OM_AI_AVAILABLE_PROVIDERS is blank or whitespace-only', () => {
|
|
27
|
+
expect(readAllowedProviders({ OM_AI_AVAILABLE_PROVIDERS: '' })).toBeNull()
|
|
28
|
+
expect(readAllowedProviders({ OM_AI_AVAILABLE_PROVIDERS: ' ' })).toBeNull()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('parses a comma-separated list with whitespace tolerance', () => {
|
|
32
|
+
expect(readAllowedProviders({ OM_AI_AVAILABLE_PROVIDERS: 'openai, anthropic ,google' }))
|
|
33
|
+
.toEqual(['openai', 'anthropic', 'google'])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('drops empty entries', () => {
|
|
37
|
+
expect(readAllowedProviders({ OM_AI_AVAILABLE_PROVIDERS: 'openai,,anthropic, ,' }))
|
|
38
|
+
.toEqual(['openai', 'anthropic'])
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('readAllowedModels', () => {
|
|
43
|
+
it('returns null when no per-provider env is set', () => {
|
|
44
|
+
expect(readAllowedModels({}, 'openai')).toBeNull()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('reads from OM_AI_AVAILABLE_MODELS_<PROVIDER> uppercased', () => {
|
|
48
|
+
expect(
|
|
49
|
+
readAllowedModels({ OM_AI_AVAILABLE_MODELS_OPENAI: 'gpt-5-mini,gpt-5' }, 'openai'),
|
|
50
|
+
).toEqual(['gpt-5-mini', 'gpt-5'])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('handles compound provider ids (e.g. lm-studio)', () => {
|
|
54
|
+
expect(
|
|
55
|
+
readAllowedModels({ OM_AI_AVAILABLE_MODELS_LM_STUDIO: 'qwen-32b' }, 'lm-studio'),
|
|
56
|
+
).toEqual(['qwen-32b'])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('uses the same underscore env variable for underscore aliases', () => {
|
|
60
|
+
expect(
|
|
61
|
+
readAllowedModels({ OM_AI_AVAILABLE_MODELS_LM_STUDIO: 'qwen-32b' }, 'lm_studio'),
|
|
62
|
+
).toEqual(['qwen-32b'])
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('isProviderAllowed', () => {
|
|
67
|
+
it('returns true when no restriction is configured', () => {
|
|
68
|
+
expect(isProviderAllowed({}, 'openai')).toBe(true)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('is case-insensitive for the provider id', () => {
|
|
72
|
+
expect(isProviderAllowed({ OM_AI_AVAILABLE_PROVIDERS: 'OpenAI' }, 'openai')).toBe(true)
|
|
73
|
+
expect(isProviderAllowed({ OM_AI_AVAILABLE_PROVIDERS: 'openai' }, 'OPENAI')).toBe(true)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('treats hyphen and underscore provider spellings as aliases', () => {
|
|
77
|
+
expect(isProviderAllowed({ OM_AI_AVAILABLE_PROVIDERS: 'lm_studio' }, 'lm-studio'))
|
|
78
|
+
.toBe(true)
|
|
79
|
+
expect(isProviderAllowed({ OM_AI_AVAILABLE_PROVIDERS: 'lm-studio' }, 'lm_studio'))
|
|
80
|
+
.toBe(true)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('returns false when the provider is not in the list', () => {
|
|
84
|
+
expect(isProviderAllowed({ OM_AI_AVAILABLE_PROVIDERS: 'openai,anthropic' }, 'google'))
|
|
85
|
+
.toBe(false)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('isModelAllowedForProvider', () => {
|
|
90
|
+
it('returns true when no per-provider restriction is configured', () => {
|
|
91
|
+
expect(isModelAllowedForProvider({}, 'openai', 'gpt-5-mini')).toBe(true)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('returns true only for case-sensitive exact model id matches', () => {
|
|
95
|
+
const env = { OM_AI_AVAILABLE_MODELS_OPENAI: 'gpt-5-mini,gpt-5' }
|
|
96
|
+
expect(isModelAllowedForProvider(env, 'openai', 'gpt-5-mini')).toBe(true)
|
|
97
|
+
expect(isModelAllowedForProvider(env, 'openai', 'GPT-5-MINI')).toBe(false)
|
|
98
|
+
expect(isModelAllowedForProvider(env, 'openai', 'gpt-4o')).toBe(false)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('isProviderModelAllowed', () => {
|
|
103
|
+
it('requires both provider and model gates to pass', () => {
|
|
104
|
+
const env = {
|
|
105
|
+
OM_AI_AVAILABLE_PROVIDERS: 'openai',
|
|
106
|
+
OM_AI_AVAILABLE_MODELS_OPENAI: 'gpt-5-mini',
|
|
107
|
+
}
|
|
108
|
+
expect(isProviderModelAllowed(env, 'openai', 'gpt-5-mini')).toBe(true)
|
|
109
|
+
expect(isProviderModelAllowed(env, 'openai', 'gpt-4o')).toBe(false)
|
|
110
|
+
expect(isProviderModelAllowed(env, 'anthropic', 'claude-haiku-4-5')).toBe(false)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('readAllowlistConfig', () => {
|
|
115
|
+
it('returns no restrictions when env is empty', () => {
|
|
116
|
+
const snapshot = readAllowlistConfig({}, ['openai', 'anthropic'])
|
|
117
|
+
expect(snapshot).toEqual({
|
|
118
|
+
providers: null,
|
|
119
|
+
modelsByProvider: {},
|
|
120
|
+
hasRestrictions: false,
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('aggregates per-provider model lists for known providers', () => {
|
|
125
|
+
const env = {
|
|
126
|
+
OM_AI_AVAILABLE_PROVIDERS: 'openai,anthropic',
|
|
127
|
+
OM_AI_AVAILABLE_MODELS_OPENAI: 'gpt-5-mini',
|
|
128
|
+
}
|
|
129
|
+
const snapshot = readAllowlistConfig(env, ['openai', 'anthropic', 'google'])
|
|
130
|
+
expect(snapshot.providers).toEqual(['openai', 'anthropic'])
|
|
131
|
+
expect(snapshot.modelsByProvider).toEqual({ openai: ['gpt-5-mini'] })
|
|
132
|
+
expect(snapshot.hasRestrictions).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('public env-var-name helpers', () => {
|
|
137
|
+
it('exposes the canonical env var names for docs/UI hints', () => {
|
|
138
|
+
expect(providerAllowlistEnvVarName()).toBe('OM_AI_AVAILABLE_PROVIDERS')
|
|
139
|
+
expect(modelAllowlistEnvVarName('openai')).toBe('OM_AI_AVAILABLE_MODELS_OPENAI')
|
|
140
|
+
expect(modelAllowlistEnvVarName('lm-studio')).toBe('OM_AI_AVAILABLE_MODELS_LM_STUDIO')
|
|
141
|
+
expect(agentOverrideProviderAllowlistEnvVarName('catalog.catalog_assistant'))
|
|
142
|
+
.toBe('OM_AI_AGENT_CATALOG_CATALOG_ASSISTANT_AVAILABLE_PROVIDERS')
|
|
143
|
+
expect(agentOverrideModelAllowlistEnvVarName('catalog.catalog_assistant', 'lm-studio'))
|
|
144
|
+
.toBe('OM_AI_AGENT_CATALOG_CATALOG_ASSISTANT_AVAILABLE_MODELS_LM_STUDIO')
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('Phase 1780-6 — intersectAllowlists / effective allowlist', () => {
|
|
149
|
+
it('returns env-only when no tenant snapshot is supplied', () => {
|
|
150
|
+
const env = {
|
|
151
|
+
OM_AI_AVAILABLE_PROVIDERS: 'openai,anthropic',
|
|
152
|
+
OM_AI_AVAILABLE_MODELS_OPENAI: 'gpt-5-mini,gpt-5',
|
|
153
|
+
}
|
|
154
|
+
const effective = intersectAllowlists(env, ['openai', 'anthropic', 'google'], null)
|
|
155
|
+
expect(effective.providers).toEqual(['openai', 'anthropic'])
|
|
156
|
+
expect(effective.modelsByProvider).toEqual({ openai: ['gpt-5-mini', 'gpt-5'] })
|
|
157
|
+
expect(effective.hasRestrictions).toBe(true)
|
|
158
|
+
expect(effective.tenantOverridesActive).toBe(false)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('clips tenant providers to the env allowlist (tenant cannot widen env)', () => {
|
|
162
|
+
const env = { OM_AI_AVAILABLE_PROVIDERS: 'openai,anthropic' }
|
|
163
|
+
const tenant = {
|
|
164
|
+
allowedProviders: ['openai', 'google'],
|
|
165
|
+
allowedModelsByProvider: {},
|
|
166
|
+
}
|
|
167
|
+
const effective = intersectAllowlists(env, ['openai', 'anthropic', 'google'], tenant)
|
|
168
|
+
expect(effective.providers).toEqual(['openai'])
|
|
169
|
+
expect(effective.tenantOverridesActive).toBe(true)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('clips tenant models to the env per-provider allowlist (tenant cannot widen env)', () => {
|
|
173
|
+
const env = {
|
|
174
|
+
OM_AI_AVAILABLE_PROVIDERS: 'openai',
|
|
175
|
+
OM_AI_AVAILABLE_MODELS_OPENAI: 'gpt-5-mini,gpt-5',
|
|
176
|
+
}
|
|
177
|
+
const tenant = {
|
|
178
|
+
allowedProviders: null,
|
|
179
|
+
allowedModelsByProvider: { openai: ['gpt-5-mini', 'gpt-4o'] },
|
|
180
|
+
}
|
|
181
|
+
const effective = intersectAllowlists(env, ['openai'], tenant)
|
|
182
|
+
expect(effective.providers).toEqual(['openai'])
|
|
183
|
+
expect(effective.modelsByProvider.openai).toEqual(['gpt-5-mini'])
|
|
184
|
+
expect(effective.tenantOverridesActive).toBe(true)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('passes through tenant providers when env imposes no provider restriction', () => {
|
|
188
|
+
const env: Record<string, string | undefined> = {}
|
|
189
|
+
const tenant = {
|
|
190
|
+
allowedProviders: ['openai'],
|
|
191
|
+
allowedModelsByProvider: {},
|
|
192
|
+
}
|
|
193
|
+
const effective = intersectAllowlists(env, ['openai', 'anthropic'], tenant)
|
|
194
|
+
expect(effective.providers).toEqual(['openai'])
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('returns no restriction when both env and tenant are empty', () => {
|
|
198
|
+
const env: Record<string, string | undefined> = {}
|
|
199
|
+
const effective = intersectAllowlists(env, ['openai'], null)
|
|
200
|
+
expect(effective.providers).toBeNull()
|
|
201
|
+
expect(effective.modelsByProvider).toEqual({})
|
|
202
|
+
expect(effective.hasRestrictions).toBe(false)
|
|
203
|
+
expect(effective.tenantOverridesActive).toBe(false)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('isProviderAllowedInEffective is case-insensitive', () => {
|
|
207
|
+
const env = { OM_AI_AVAILABLE_PROVIDERS: 'OpenAI' }
|
|
208
|
+
const effective = intersectAllowlists(env, ['openai'], null)
|
|
209
|
+
expect(isProviderAllowedInEffective(effective, 'openai')).toBe(true)
|
|
210
|
+
expect(isProviderAllowedInEffective(effective, 'OPENAI')).toBe(true)
|
|
211
|
+
expect(isProviderAllowedInEffective(effective, 'anthropic')).toBe(false)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('isModelAllowedForProviderInEffective returns true when no per-provider list applies', () => {
|
|
215
|
+
const effective = intersectAllowlists({}, ['openai'], null)
|
|
216
|
+
expect(isModelAllowedForProviderInEffective(effective, 'openai', 'any-model')).toBe(true)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('isModelAllowedForProviderInEffective is case-sensitive on model id', () => {
|
|
220
|
+
const env = { OM_AI_AVAILABLE_MODELS_OPENAI: 'gpt-5-mini' }
|
|
221
|
+
const effective = intersectAllowlists(env, ['openai'], null)
|
|
222
|
+
expect(isModelAllowedForProviderInEffective(effective, 'openai', 'gpt-5-mini')).toBe(true)
|
|
223
|
+
expect(isModelAllowedForProviderInEffective(effective, 'openai', 'GPT-5-MINI')).toBe(false)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('isProviderModelAllowedInEffective enforces both provider and model allowlists', () => {
|
|
227
|
+
const env = {
|
|
228
|
+
OM_AI_AVAILABLE_PROVIDERS: 'openai',
|
|
229
|
+
OM_AI_AVAILABLE_MODELS_OPENAI: 'gpt-5-mini',
|
|
230
|
+
}
|
|
231
|
+
const effective = intersectAllowlists(env, ['openai'], null)
|
|
232
|
+
expect(isProviderModelAllowedInEffective(effective, 'openai', 'gpt-5-mini')).toBe(true)
|
|
233
|
+
expect(isProviderModelAllowedInEffective(effective, 'openai', 'gpt-4o')).toBe(false)
|
|
234
|
+
expect(isProviderModelAllowedInEffective(effective, 'anthropic', 'claude-haiku-4-5')).toBe(false)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('canonicalizes hyphen and underscore provider aliases for tenant intersections', () => {
|
|
238
|
+
const env = {
|
|
239
|
+
OM_AI_AVAILABLE_PROVIDERS: 'lm_studio',
|
|
240
|
+
OM_AI_AVAILABLE_MODELS_LM_STUDIO: 'qwen/qwen3.5-9b',
|
|
241
|
+
}
|
|
242
|
+
const tenant = {
|
|
243
|
+
allowedProviders: ['lm-studio'],
|
|
244
|
+
allowedModelsByProvider: {
|
|
245
|
+
lm_studio: ['qwen/qwen3.5-9b'],
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
const effective = intersectAllowlists(env, ['lm-studio'], tenant)
|
|
249
|
+
|
|
250
|
+
expect(effective.providers).toEqual(['lm-studio'])
|
|
251
|
+
expect(effective.modelsByProvider['lm-studio']).toEqual(['qwen/qwen3.5-9b'])
|
|
252
|
+
expect(isProviderModelAllowedInEffective(effective, 'lm_studio', 'qwen/qwen3.5-9b'))
|
|
253
|
+
.toBe(true)
|
|
254
|
+
expect(isProviderModelAllowedInEffective(effective, 'lm-studio', 'qwen/qwen3.5-9b'))
|
|
255
|
+
.toBe(true)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('intersects per-agent chat override env and settings with the effective allowlist', () => {
|
|
259
|
+
const env = {
|
|
260
|
+
OM_AI_AVAILABLE_PROVIDERS: 'openai,anthropic',
|
|
261
|
+
OM_AI_AVAILABLE_MODELS_OPENAI: 'gpt-5-mini,gpt-4o',
|
|
262
|
+
OM_AI_AGENT_CATALOG_CATALOG_ASSISTANT_AVAILABLE_PROVIDERS: 'openai',
|
|
263
|
+
OM_AI_AGENT_CATALOG_CATALOG_ASSISTANT_AVAILABLE_MODELS_OPENAI: 'gpt-5-mini',
|
|
264
|
+
}
|
|
265
|
+
const base = intersectAllowlists(env, ['openai', 'anthropic'], {
|
|
266
|
+
allowedProviders: ['openai', 'anthropic'],
|
|
267
|
+
allowedModelsByProvider: { openai: ['gpt-5-mini', 'gpt-4o'] },
|
|
268
|
+
})
|
|
269
|
+
const agentEnv = readAgentRuntimeOverrideAllowlist(
|
|
270
|
+
env,
|
|
271
|
+
'catalog.catalog_assistant',
|
|
272
|
+
['openai', 'anthropic'],
|
|
273
|
+
)
|
|
274
|
+
const effective = intersectEffectiveAllowlistWithSnapshot(
|
|
275
|
+
intersectEffectiveAllowlistWithSnapshot(base, ['openai', 'anthropic'], agentEnv),
|
|
276
|
+
['openai', 'anthropic'],
|
|
277
|
+
{
|
|
278
|
+
allowedProviders: ['openai', 'anthropic'],
|
|
279
|
+
allowedModelsByProvider: { openai: ['gpt-4o', 'gpt-5-mini'] },
|
|
280
|
+
},
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
expect(effective.providers).toEqual(['openai'])
|
|
284
|
+
expect(effective.modelsByProvider.openai).toEqual(['gpt-5-mini'])
|
|
285
|
+
expect(isProviderModelAllowedInEffective(effective, 'openai', 'gpt-5-mini')).toBe(true)
|
|
286
|
+
expect(isProviderModelAllowedInEffective(effective, 'openai', 'gpt-4o')).toBe(false)
|
|
287
|
+
expect(isProviderAllowedInEffective(effective, 'anthropic')).toBe(false)
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
})
|