@open-mercato/ai-assistant 0.6.1-develop.3246.1.dbef9d7392 → 0.6.1-develop.3256.1.fe3dec2464
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +82 -18
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js +370 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +194 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/route.js +4 -0
- package/dist/modules/ai_assistant/api/ai/agents/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +169 -5
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/route/route.js +38 -19
- package/dist/modules/ai_assistant/api/route/route.js.map +3 -3
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js +195 -0
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/settings/route.js +537 -22
- package/dist/modules/ai_assistant/api/settings/route.js.map +3 -3
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +701 -147
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +338 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.js +25 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js +1 -1
- package/dist/modules/ai_assistant/backend/config/ai-assistant/legacy/page.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +75 -26
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.js +25 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js +503 -168
- package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities/AiTenantModelAllowlist.js +5 -0
- package/dist/modules/ai_assistant/data/entities/AiTenantModelAllowlist.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities.js +123 -1
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +157 -0
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +7 -0
- package/dist/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.js +77 -0
- package/dist/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.js.map +7 -0
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js +1 -1
- package/dist/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +90 -1
- package/dist/modules/ai_assistant/i18n/en.json +90 -1
- package/dist/modules/ai_assistant/i18n/es.json +90 -1
- package/dist/modules/ai_assistant/i18n/pl.json +90 -1
- package/dist/modules/ai_assistant/lib/agent-registry.js +17 -1
- package/dist/modules/ai_assistant/lib/agent-registry.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-runtime.js +133 -36
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
- package/dist/modules/ai_assistant/lib/baseurl-allowlist.js +29 -0
- package/dist/modules/ai_assistant/lib/baseurl-allowlist.js.map +7 -0
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js +4 -1
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +2 -2
- package/dist/modules/ai_assistant/lib/llm-adapters/google.js +4 -1
- package/dist/modules/ai_assistant/lib/llm-adapters/google.js.map +2 -2
- package/dist/modules/ai_assistant/lib/model-allowlist.js +211 -0
- package/dist/modules/ai_assistant/lib/model-allowlist.js.map +7 -0
- package/dist/modules/ai_assistant/lib/model-factory.js +203 -31
- package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +32 -1
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +2 -2
- package/dist/modules/ai_assistant/migrations/Migration20260508140000.js +18 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508140000.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512090000.js +16 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512090000.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512130000.js +15 -0
- package/dist/modules/ai_assistant/migrations/Migration20260512130000.js.map +7 -0
- package/generated/entities/ai_agent_runtime_override/index.ts +13 -0
- package/generated/entities/ai_tenant_model_allowlist/index.ts +9 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +26 -0
- package/jest.config.cjs +2 -0
- package/package.json +4 -4
- package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts +477 -0
- package/src/modules/ai_assistant/__tests__/settings-page-logic.test.ts +116 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/__tests__/route.test.ts +240 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +251 -0
- package/src/modules/ai_assistant/api/ai/agents/route.ts +4 -0
- package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +273 -0
- package/src/modules/ai_assistant/api/ai/chat/route.ts +211 -2
- package/src/modules/ai_assistant/api/route/route.ts +49 -25
- package/src/modules/ai_assistant/api/settings/__tests__/route.test.ts +408 -0
- package/src/modules/ai_assistant/api/settings/allowlist/route.ts +221 -0
- package/src/modules/ai_assistant/api/settings/route.ts +721 -27
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +858 -177
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +458 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.meta.ts +23 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/page.tsx +12 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/legacy/page.tsx +1 -1
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +89 -12
- package/src/modules/ai_assistant/backend/config/ai-assistant/settings/page.meta.ts +23 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/settings/page.tsx +18 -0
- package/src/modules/ai_assistant/components/AiAssistantSettingsPageClient.tsx +617 -209
- package/src/modules/ai_assistant/data/entities/AiAgentRuntimeOverride.ts +7 -0
- package/src/modules/ai_assistant/data/entities/AiTenantModelAllowlist.ts +2 -0
- package/src/modules/ai_assistant/data/entities.ts +164 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +227 -0
- package/src/modules/ai_assistant/data/repositories/AiTenantModelAllowlistRepository.ts +132 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentRuntimeOverrideRepository.test.ts +337 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiTenantModelAllowlistRepository.test.ts +181 -0
- package/src/modules/ai_assistant/frontend/components/AiAssistantSettingsPageClient.tsx +1 -1
- package/src/modules/ai_assistant/i18n/de.json +90 -1
- package/src/modules/ai_assistant/i18n/en.json +90 -1
- package/src/modules/ai_assistant/i18n/es.json +90 -1
- package/src/modules/ai_assistant/i18n/pl.json +90 -1
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +396 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +60 -6
- package/src/modules/ai_assistant/lib/__tests__/ai-api-operation-runner.test.ts +4 -2
- package/src/modules/ai_assistant/lib/__tests__/baseurl-allowlist.test.ts +75 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-anthropic.test.ts +18 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-google.test.ts +18 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +150 -4
- package/src/modules/ai_assistant/lib/__tests__/model-allowlist.test.ts +290 -0
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +634 -0
- package/src/modules/ai_assistant/lib/agent-registry.ts +20 -1
- package/src/modules/ai_assistant/lib/agent-runtime.ts +220 -44
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +48 -0
- package/src/modules/ai_assistant/lib/baseurl-allowlist.ts +64 -0
- package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +11 -1
- package/src/modules/ai_assistant/lib/llm-adapters/google.ts +4 -1
- package/src/modules/ai_assistant/lib/model-allowlist.ts +407 -0
- package/src/modules/ai_assistant/lib/model-factory.ts +486 -58
- package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +44 -0
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +704 -235
- package/src/modules/ai_assistant/migrations/Migration20260508140000.ts +18 -0
- package/src/modules/ai_assistant/migrations/Migration20260512090000.ts +16 -0
- package/src/modules/ai_assistant/migrations/Migration20260512130000.ts +13 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
|
|
1
2
|
import type { AiAgentDefinition, AiAgentExtension, AiAgentSuggestion } from './ai-agent-definition'
|
|
2
3
|
import {
|
|
3
4
|
applyAgentOverrideMap,
|
|
@@ -104,6 +105,24 @@ function applySuggestionPatch(
|
|
|
104
105
|
return out
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
function validateAndNormalizeAgent(candidate: AiAgentDefinition): AiAgentDefinition {
|
|
109
|
+
const rawProvider = candidate.defaultProvider
|
|
110
|
+
if (typeof rawProvider !== 'string' || rawProvider.trim().length === 0) {
|
|
111
|
+
return candidate
|
|
112
|
+
}
|
|
113
|
+
const providerHint = rawProvider.trim()
|
|
114
|
+
const registered = llmProviderRegistry.get(providerHint)
|
|
115
|
+
if (!registered) {
|
|
116
|
+
console.warn(
|
|
117
|
+
`[AI Agents] Agent "${candidate.id}" declares defaultProvider "${providerHint}" which is not registered in llmProviderRegistry. ` +
|
|
118
|
+
`The agent will be registered with defaultProvider: undefined so the resolution chain still works. ` +
|
|
119
|
+
`Built-in provider ids: anthropic, google, openai, deepinfra, groq, together, fireworks, azure, litellm, ollama.`,
|
|
120
|
+
)
|
|
121
|
+
return { ...candidate, defaultProvider: undefined }
|
|
122
|
+
}
|
|
123
|
+
return candidate
|
|
124
|
+
}
|
|
125
|
+
|
|
107
126
|
function populateFromAgents(agents: unknown[]): void {
|
|
108
127
|
for (const candidate of agents) {
|
|
109
128
|
if (!isAiAgentDefinition(candidate)) {
|
|
@@ -116,7 +135,7 @@ function populateFromAgents(agents: unknown[]): void {
|
|
|
116
135
|
`[AI Agents] Duplicate agent id "${candidate.id}" — already registered by module "${existing.moduleId}", conflicts with module "${candidate.moduleId}". Export \`aiAgentOverrides\` from your module's \`ai-agents.ts\` (or set it on the modules.ts entry) to replace an agent across modules.`
|
|
117
136
|
)
|
|
118
137
|
}
|
|
119
|
-
agentsById.set(candidate.id, candidate)
|
|
138
|
+
agentsById.set(candidate.id, validateAndNormalizeAgent(candidate))
|
|
120
139
|
}
|
|
121
140
|
}
|
|
122
141
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createContainer } from 'awilix'
|
|
1
2
|
import type { AwilixContainer } from 'awilix'
|
|
2
3
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
4
|
import type { LanguageModel, UIMessage } from 'ai'
|
|
@@ -9,8 +10,7 @@ import {
|
|
|
9
10
|
streamText,
|
|
10
11
|
} from 'ai'
|
|
11
12
|
import type { ZodTypeAny } from 'zod'
|
|
12
|
-
import {
|
|
13
|
-
import { resolveAiProviderIdFromEnv } from '@open-mercato/shared/lib/ai/opencode-provider'
|
|
13
|
+
import { createModelFactory } from './model-factory'
|
|
14
14
|
import type {
|
|
15
15
|
AiAgentDefinition,
|
|
16
16
|
AiAgentPageContextInput,
|
|
@@ -30,6 +30,9 @@ import {
|
|
|
30
30
|
} from './attachment-parts'
|
|
31
31
|
import { AiAgentPromptOverrideRepository } from '../data/repositories/AiAgentPromptOverrideRepository'
|
|
32
32
|
import { AiAgentMutationPolicyOverrideRepository } from '../data/repositories/AiAgentMutationPolicyOverrideRepository'
|
|
33
|
+
import { AiAgentRuntimeOverrideRepository } from '../data/repositories/AiAgentRuntimeOverrideRepository'
|
|
34
|
+
import { AiTenantModelAllowlistRepository } from '../data/repositories/AiTenantModelAllowlistRepository'
|
|
35
|
+
import type { TenantAllowlistSnapshot } from './model-allowlist'
|
|
33
36
|
import { composeSystemPromptWithOverride } from './prompt-override-merge'
|
|
34
37
|
import { isKnownMutationPolicy } from './agent-policy'
|
|
35
38
|
import type { AiAgentMutationPolicy } from './ai-agent-definition'
|
|
@@ -65,6 +68,36 @@ export interface RunAiAgentTextInput {
|
|
|
65
68
|
* `agent.defaultModel` without inventing a new indirection layer.
|
|
66
69
|
*/
|
|
67
70
|
modelOverride?: string
|
|
71
|
+
/**
|
|
72
|
+
* Optional request-time provider override. When non-empty, wins for the
|
|
73
|
+
* provider axis at the same priority as `modelOverride` for the model axis.
|
|
74
|
+
* A value that does not match any registered provider id is silently ignored.
|
|
75
|
+
*
|
|
76
|
+
* Phase 1 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
77
|
+
*/
|
|
78
|
+
providerOverride?: string
|
|
79
|
+
/**
|
|
80
|
+
* Optional per-call base URL override. Wins over every other source in the
|
|
81
|
+
* baseURL resolution chain. Intended for programmatic callers only — the
|
|
82
|
+
* HTTP query-param baseUrl and the AI_RUNTIME_BASEURL_ALLOWLIST arrive in
|
|
83
|
+
* Phase 4a and MUST NOT be exposed here.
|
|
84
|
+
*
|
|
85
|
+
* Phase 2 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
86
|
+
*/
|
|
87
|
+
baseUrlOverride?: string
|
|
88
|
+
/**
|
|
89
|
+
* Per-request HTTP dispatcher override (query params `?provider=`, `?model=`,
|
|
90
|
+
* `?baseUrl=`). Validated by the dispatcher route before being forwarded
|
|
91
|
+
* here. Wins over tenantOverride and all lower-priority sources when
|
|
92
|
+
* `agent.allowRuntimeModelOverride !== false`.
|
|
93
|
+
*
|
|
94
|
+
* Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
95
|
+
*/
|
|
96
|
+
requestOverride?: {
|
|
97
|
+
providerId?: string | null
|
|
98
|
+
modelId?: string | null
|
|
99
|
+
baseURL?: string | null
|
|
100
|
+
}
|
|
68
101
|
/**
|
|
69
102
|
* Optional DI container used by `resolvePageContext` callbacks. When omitted
|
|
70
103
|
* and the agent declares a `resolvePageContext`, hydration is skipped with a
|
|
@@ -90,38 +123,61 @@ interface ResolvedAgentModel {
|
|
|
90
123
|
function resolveAgentModel(
|
|
91
124
|
agent: AiAgentDefinition,
|
|
92
125
|
modelOverride: string | undefined,
|
|
126
|
+
providerOverride: string | undefined,
|
|
127
|
+
container: AwilixContainer | undefined,
|
|
128
|
+
baseUrlOverride?: string,
|
|
129
|
+
tenantOverride?: { providerId?: string | null; modelId?: string | null; baseURL?: string | null } | null,
|
|
130
|
+
requestOverride?: { providerId?: string | null; modelId?: string | null; baseURL?: string | null } | null,
|
|
131
|
+
tenantAllowlist?: TenantAllowlistSnapshot | null,
|
|
93
132
|
): ResolvedAgentModel {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
133
|
+
const effectiveContainer = container ?? createContainer()
|
|
134
|
+
const allowRuntimeModelOverride = agent.allowRuntimeModelOverride !== false
|
|
135
|
+
const resolution = createModelFactory(effectiveContainer).resolveModel({
|
|
136
|
+
moduleId: agent.moduleId,
|
|
137
|
+
agentDefaultModel: agent.defaultModel,
|
|
138
|
+
agentDefaultProvider: agent.defaultProvider,
|
|
139
|
+
agentDefaultBaseUrl: agent.defaultBaseUrl,
|
|
140
|
+
callerOverride: modelOverride,
|
|
141
|
+
providerOverride,
|
|
142
|
+
baseUrlOverride,
|
|
143
|
+
allowRuntimeModelOverride,
|
|
144
|
+
tenantOverride: tenantOverride ?? undefined,
|
|
145
|
+
requestOverride: requestOverride ?? undefined,
|
|
146
|
+
tenantAllowlist: tenantAllowlist ?? null,
|
|
147
|
+
})
|
|
148
|
+
return {
|
|
149
|
+
model: resolution.model as LanguageModel,
|
|
150
|
+
modelId: resolution.modelId,
|
|
151
|
+
providerId: resolution.providerId,
|
|
110
152
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function resolveTenantAllowlistSnapshot(
|
|
156
|
+
container: AwilixContainer | undefined,
|
|
157
|
+
tenantId: string | null,
|
|
158
|
+
organizationId: string | null,
|
|
159
|
+
): Promise<TenantAllowlistSnapshot | null> {
|
|
160
|
+
if (!tenantId || !container) return null
|
|
161
|
+
let em: EntityManager | null = null
|
|
162
|
+
try {
|
|
163
|
+
em = container.resolve<EntityManager>('em')
|
|
164
|
+
} catch {
|
|
165
|
+
em = null
|
|
166
|
+
}
|
|
167
|
+
if (!em) return null
|
|
168
|
+
try {
|
|
169
|
+
const repo = new AiTenantModelAllowlistRepository(em)
|
|
170
|
+
return await repo.getSnapshot({
|
|
171
|
+
tenantId,
|
|
172
|
+
organizationId: organizationId ?? null,
|
|
173
|
+
})
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.warn(
|
|
176
|
+
'[AI Agents] Tenant allowlist lookup failed; falling back to env-only enforcement.',
|
|
177
|
+
error,
|
|
115
178
|
)
|
|
179
|
+
return null
|
|
116
180
|
}
|
|
117
|
-
const globalEnvModel = ((env.OM_AI_MODEL ?? env.OPENCODE_MODEL) ?? '').trim()
|
|
118
|
-
const modelId =
|
|
119
|
-
(modelOverride && modelOverride.trim().length > 0 ? modelOverride : undefined) ??
|
|
120
|
-
(globalEnvModel.length > 0 ? globalEnvModel : undefined) ??
|
|
121
|
-
agent.defaultModel ??
|
|
122
|
-
provider.defaultModel
|
|
123
|
-
const model = provider.createModel({ modelId, apiKey }) as LanguageModel
|
|
124
|
-
return { model, modelId, providerId: provider.id }
|
|
125
181
|
}
|
|
126
182
|
|
|
127
183
|
/**
|
|
@@ -264,6 +320,53 @@ async function resolveMutationPolicyOverride(
|
|
|
264
320
|
}
|
|
265
321
|
}
|
|
266
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Looks up the per-tenant AI runtime override (provider / model / baseURL) for
|
|
325
|
+
* the given agent (Phase 4a of spec
|
|
326
|
+
* `2026-04-27-ai-agents-provider-model-baseurl-overrides`).
|
|
327
|
+
*
|
|
328
|
+
* Mirrors the fail-open contract of {@link resolveMutationPolicyOverride}: any
|
|
329
|
+
* error — missing container, missing `em`, repository throw, missing migration
|
|
330
|
+
* — is logged at `warn` level and returns null so the model factory falls
|
|
331
|
+
* through to lower-priority sources. A chat turn MUST never fail on override
|
|
332
|
+
* lookup.
|
|
333
|
+
*/
|
|
334
|
+
async function resolveRuntimeModelOverride(
|
|
335
|
+
agentId: string,
|
|
336
|
+
container: AwilixContainer | undefined,
|
|
337
|
+
tenantId: string | null,
|
|
338
|
+
organizationId: string | null,
|
|
339
|
+
): Promise<{ providerId?: string | null; modelId?: string | null; baseURL?: string | null } | null> {
|
|
340
|
+
if (!tenantId || !container) return null
|
|
341
|
+
let em: EntityManager | null = null
|
|
342
|
+
try {
|
|
343
|
+
em = container.resolve<EntityManager>('em')
|
|
344
|
+
} catch {
|
|
345
|
+
em = null
|
|
346
|
+
}
|
|
347
|
+
if (!em) return null
|
|
348
|
+
try {
|
|
349
|
+
const repo = new AiAgentRuntimeOverrideRepository(em)
|
|
350
|
+
const row = await repo.getDefault({
|
|
351
|
+
tenantId,
|
|
352
|
+
organizationId: organizationId ?? null,
|
|
353
|
+
agentId,
|
|
354
|
+
})
|
|
355
|
+
if (!row) return null
|
|
356
|
+
return {
|
|
357
|
+
providerId: row.providerId ?? null,
|
|
358
|
+
modelId: row.modelId ?? null,
|
|
359
|
+
baseURL: row.baseUrl ?? null,
|
|
360
|
+
}
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.warn(
|
|
363
|
+
`[AI Agents] Runtime model override lookup failed for agent "${agentId}"; falling back to lower-priority sources.`,
|
|
364
|
+
error,
|
|
365
|
+
)
|
|
366
|
+
return null
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
267
370
|
/**
|
|
268
371
|
* Normalizes simple `{ role, content }` chat messages into the AI SDK
|
|
269
372
|
* `UIMessage` shape that `convertToModelMessages` requires. When the
|
|
@@ -481,12 +584,25 @@ function appendRuntimeMutationPolicy(
|
|
|
481
584
|
* invariant #7 still holds.
|
|
482
585
|
*/
|
|
483
586
|
export async function runAiAgentText(input: RunAiAgentTextInput): Promise<Response> {
|
|
484
|
-
const mutationPolicyOverride = await
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
587
|
+
const [mutationPolicyOverride, tenantRuntimeOverride, tenantAllowlistSnapshot] = await Promise.all([
|
|
588
|
+
resolveMutationPolicyOverride(
|
|
589
|
+
input.agentId,
|
|
590
|
+
input.container,
|
|
591
|
+
input.authContext.tenantId,
|
|
592
|
+
input.authContext.organizationId,
|
|
593
|
+
),
|
|
594
|
+
resolveRuntimeModelOverride(
|
|
595
|
+
input.agentId,
|
|
596
|
+
input.container,
|
|
597
|
+
input.authContext.tenantId,
|
|
598
|
+
input.authContext.organizationId,
|
|
599
|
+
),
|
|
600
|
+
resolveTenantAllowlistSnapshot(
|
|
601
|
+
input.container,
|
|
602
|
+
input.authContext.tenantId,
|
|
603
|
+
input.authContext.organizationId,
|
|
604
|
+
),
|
|
605
|
+
])
|
|
490
606
|
const { agent, tools } = await resolveAiAgentTools({
|
|
491
607
|
agentId: input.agentId,
|
|
492
608
|
authContext: input.authContext,
|
|
@@ -517,7 +633,16 @@ export async function runAiAgentText(input: RunAiAgentTextInput): Promise<Respon
|
|
|
517
633
|
mutationPolicyOverride,
|
|
518
634
|
)
|
|
519
635
|
|
|
520
|
-
const { model } = resolveAgentModel(
|
|
636
|
+
const { model } = resolveAgentModel(
|
|
637
|
+
agent,
|
|
638
|
+
input.modelOverride,
|
|
639
|
+
input.providerOverride,
|
|
640
|
+
input.container,
|
|
641
|
+
input.baseUrlOverride,
|
|
642
|
+
tenantRuntimeOverride,
|
|
643
|
+
input.requestOverride,
|
|
644
|
+
tenantAllowlistSnapshot,
|
|
645
|
+
)
|
|
521
646
|
const normalizedMessages = ensureUiMessageShape(input.messages)
|
|
522
647
|
const hydratedMessages = attachAttachmentsToMessages(normalizedMessages, resolvedAttachments)
|
|
523
648
|
const modelMessages = await convertToModelMessages(hydratedMessages)
|
|
@@ -580,6 +705,35 @@ export interface RunAiAgentObjectInput<TSchema = ZodTypeAny> {
|
|
|
580
705
|
*/
|
|
581
706
|
authContext: AiChatRequestContext
|
|
582
707
|
modelOverride?: string
|
|
708
|
+
/**
|
|
709
|
+
* Optional request-time provider override. When non-empty, wins for the
|
|
710
|
+
* provider axis at the same priority as `modelOverride` for the model axis.
|
|
711
|
+
*
|
|
712
|
+
* Phase 1 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
713
|
+
*/
|
|
714
|
+
providerOverride?: string
|
|
715
|
+
/**
|
|
716
|
+
* Optional per-call base URL override. Wins over every other source in the
|
|
717
|
+
* baseURL resolution chain. Intended for programmatic callers only — the
|
|
718
|
+
* HTTP query-param baseUrl and the AI_RUNTIME_BASEURL_ALLOWLIST arrive in
|
|
719
|
+
* Phase 4a and MUST NOT be exposed here.
|
|
720
|
+
*
|
|
721
|
+
* Phase 2 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
722
|
+
*/
|
|
723
|
+
baseUrlOverride?: string
|
|
724
|
+
/**
|
|
725
|
+
* Per-request HTTP dispatcher override (query params `?provider=`, `?model=`,
|
|
726
|
+
* `?baseUrl=`). Validated by the dispatcher route before being forwarded
|
|
727
|
+
* here. Wins over tenantOverride and all lower-priority sources when
|
|
728
|
+
* `agent.allowRuntimeModelOverride !== false`.
|
|
729
|
+
*
|
|
730
|
+
* Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
731
|
+
*/
|
|
732
|
+
requestOverride?: {
|
|
733
|
+
providerId?: string | null
|
|
734
|
+
modelId?: string | null
|
|
735
|
+
baseURL?: string | null
|
|
736
|
+
}
|
|
583
737
|
output?: RunAiAgentObjectOutputOverride<TSchema>
|
|
584
738
|
debug?: boolean
|
|
585
739
|
container?: AwilixContainer
|
|
@@ -662,12 +816,25 @@ function resolveStructuredOutput<TSchema>(
|
|
|
662
816
|
export async function runAiAgentObject<TSchema = unknown>(
|
|
663
817
|
input: RunAiAgentObjectInput<TSchema>,
|
|
664
818
|
): Promise<RunAiAgentObjectResult<TSchema>> {
|
|
665
|
-
const mutationPolicyOverride = await
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
819
|
+
const [mutationPolicyOverride, tenantRuntimeOverride, tenantAllowlistSnapshot] = await Promise.all([
|
|
820
|
+
resolveMutationPolicyOverride(
|
|
821
|
+
input.agentId,
|
|
822
|
+
input.container,
|
|
823
|
+
input.authContext.tenantId,
|
|
824
|
+
input.authContext.organizationId,
|
|
825
|
+
),
|
|
826
|
+
resolveRuntimeModelOverride(
|
|
827
|
+
input.agentId,
|
|
828
|
+
input.container,
|
|
829
|
+
input.authContext.tenantId,
|
|
830
|
+
input.authContext.organizationId,
|
|
831
|
+
),
|
|
832
|
+
resolveTenantAllowlistSnapshot(
|
|
833
|
+
input.container,
|
|
834
|
+
input.authContext.tenantId,
|
|
835
|
+
input.authContext.organizationId,
|
|
836
|
+
),
|
|
837
|
+
])
|
|
671
838
|
const { agent, tools } = await resolveAiAgentTools({
|
|
672
839
|
agentId: input.agentId,
|
|
673
840
|
authContext: input.authContext,
|
|
@@ -700,7 +867,16 @@ export async function runAiAgentObject<TSchema = unknown>(
|
|
|
700
867
|
mutationPolicyOverride,
|
|
701
868
|
)
|
|
702
869
|
|
|
703
|
-
const { model } = resolveAgentModel(
|
|
870
|
+
const { model } = resolveAgentModel(
|
|
871
|
+
agent,
|
|
872
|
+
input.modelOverride,
|
|
873
|
+
input.providerOverride,
|
|
874
|
+
input.container,
|
|
875
|
+
input.baseUrlOverride,
|
|
876
|
+
tenantRuntimeOverride,
|
|
877
|
+
input.requestOverride,
|
|
878
|
+
tenantAllowlistSnapshot,
|
|
879
|
+
)
|
|
704
880
|
const normalizedMessages = ensureUiMessageShape(normalizeObjectMessages(input.input))
|
|
705
881
|
const hydratedMessages = attachAttachmentsToMessages(
|
|
706
882
|
normalizedMessages,
|
|
@@ -46,7 +46,55 @@ export interface AiAgentDefinition {
|
|
|
46
46
|
allowedTools: string[]
|
|
47
47
|
suggestions?: AiAgentSuggestion[]
|
|
48
48
|
executionMode?: AiAgentExecutionMode
|
|
49
|
+
/**
|
|
50
|
+
* Optional provider id this agent prefers (e.g. `'openai'`, `'anthropic'`).
|
|
51
|
+
* Must match a registered `LlmProvider.id`. When the named provider is
|
|
52
|
+
* registered but unconfigured at runtime the factory falls through
|
|
53
|
+
* transparently to the next configured provider.
|
|
54
|
+
*
|
|
55
|
+
* Phase 1 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
56
|
+
*/
|
|
57
|
+
defaultProvider?: string
|
|
58
|
+
/**
|
|
59
|
+
* Optional model id fed through `createModelFactory` for this agent.
|
|
60
|
+
* Accepts either a plain model id (`claude-haiku-4-5-20251001`) or a
|
|
61
|
+
* slash-qualified `<provider>/<model>` shorthand (e.g. `openai/gpt-5-mini`).
|
|
62
|
+
* When the slash form is used the prefix must match a registered provider id;
|
|
63
|
+
* the registry-membership guard prevents mis-splitting model ids that already
|
|
64
|
+
* contain slashes (DeepInfra: `meta-llama/Llama-3.3-70B-Instruct-Turbo`).
|
|
65
|
+
*
|
|
66
|
+
* A higher-priority provider source still wins over the slash hint, but a
|
|
67
|
+
* lower-priority one cannot overwrite a slash-qualified model (cross-axis
|
|
68
|
+
* tie-break rule from spec §Phase-1).
|
|
69
|
+
*
|
|
70
|
+
* Phase 0 and Phase 1 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
71
|
+
*/
|
|
49
72
|
defaultModel?: string
|
|
73
|
+
/**
|
|
74
|
+
* Optional base URL this agent prefers for its chosen provider.
|
|
75
|
+
* Sits between the `<MODULE>_AI_BASE_URL` env (step 2 of the public 5-step
|
|
76
|
+
* baseURL hierarchy) and the preset env override (`baseURLEnvKeys`, step 4).
|
|
77
|
+
* Only honoured by adapters that support baseURL (Anthropic Messages-
|
|
78
|
+
* protocol relays, all OpenAI-compatible adapters, Google via
|
|
79
|
+
* @ai-sdk/google ≥3.0). See `packages/ai-assistant/AGENTS.md` →
|
|
80
|
+
* "baseURL override hierarchy" for the full numbered chain.
|
|
81
|
+
*
|
|
82
|
+
* Phase 2 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
83
|
+
*/
|
|
84
|
+
defaultBaseUrl?: string
|
|
85
|
+
/**
|
|
86
|
+
* When false, per-request HTTP overrides (query params `provider`, `model`,
|
|
87
|
+
* `baseUrl`) and the per-tenant settings override stored in
|
|
88
|
+
* `ai_agent_runtime_overrides` are both suppressed. Steps 1 and 3 of the
|
|
89
|
+
* model-factory resolution chain are skipped for this agent.
|
|
90
|
+
*
|
|
91
|
+
* Default is `true` (permissive). Agents that pin a specific model for
|
|
92
|
+
* correctness reasons (e.g. a structured-output agent whose JSON-mode schema
|
|
93
|
+
* only works with one provider) should set this to `false`.
|
|
94
|
+
*
|
|
95
|
+
* Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
96
|
+
*/
|
|
97
|
+
allowRuntimeModelOverride?: boolean
|
|
50
98
|
acceptedMediaTypes?: AiAgentAcceptedMediaType[]
|
|
51
99
|
requiredFeatures?: string[]
|
|
52
100
|
uiParts?: string[]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `AI_RUNTIME_BASEURL_ALLOWLIST` guard helper.
|
|
3
|
+
*
|
|
4
|
+
* Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`
|
|
5
|
+
* (R6 mitigation). An operator who wants to expose the `?baseUrl=` query
|
|
6
|
+
* parameter over HTTP MUST set this env var to a comma-separated list of
|
|
7
|
+
* allowed host patterns. When the env var is absent or empty, ANY non-empty
|
|
8
|
+
* `baseUrl` value received by the dispatcher is rejected with a typed 400
|
|
9
|
+
* error — ensuring that a misconfigured deployment cannot be tricked into
|
|
10
|
+
* forwarding API calls to an attacker-controlled endpoint.
|
|
11
|
+
*
|
|
12
|
+
* Host patterns use a simple glob: a leading `*` matches any subdomain.
|
|
13
|
+
* Examples:
|
|
14
|
+
* `openrouter.ai` — exact host
|
|
15
|
+
* `*.openrouter.ai` — any subdomain of openrouter.ai
|
|
16
|
+
* `openrouter.ai,api.myproxy.io` — two hosts (comma-separated, no spaces)
|
|
17
|
+
*
|
|
18
|
+
* The comparison is case-insensitive and ignores the URL path/query —
|
|
19
|
+
* only the `hostname` portion of the submitted URL is checked.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns the parsed allowlist from `AI_RUNTIME_BASEURL_ALLOWLIST`.
|
|
24
|
+
* An empty array means the allowlist is empty (reject all).
|
|
25
|
+
*/
|
|
26
|
+
export function readBaseurlAllowlist(env: Record<string, string | undefined> = process.env): string[] {
|
|
27
|
+
const raw = env.AI_RUNTIME_BASEURL_ALLOWLIST ?? ''
|
|
28
|
+
return raw
|
|
29
|
+
.split(',')
|
|
30
|
+
.map((entry) => entry.trim().toLowerCase())
|
|
31
|
+
.filter((entry) => entry.length > 0)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns true when `baseUrl` is permitted under the current allowlist.
|
|
36
|
+
*
|
|
37
|
+
* Rules:
|
|
38
|
+
* - An empty `baseUrl` is always allowed (no override requested).
|
|
39
|
+
* - When the allowlist is empty, any non-empty `baseUrl` is rejected.
|
|
40
|
+
* - A pattern `*.example.com` matches `foo.example.com` but NOT `example.com`.
|
|
41
|
+
* - A pattern `example.com` matches only `example.com` (exact, case-insensitive).
|
|
42
|
+
*/
|
|
43
|
+
export function isBaseurlAllowlisted(baseUrl: string, allowlist: string[]): boolean {
|
|
44
|
+
const trimmed = baseUrl.trim()
|
|
45
|
+
if (!trimmed) return true
|
|
46
|
+
if (allowlist.length === 0) return false
|
|
47
|
+
|
|
48
|
+
let parsedHostname: string
|
|
49
|
+
try {
|
|
50
|
+
parsedHostname = new URL(trimmed).hostname.toLowerCase()
|
|
51
|
+
} catch {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const pattern of allowlist) {
|
|
56
|
+
if (pattern.startsWith('*.')) {
|
|
57
|
+
const suffix = pattern.slice(2)
|
|
58
|
+
if (parsedHostname.endsWith(`.${suffix}`)) return true
|
|
59
|
+
} else if (pattern === parsedHostname) {
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
@@ -43,6 +43,13 @@ const DEFAULT_MODELS: readonly LlmModelInfo[] = [
|
|
|
43
43
|
/**
|
|
44
44
|
* Factory returning a fresh `AnthropicAdapter` instance. The adapter is
|
|
45
45
|
* stateless — caller is free to reuse the returned object.
|
|
46
|
+
*
|
|
47
|
+
* `createModel` accepts an optional `baseURL` from {@link LlmCreateModelOptions}
|
|
48
|
+
* and forwards it to `createAnthropic({ apiKey, baseURL })`.
|
|
49
|
+
*
|
|
50
|
+
* baseURL only works for Anthropic Messages-protocol relays (Cloudflare AI
|
|
51
|
+
* Gateway in Anthropic mode, Helicone proxy). For OpenAI-format gateways,
|
|
52
|
+
* use the OpenAI / OpenRouter presets.
|
|
46
53
|
*/
|
|
47
54
|
export function createAnthropicAdapter(): LlmProvider {
|
|
48
55
|
const envKeys = ['ANTHROPIC_API_KEY', 'OPENCODE_ANTHROPIC_API_KEY'] as const
|
|
@@ -84,7 +91,10 @@ export function createAnthropicAdapter(): LlmProvider {
|
|
|
84
91
|
},
|
|
85
92
|
|
|
86
93
|
createModel(options: LlmCreateModelOptions): unknown {
|
|
87
|
-
const anthropic = createAnthropic({
|
|
94
|
+
const anthropic = createAnthropic({
|
|
95
|
+
apiKey: options.apiKey,
|
|
96
|
+
...(options.baseURL ? { baseURL: options.baseURL } : {}),
|
|
97
|
+
})
|
|
88
98
|
return anthropic(options.modelId)
|
|
89
99
|
},
|
|
90
100
|
}
|
|
@@ -73,7 +73,10 @@ export function createGoogleAdapter(): LlmProvider {
|
|
|
73
73
|
},
|
|
74
74
|
|
|
75
75
|
createModel(options: LlmCreateModelOptions): unknown {
|
|
76
|
-
const google = createGoogleGenerativeAI({
|
|
76
|
+
const google = createGoogleGenerativeAI({
|
|
77
|
+
apiKey: options.apiKey,
|
|
78
|
+
...(options.baseURL ? { baseURL: options.baseURL } : {}),
|
|
79
|
+
})
|
|
77
80
|
return google(options.modelId)
|
|
78
81
|
},
|
|
79
82
|
}
|