@open-mercato/ai-assistant 0.6.1-develop.3188.1.499d530779 → 0.6.1-develop.3198.1.aa979623f0
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/AGENTS.md +28 -7
- package/dist/modules/ai_assistant/api/route/route.js +11 -4
- package/dist/modules/ai_assistant/api/route/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/settings/route.js +3 -3
- package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-runtime.js +11 -3
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
- package/dist/modules/ai_assistant/lib/model-factory.js +41 -4
- package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
- package/package.json +4 -4
- package/src/modules/ai_assistant/api/route/route.ts +15 -6
- package/src/modules/ai_assistant/api/settings/route.ts +6 -3
- package/src/modules/ai_assistant/lib/__tests__/model-factory.integration.test.ts +32 -10
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +343 -16
- package/src/modules/ai_assistant/lib/agent-runtime.ts +16 -2
- package/src/modules/ai_assistant/lib/model-factory.ts +161 -21
package/AGENTS.md
CHANGED
|
@@ -315,9 +315,19 @@ The unified AI runtime picks the first configured provider from `llmProviderRegi
|
|
|
315
315
|
|
|
316
316
|
At least one MUST be set or the runtime throws `AiModelFactoryError` with `code: 'no_provider_configured'` on first invocation. See `/framework/ai-assistant/overview` for the full matrix.
|
|
317
317
|
|
|
318
|
-
|
|
318
|
+
Process-wide defaults (Phase 0 of spec
|
|
319
|
+
`2026-04-27-ai-agents-provider-model-baseurl-overrides`):
|
|
319
320
|
|
|
320
|
-
|
|
321
|
+
| Variable | Purpose |
|
|
322
|
+
|----------|---------|
|
|
323
|
+
| `OM_AI_PROVIDER` | Optional. Names the registered provider id to prefer when multiple are configured. Falls through transparently when the named provider is registered-but-unconfigured. Built-in ids: `anthropic`, `google`, `openai`, `deepinfra`, `groq`, `together`, `fireworks`, `azure`, `litellm`, `ollama`. |
|
|
324
|
+
| `OM_AI_MODEL` | Optional. Process-wide model id used when neither caller override, `OM_AI_<MODULE>_MODEL`, nor `agentDefaultModel` applies. Slash-qualified ids (e.g. `openai/gpt-5-mini`) consume the provider axis at the same step — DeepInfra ids that already contain slashes (`meta-llama/Llama-3.3-70B-Instruct-Turbo`) stay intact via the registry-membership guard. |
|
|
325
|
+
|
|
326
|
+
Both are deliberately decoupled from the legacy `OPENCODE_PROVIDER` / `OPENCODE_MODEL` envs (which stay bound to the OpenCode Code Mode stack) — see "Coexistence with OpenCode Code Mode" below.
|
|
327
|
+
|
|
328
|
+
Per-module model overrides use `OM_AI_<MODULE>_MODEL` (uppercased from the agent's `moduleId`): for example, `OM_AI_CATALOG_MODEL=claude-opus-4-20250514`, `OM_AI_INBOX_OPS_MODEL=gpt-4o`.
|
|
329
|
+
|
|
330
|
+
All new callers MUST use `createModelFactory(container)` from `@open-mercato/ai-assistant/modules/ai_assistant/lib/model-factory` — never inline provider SDK calls (`createAnthropic`, `createOpenAI`, `createGoogleGenerativeAI`). The factory enforces the resolution order (caller override → `OM_AI_<MODULE>_MODEL` → `agentDefaultModel` → `OM_AI_MODEL` → provider default) and throws the documented `AiModelFactoryError` codes when misconfigured. See **Model Resolution** below.
|
|
321
331
|
|
|
322
332
|
## Architecture Constraints
|
|
323
333
|
|
|
@@ -502,11 +512,22 @@ modules — route them through the factory instead.
|
|
|
502
512
|
Resolution order (highest precedence first):
|
|
503
513
|
|
|
504
514
|
1. `callerOverride` (non-empty string) — typically `runAiAgentText({ modelOverride })`.
|
|
505
|
-
2.
|
|
506
|
-
e.g. `
|
|
515
|
+
2. `OM_AI_<MODULE>_MODEL` env variable (uppercased from `moduleId`) —
|
|
516
|
+
e.g. `OM_AI_INBOX_OPS_MODEL`, `OM_AI_CATALOG_MODEL`. Internal convention;
|
|
507
517
|
no need to enumerate each one in `.env.example`.
|
|
508
518
|
3. `agentDefaultModel` (typically `AiAgentDefinition.defaultModel`).
|
|
509
|
-
4.
|
|
519
|
+
4. `OM_AI_MODEL` env (Phase 0 of spec
|
|
520
|
+
`2026-04-27-ai-agents-provider-model-baseurl-overrides`). Plain id
|
|
521
|
+
uses the resolved provider; slash-qualified id (`openai/gpt-5-mini`)
|
|
522
|
+
consumes the provider axis at the same step.
|
|
523
|
+
5. The configured provider's own default (`llmProvider.defaultModel`).
|
|
524
|
+
|
|
525
|
+
The provider axis is resolved through `llmProviderRegistry.resolveFirstConfigured`. The walk's `order` argument is seeded from (in priority order):
|
|
526
|
+
|
|
527
|
+
1. Slash-prefix from `OM_AI_MODEL` (when present and matches a registered provider id).
|
|
528
|
+
2. `OM_AI_PROVIDER` env.
|
|
529
|
+
|
|
530
|
+
The named provider is preferred but the walk falls through transparently when it is registered-but-unconfigured.
|
|
510
531
|
|
|
511
532
|
The factory throws `AiModelFactoryError` with `code: 'no_provider_configured'`
|
|
512
533
|
when the registry has no configured provider and `code: 'api_key_missing'`
|
|
@@ -1445,7 +1466,7 @@ This section captures what existing deployments need to know when picking up the
|
|
|
1445
1466
|
| Variable | Default | Purpose |
|
|
1446
1467
|
|----------|---------|---------|
|
|
1447
1468
|
| `AI_PENDING_ACTION_TTL_SECONDS` | `900` (15 minutes) | Expiry window for pending mutation approvals. After this window the cleanup worker flips `status = 'pending'` rows whose `expires_at < now` to `expired` and emits `ai.action.expired`. |
|
|
1448
|
-
|
|
|
1469
|
+
| `OM_AI_<MODULE>_MODEL` | unset | Per-module model override, uppercased from the module id. Examples: `OM_AI_INBOX_OPS_MODEL`, `OM_AI_CATALOG_MODEL`. The legacy `<MODULE>_AI_MODEL` form (`INBOX_OPS_AI_MODEL`, `CATALOG_AI_MODEL`) is read as a backward-compatibility fallback. Internal convention — no need to enumerate each one in `.env.example`. Resolution order: caller override → this env var → `agentDefaultModel` → configured provider's default. |
|
|
1449
1470
|
|
|
1450
1471
|
### New database table
|
|
1451
1472
|
|
|
@@ -1508,7 +1529,7 @@ Tenants who want both keep OpenCode Code Mode for ad-hoc chat and Code-Mode-styl
|
|
|
1508
1529
|
|
|
1509
1530
|
For a live end-to-end walkthrough against a real LLM:
|
|
1510
1531
|
|
|
1511
|
-
1. Set `AI_PENDING_ACTION_TTL_SECONDS=900`, your chosen provider env vars (e.g., `ANTHROPIC_API_KEY`), and optional `
|
|
1532
|
+
1. Set `AI_PENDING_ACTION_TTL_SECONDS=900`, your chosen provider env vars (e.g., `ANTHROPIC_API_KEY`), and optional `OM_AI_CATALOG_MODEL`.
|
|
1512
1533
|
2. Run `yarn db:migrate` to pick up `Migration20260419134235_ai_assistant`.
|
|
1513
1534
|
3. Run `yarn mercato configs cache structural --all-tenants` to register the cleanup worker on existing tenants.
|
|
1514
1535
|
4. Open `/backend/catalog/catalog/products`, pick a handful of rows, open the `<AiChat>` sheet, and walk through each of the four named use cases (description drafting, attribute extraction, title variants, price adjustment suggestion).
|
|
@@ -4,7 +4,10 @@ import { z } from "zod";
|
|
|
4
4
|
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
5
5
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
6
6
|
import { llmProviderRegistry } from "@open-mercato/shared/lib/ai/llm-provider-registry";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
resolveAiProviderIdFromEnv,
|
|
9
|
+
resolveOpenCodeModel
|
|
10
|
+
} from "@open-mercato/shared/lib/ai/opencode-provider";
|
|
8
11
|
import {
|
|
9
12
|
resolveChatConfig,
|
|
10
13
|
isProviderConfigured
|
|
@@ -70,9 +73,13 @@ async function POST(req) {
|
|
|
70
73
|
const container = await createRequestContainer();
|
|
71
74
|
let config = await resolveChatConfig(container);
|
|
72
75
|
if (!config) {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
const operatorPreferred = resolveAiProviderIdFromEnv(process.env);
|
|
77
|
+
const baseOrder = ["openai", "anthropic", "google"];
|
|
78
|
+
const order = [
|
|
79
|
+
operatorPreferred,
|
|
80
|
+
...baseOrder.filter((id) => id !== operatorPreferred)
|
|
81
|
+
];
|
|
82
|
+
const picked = llmProviderRegistry.resolveFirstConfigured({ order });
|
|
76
83
|
if (!picked) {
|
|
77
84
|
return NextResponse.json(
|
|
78
85
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/ai_assistant/api/route/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { generateObject } from '../../lib/ai-sdk'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport {
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAsC;AAE/C,SAAS,sBAAsB;AAC/B,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AACpC,
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { generateObject } from '../../lib/ai-sdk'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport {\n resolveAiProviderIdFromEnv,\n resolveOpenCodeModel,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\nimport {\n resolveChatConfig,\n isProviderConfigured,\n type ChatProviderId,\n} from '../../lib/chat-config'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI query routing',\n methods: {\n POST: { summary: 'Route user query to appropriate AI handler' },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nconst RouteResultSchema = z.object({\n intent: z.enum(['tool', 'general_chat']),\n toolName: z.string().optional(),\n confidence: z.number().min(0).max(1),\n reasoning: z.string(),\n})\n\nfunction createRoutingModel(providerId: ChatProviderId, configuredModel?: string) {\n const provider = llmProviderRegistry.get(providerId)\n if (!provider) {\n throw new Error(`Unknown provider: ${providerId}`)\n }\n\n // resolveOpenCodeModel is still used for token parsing and provider-prefix\n // validation (`openai/gpt-5-mini` vs `anthropic/claude-\u2026`). It falls back\n // to the provider's defaultModel via the opencode-provider facade, which\n // is only populated for the three native providers \u2014 if the registry\n // returns a preset-based provider whose id is unknown to opencode-provider,\n // we short-circuit and use the provider's own defaultModel.\n let modelId: string\n let modelWithProvider: string\n try {\n const resolved = resolveOpenCodeModel(providerId as 'anthropic' | 'openai' | 'google', {\n overrideModel: configuredModel,\n })\n modelId = resolved.modelId\n modelWithProvider = resolved.modelWithProvider\n } catch {\n // Preset-based provider or unknown id \u2014 fall back to the provider's own\n // model list. The explicit override (if any) wins.\n const requested = (configuredModel ?? '').trim()\n modelId = requested.length > 0 ? requested : provider.defaultModel\n modelWithProvider = `${providerId}/${modelId}`\n }\n\n const apiKey = provider.resolveApiKey()\n if (!apiKey) {\n const envKey = provider.getConfiguredEnvKey()\n throw new Error(`${envKey} not configured for provider \"${providerId}\"`)\n }\n\n const model = provider.createModel({ modelId, apiKey }) as unknown as Parameters<\n typeof generateObject\n >[0]['model']\n return { model, modelWithProvider }\n}\n\nexport async function POST(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n\n if (!auth) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const body = await req.json()\n const { query, availableTools } = body as {\n query: string\n availableTools: Array<{ name: string; description: string }>\n }\n\n console.log('[AI Route] Routing query:', query)\n console.log('[AI Route] Available tools count:', availableTools?.length)\n\n if (!query || typeof query !== 'string') {\n return NextResponse.json({ error: 'query is required' }, { status: 400 })\n }\n\n if (!availableTools || !Array.isArray(availableTools)) {\n return NextResponse.json({ error: 'availableTools array is required' }, { status: 400 })\n }\n\n // Get user's configured provider\n const container = await createRequestContainer()\n let config = await resolveChatConfig(container)\n\n // Fallback to first configured provider from the LLM provider registry.\n // Honors the operator-selected provider via OM_AI_PROVIDER (with the\n // legacy OPENCODE_PROVIDER as the BC fallback) and only then walks the\n // native adapters before any OpenAI-compatible presets so a deployment\n // that just sets OPENAI_API_KEY still resolves to OpenAI by default.\n if (!config) {\n const operatorPreferred = resolveAiProviderIdFromEnv(process.env)\n const baseOrder: ChatProviderId[] = ['openai', 'anthropic', 'google']\n const order = [\n operatorPreferred,\n ...baseOrder.filter((id) => id !== operatorPreferred),\n ]\n const picked = llmProviderRegistry.resolveFirstConfigured({ order })\n if (!picked) {\n return NextResponse.json(\n {\n error:\n 'No AI provider configured. Please set an API key for one of the registered providers (Anthropic, OpenAI, Google, DeepInfra, Groq, \u2026).',\n },\n { status: 503 },\n )\n }\n config = { providerId: picked.id, model: '', updatedAt: '' }\n }\n\n console.log('[AI Route] Using provider:', config.providerId)\n\n // Verify the configured provider is still available\n if (!isProviderConfigured(config.providerId)) {\n return NextResponse.json(\n { error: `Configured provider ${config.providerId} is no longer available. Please update settings.` },\n { status: 503 }\n )\n }\n\n // Use fast model for the configured provider\n const { model, modelWithProvider } = createRoutingModel(config.providerId, config.model)\n\n const toolList = availableTools\n .map((t) => `- ${t.name}: ${t.description}`)\n .join('\\n')\n\n console.log('[AI Route] Calling generateObject with', modelWithProvider)\n\n const result = await generateObject({\n model,\n schema: RouteResultSchema,\n prompt: `You are a routing assistant. Given a user query, determine if they want to use a specific tool or have a general conversation.\n\nAvailable tools:\n${toolList}\n\nUser query: \"${query}\"\n\nRespond with:\n- intent: \"tool\" if user wants to perform an action with a specific tool, \"general_chat\" otherwise\n- toolName: the exact tool name if intent is \"tool\"\n- confidence: 0-1 how confident you are\n- reasoning: brief explanation`,\n })\n\n console.log('[AI Route] Result:', result.object)\n return NextResponse.json(result.object)\n } catch (error) {\n console.error('[AI Route] Error routing query:', error)\n return NextResponse.json(\n { error: 'Routing request failed' },\n { status: 500 }\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
OPEN_CODE_PROVIDERS,
|
|
6
6
|
getOpenCodeProviderConfiguredEnvKey,
|
|
7
7
|
isOpenCodeProviderConfigured,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
resolveAiProviderIdFromEnv,
|
|
9
|
+
resolveOpenCodeModel
|
|
10
10
|
} from "@open-mercato/shared/lib/ai/opencode-provider";
|
|
11
11
|
const openApi = {
|
|
12
12
|
tag: "AI Assistant",
|
|
@@ -24,7 +24,7 @@ async function GET(req) {
|
|
|
24
24
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
25
25
|
}
|
|
26
26
|
try {
|
|
27
|
-
const providerId =
|
|
27
|
+
const providerId = resolveAiProviderIdFromEnv(process.env);
|
|
28
28
|
const providerInfo = OPEN_CODE_PROVIDERS[providerId];
|
|
29
29
|
const apiKeyConfigured = isOpenCodeProviderConfigured(providerId);
|
|
30
30
|
const resolvedModel = resolveOpenCodeModel(providerId);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/ai_assistant/api/settings/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport {\n OPEN_CODE_PROVIDER_IDS,\n OPEN_CODE_PROVIDERS,\n getOpenCodeProviderConfiguredEnvKey,\n isOpenCodeProviderConfigured,\n
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAsC;AAE/C,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK,EAAE,SAAS,gCAAgC;AAAA,EAClD;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACnE;AAOA,eAAsB,IAAI,KAAkB;AAC1C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,MAAI;
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport {\n OPEN_CODE_PROVIDER_IDS,\n OPEN_CODE_PROVIDERS,\n getOpenCodeProviderConfiguredEnvKey,\n isOpenCodeProviderConfigured,\n resolveAiProviderIdFromEnv,\n resolveOpenCodeModel,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI assistant settings',\n methods: {\n GET: { summary: 'Get AI provider configuration' },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\n/**\n * GET /api/ai_assistant/settings\n *\n * Returns the current OpenCode provider configuration from environment variables.\n */\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n // Read provider config from environment. `OM_AI_PROVIDER` is the new\n // canonical variable; `OPENCODE_PROVIDER` is kept as a BC fallback by\n // `resolveAiProviderIdFromEnv`. Falls back to the unified default\n // (`openai`) when neither is set.\n const providerId = resolveAiProviderIdFromEnv(process.env)\n const providerInfo = OPEN_CODE_PROVIDERS[providerId]\n\n // Check if the provider's API key is configured (supports multiple fallback keys)\n const apiKeyConfigured = isOpenCodeProviderConfigured(providerId)\n\n // Get model (custom or default)\n const resolvedModel = resolveOpenCodeModel(providerId)\n\n // Show the env key that's configured, or the first one as instruction\n const displayEnvKey = getOpenCodeProviderConfiguredEnvKey(providerId)\n\n // Check if MCP_SERVER_API_KEY is configured (required for MCP authentication)\n const mcpKeyConfigured = !!process.env.MCP_SERVER_API_KEY?.trim()\n\n return NextResponse.json({\n provider: {\n id: providerId,\n name: providerInfo.name,\n model: resolvedModel.modelWithProvider,\n defaultModel: providerInfo.defaultModel,\n envKey: displayEnvKey,\n configured: apiKeyConfigured,\n },\n availableProviders: OPEN_CODE_PROVIDER_IDS.map((id) => {\n const info = OPEN_CODE_PROVIDERS[id]\n return {\n id,\n name: info.name,\n defaultModel: info.defaultModel,\n envKey: getOpenCodeProviderConfiguredEnvKey(id),\n configured: isOpenCodeProviderConfigured(id),\n }\n }),\n mcpKeyConfigured,\n })\n } catch (error) {\n console.error('[AI Settings] GET error:', error)\n return NextResponse.json({ error: 'Failed to fetch settings' }, { status: 500 })\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAsC;AAE/C,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK,EAAE,SAAS,gCAAgC;AAAA,EAClD;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACnE;AAOA,eAAsB,IAAI,KAAkB;AAC1C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,MAAI;AAKF,UAAM,aAAa,2BAA2B,QAAQ,GAAG;AACzD,UAAM,eAAe,oBAAoB,UAAU;AAGnD,UAAM,mBAAmB,6BAA6B,UAAU;AAGhE,UAAM,gBAAgB,qBAAqB,UAAU;AAGrD,UAAM,gBAAgB,oCAAoC,UAAU;AAGpE,UAAM,mBAAmB,CAAC,CAAC,QAAQ,IAAI,oBAAoB,KAAK;AAEhE,WAAO,aAAa,KAAK;AAAA,MACvB,UAAU;AAAA,QACR,IAAI;AAAA,QACJ,MAAM,aAAa;AAAA,QACnB,OAAO,cAAc;AAAA,QACrB,cAAc,aAAa;AAAA,QAC3B,QAAQ;AAAA,QACR,YAAY;AAAA,MACd;AAAA,MACA,oBAAoB,uBAAuB,IAAI,CAAC,OAAO;AACrD,cAAM,OAAO,oBAAoB,EAAE;AACnC,eAAO;AAAA,UACL;AAAA,UACA,MAAM,KAAK;AAAA,UACX,cAAc,KAAK;AAAA,UACnB,QAAQ,oCAAoC,EAAE;AAAA,UAC9C,YAAY,6BAA6B,EAAE;AAAA,QAC7C;AAAA,MACF,CAAC;AAAA,MACD;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,MAAM,4BAA4B,KAAK;AAC/C,WAAO,aAAa,KAAK,EAAE,OAAO,2BAA2B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACjF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
streamText
|
|
7
7
|
} from "ai";
|
|
8
8
|
import { llmProviderRegistry } from "@open-mercato/shared/lib/ai/llm-provider-registry";
|
|
9
|
+
import { resolveAiProviderIdFromEnv } from "@open-mercato/shared/lib/ai/opencode-provider";
|
|
9
10
|
import { resolveAiAgentTools, AgentPolicyError } from "./agent-tools.js";
|
|
10
11
|
import { resolveEffectiveMutationPolicy } from "./agent-policy.js";
|
|
11
12
|
import { toolRegistry } from "./tool-registry.js";
|
|
@@ -20,10 +21,16 @@ import { composeSystemPromptWithOverride } from "./prompt-override-merge.js";
|
|
|
20
21
|
import { isKnownMutationPolicy } from "./agent-policy.js";
|
|
21
22
|
import "./llm-bootstrap.js";
|
|
22
23
|
function resolveAgentModel(agent, modelOverride) {
|
|
23
|
-
const
|
|
24
|
+
const env = process.env;
|
|
25
|
+
const omProvider = (env.OM_AI_PROVIDER ?? "").trim();
|
|
26
|
+
const opencodeProvider = (env.OPENCODE_PROVIDER ?? "").trim();
|
|
27
|
+
const providerHint = omProvider.length > 0 || opencodeProvider.length > 0 ? [resolveAiProviderIdFromEnv(env)] : void 0;
|
|
28
|
+
const provider = llmProviderRegistry.resolveFirstConfigured(
|
|
29
|
+
providerHint ? { order: providerHint } : void 0
|
|
30
|
+
);
|
|
24
31
|
if (!provider) {
|
|
25
32
|
throw new Error(
|
|
26
|
-
"No LLM provider is configured. Set OPENCODE_PROVIDER plus a matching API key such as
|
|
33
|
+
"No LLM provider is configured. Set OM_AI_PROVIDER (or the legacy OPENCODE_PROVIDER) plus a matching API key such as OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY, then restart the app. See https://docs.openmercato.com/framework/ai-assistant/overview."
|
|
27
34
|
);
|
|
28
35
|
}
|
|
29
36
|
const apiKey = provider.resolveApiKey();
|
|
@@ -32,7 +39,8 @@ function resolveAgentModel(agent, modelOverride) {
|
|
|
32
39
|
`LLM provider "${provider.id}" is advertised as configured but resolveApiKey() returned empty.`
|
|
33
40
|
);
|
|
34
41
|
}
|
|
35
|
-
const
|
|
42
|
+
const globalEnvModel = (env.OM_AI_MODEL ?? env.OPENCODE_MODEL ?? "").trim();
|
|
43
|
+
const modelId = (modelOverride && modelOverride.trim().length > 0 ? modelOverride : void 0) ?? (globalEnvModel.length > 0 ? globalEnvModel : void 0) ?? agent.defaultModel ?? provider.defaultModel;
|
|
36
44
|
const model = provider.createModel({ modelId, apiKey });
|
|
37
45
|
return { model, modelId, providerId: provider.id };
|
|
38
46
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/agent-runtime.ts"],
|
|
4
|
-
"sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { LanguageModel, UIMessage } from 'ai'\nimport {\n convertToModelMessages,\n generateObject,\n stepCountIs,\n streamObject,\n streamText,\n} from 'ai'\nimport type { ZodTypeAny } from 'zod'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport type {\n AiAgentDefinition,\n AiAgentPageContextInput,\n AiAgentStructuredOutput,\n} from './ai-agent-definition'\nimport type {\n AiChatRequestContext,\n AiResolvedAttachmentPart,\n} from './attachment-bridge-types'\nimport { resolveAiAgentTools, AgentPolicyError } from './agent-tools'\nimport { resolveEffectiveMutationPolicy } from './agent-policy'\nimport { toolRegistry } from './tool-registry'\nimport {\n attachmentPartsToUiFileParts,\n resolveAttachmentPartsForAgent,\n summarizeAttachmentPartsForPrompt,\n} from './attachment-parts'\nimport { AiAgentPromptOverrideRepository } from '../data/repositories/AiAgentPromptOverrideRepository'\nimport { AiAgentMutationPolicyOverrideRepository } from '../data/repositories/AiAgentMutationPolicyOverrideRepository'\nimport { composeSystemPromptWithOverride } from './prompt-override-merge'\nimport { isKnownMutationPolicy } from './agent-policy'\nimport type { AiAgentMutationPolicy } from './ai-agent-definition'\n\n// Ensure built-in LLM providers are registered. Side-effect import; identical to\n// what `./ai-sdk.ts` consumers already rely on.\nimport './llm-bootstrap'\n\nexport interface AgentRequestPageContext {\n pageId?: string | null\n entityType?: string | null\n recordId?: string | null\n [key: string]: unknown\n}\n\nexport interface RunAiAgentTextInput {\n agentId: string\n messages: UIMessage[]\n attachmentIds?: string[]\n pageContext?: AgentRequestPageContext\n debug?: boolean\n /**\n * Phase 1 exposes the caller-supplied auth context directly on the helper\n * input. Phase 4 may wrap this behind a thinner public API once a global\n * request-context resolver exists. Helpers running inside the HTTP\n * dispatcher receive the same `AiChatRequestContext` used by `checkAgentPolicy`.\n */\n authContext: AiChatRequestContext\n /**\n * Optional per-call model id override that wins over `agent.defaultModel`.\n * The production model-factory extraction lives in Step 5.1; this Step\n * accepts a literal model id string so the Phase 1 runtime already honors\n * `agent.defaultModel` without inventing a new indirection layer.\n */\n modelOverride?: string\n /**\n * Optional DI container used by `resolvePageContext` callbacks. When omitted\n * and the agent declares a `resolvePageContext`, hydration is skipped with a\n * warning (callbacks that need database/DI cannot run safely without one).\n */\n container?: AwilixContainer\n /**\n * Optional stable chat-turn conversation id forwarded from `<AiChat>`.\n * Bridged into the Step 5.6 `prepareMutation` idempotency hash so repeated\n * turns within the same chat collapse onto the same pending action. When\n * omitted, the idempotency hash falls back to `null` which still preserves\n * per-tenant/org uniqueness within the TTL window.\n */\n conversationId?: string | null\n}\n\ninterface ResolvedAgentModel {\n model: LanguageModel\n modelId: string\n providerId: string\n}\n\nfunction resolveAgentModel(\n agent: AiAgentDefinition,\n modelOverride: string | undefined,\n): ResolvedAgentModel {\n const provider = llmProviderRegistry.resolveFirstConfigured()\n if (!provider) {\n throw new Error(\n 'No LLM provider is configured. Set OPENCODE_PROVIDER plus a matching API key such as ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY, then restart the app. See https://docs.openmercato.com/framework/ai-assistant/overview.',\n )\n }\n const apiKey = provider.resolveApiKey()\n if (!apiKey) {\n throw new Error(\n `LLM provider \"${provider.id}\" is advertised as configured but resolveApiKey() returned empty.`,\n )\n }\n const modelId =\n (modelOverride && modelOverride.trim().length > 0 ? modelOverride : undefined) ??\n agent.defaultModel ??\n provider.defaultModel\n const model = provider.createModel({ modelId, apiKey }) as LanguageModel\n return { model, modelId, providerId: provider.id }\n}\n\n/**\n * Composes the effective system prompt for a run. When the agent declares a\n * `resolvePageContext` callback AND the incoming request carries both\n * `entityType` and `recordId`, the callback is invoked and its return value\n * is appended to `agent.systemPrompt`. Throwing callbacks are caught and\n * logged without failing the request \u2014 the spec allows hydration to be\n * best-effort until Step 5.2 wires a stricter contract.\n */\nexport async function composeSystemPrompt(\n agent: AiAgentDefinition,\n pageContext: AgentRequestPageContext | undefined,\n container: AwilixContainer | undefined,\n tenantId: string | null,\n organizationId: string | null,\n): Promise<string> {\n const baseFromOverride = await resolveBaseSystemPromptWithOverride(\n agent,\n container,\n tenantId,\n organizationId,\n )\n const resolve = agent.resolvePageContext\n if (!resolve) return baseFromOverride\n const entityType = pageContext?.entityType\n const recordId = pageContext?.recordId\n if (typeof entityType !== 'string' || entityType.length === 0) return baseFromOverride\n if (typeof recordId !== 'string' || recordId.length === 0) return baseFromOverride\n if (!container) {\n console.warn(\n `[AI Agents] Agent \"${agent.id}\" declares resolvePageContext but no container was passed to runAiAgentText; skipping hydration.`,\n )\n return baseFromOverride\n }\n const hydrationInput: AiAgentPageContextInput = {\n entityType,\n recordId,\n container,\n tenantId,\n organizationId,\n }\n try {\n const hydrated = await resolve(hydrationInput)\n if (typeof hydrated === 'string' && hydrated.trim().length > 0) {\n return `${baseFromOverride}\\n\\n${hydrated}`\n }\n } catch (error) {\n console.error(\n `[AI Agents] resolvePageContext for agent \"${agent.id}\" failed; continuing without hydration:`,\n error,\n )\n }\n return baseFromOverride\n}\n\n/**\n * Fetches the latest tenant-scoped prompt override for `agent` (if any) and\n * layers it onto the built-in `systemPrompt` via the additive merge helper.\n *\n * BC + fail-open: every failure mode \u2014 missing container, missing `em`\n * registration, repository throw, missing migration \u2014 is logged at `warn`\n * and falls back to `agent.systemPrompt`. A chat turn MUST never fail on\n * override lookup (per Step 5.3 spec: \"If the repo call throws, log and\n * fall back to the built-in prompt \u2014 never fail the chat request\").\n */\nasync function resolveBaseSystemPromptWithOverride(\n agent: AiAgentDefinition,\n container: AwilixContainer | undefined,\n tenantId: string | null,\n organizationId: string | null,\n): Promise<string> {\n const base = agent.systemPrompt\n if (!tenantId || !container) return base\n let em: EntityManager | null = null\n try {\n em = container.resolve<EntityManager>('em')\n } catch {\n em = null\n }\n if (!em) return base\n try {\n const repo = new AiAgentPromptOverrideRepository(em)\n const latest = await repo.getLatest(agent.id, {\n tenantId,\n organizationId: organizationId ?? null,\n })\n if (!latest || !latest.sections || Object.keys(latest.sections).length === 0) {\n return base\n }\n return composeSystemPromptWithOverride(base, { sections: latest.sections })\n } catch (error) {\n console.warn(\n `[AI Agents] Prompt-override lookup failed for agent \"${agent.id}\"; falling back to built-in prompt.`,\n error,\n )\n return base\n }\n}\n\n/**\n * Looks up the tenant-scoped `mutationPolicy` override for `agentId` (Step\n * 5.4). Fails SAFE: any repo error, missing container, missing `em`\n * registration, or corrupt enum value returns `null`, which causes the\n * runtime to fall back to the agent's code-declared policy. A chat turn\n * MUST never fail on override lookup.\n */\nasync function resolveMutationPolicyOverride(\n agentId: string,\n container: AwilixContainer | undefined,\n tenantId: string | null,\n organizationId: string | null,\n): Promise<AiAgentMutationPolicy | null> {\n if (!tenantId || !container) return null\n let em: EntityManager | null = null\n try {\n em = container.resolve<EntityManager>('em')\n } catch {\n em = null\n }\n if (!em) return null\n try {\n const repo = new AiAgentMutationPolicyOverrideRepository(em)\n const row = await repo.get(agentId, { tenantId, organizationId: organizationId ?? null })\n if (!row) return null\n const raw = row.mutationPolicy\n if (!isKnownMutationPolicy(raw)) {\n console.warn(\n `[AI Agents] Ignoring corrupt mutationPolicy override row for agent \"${agentId}\": \"${raw}\". Falling back to code-declared policy.`,\n )\n return null\n }\n return raw\n } catch (error) {\n console.warn(\n `[AI Agents] mutationPolicy override lookup failed for agent \"${agentId}\"; falling back to code-declared policy.`,\n error,\n )\n return null\n }\n}\n\n/**\n * Normalizes simple `{ role, content }` chat messages into the AI SDK\n * `UIMessage` shape that `convertToModelMessages` requires. When the\n * incoming message already carries a `parts` array it is left untouched;\n * otherwise a single `TextUIPart` is synthesized from `content`.\n */\nfunction ensureUiMessageShape(messages: UIMessage[]): UIMessage[] {\n return messages.map((message, index) => {\n const raw = message as unknown as { id?: string; role?: string; content?: string; parts?: unknown[] }\n if (Array.isArray(raw.parts) && raw.parts.length > 0) {\n // Already has parts \u2014 only ensure `id` is present\n return { ...message, id: raw.id ?? `msg-${index}` } as UIMessage\n }\n const textContent = typeof raw.content === 'string' ? raw.content : ''\n return {\n id: raw.id ?? `msg-${index}`,\n role: raw.role ?? 'user',\n parts: [{ type: 'text', text: textContent }],\n } as unknown as UIMessage\n })\n}\n\n/**\n * Appends AI SDK v6 `FileUIPart` entries to the last user message in the\n * request so resolved attachment bytes / signed URLs reach the model. Pure\n * helper so chat-mode and object-mode share identical behavior \u2014 any\n * divergence here breaks the Step 3.6 parity contract.\n */\nfunction attachAttachmentsToMessages(\n messages: UIMessage[],\n parts: readonly AiResolvedAttachmentPart[],\n): UIMessage[] {\n if (parts.length === 0) return messages\n const fileParts = attachmentPartsToUiFileParts(parts)\n if (fileParts.length === 0) return messages\n const next = messages.slice()\n let lastUserIndex = -1\n for (let index = next.length - 1; index >= 0; index -= 1) {\n const candidate = next[index] as unknown as { role?: string }\n if (candidate?.role === 'user') {\n lastUserIndex = index\n break\n }\n }\n if (lastUserIndex === -1) {\n next.push({\n id: 'ai-runtime-attachments',\n role: 'user',\n parts: fileParts as unknown as UIMessage['parts'],\n } as unknown as UIMessage)\n return next\n }\n const source = next[lastUserIndex] as unknown as { parts?: unknown[] }\n const existingParts = Array.isArray(source.parts) ? source.parts : []\n next[lastUserIndex] = {\n ...(next[lastUserIndex] as object),\n parts: [...existingParts, ...fileParts],\n } as UIMessage\n return next\n}\n\nfunction appendAttachmentSummary(\n systemPrompt: string,\n parts: readonly AiResolvedAttachmentPart[],\n): string {\n const summary = summarizeAttachmentPartsForPrompt(parts)\n if (!summary) return systemPrompt\n return `${systemPrompt}\\n\\n${summary}`\n}\n\n/**\n * Builds a runtime \"MUTATION POLICY (RUNTIME)\" block describing the\n * EFFECTIVE policy for this turn \u2014 what the model should expect when it\n * calls each whitelisted mutation tool. Generated dynamically because:\n *\n * - the agent's static prompt cannot know which per-tenant override is\n * in force (`destructive-confirm-required` flips most writes to\n * run-direct) and would otherwise mislead the operator with stale\n * \"this requires approval\" copy;\n * - the per-tool `isDestructive` flag determines whether each\n * whitelisted write goes through the approval card or runs inline.\n *\n * Without this block, the model parrots its hardcoded \"always route\n * through the approval card\" prompt language and tells the user \"your\n * change is awaiting approval\" when in fact the dispatcher already\n * applied the change directly. The injected block flips the model to\n * report results accurately (\"applied\", \"pending your approval\", or\n * \"blocked because read-only\") tool-by-tool.\n */\nfunction buildRuntimeMutationPolicySection(\n agent: { id: string; mutationPolicy?: string | null; allowedTools: string[] },\n mutationPolicyOverride: string | null,\n): string | null {\n const effective = resolveEffectiveMutationPolicy(\n (agent.mutationPolicy ?? null) as never,\n (mutationPolicyOverride ?? null) as never,\n agent.id,\n )\n const lines: string[] = []\n lines.push('MUTATION POLICY (RUNTIME)')\n lines.push(`Declared agent policy: ${agent.mutationPolicy ?? 'read-only'}.`)\n if (mutationPolicyOverride && mutationPolicyOverride !== agent.mutationPolicy) {\n lines.push(`Tenant override active: ${mutationPolicyOverride}.`)\n }\n lines.push(`Effective policy: ${effective}.`)\n\n // Bucket the agent's allowlisted tools into \"gated\" / \"direct\" / \"conditional\"\n // / \"blocked\" so the model can phrase outcomes correctly per tool.\n // `conditional` covers tools whose `isDestructive` is a predicate function:\n // their gate-vs-direct decision depends on the per-call input (e.g.\n // `customers.manage_deal_comment` gates only its delete branch under\n // `destructive-confirm-required`).\n const direct: string[] = []\n const gated: string[] = []\n const conditional: string[] = []\n const blocked: string[] = []\n for (const toolName of agent.allowedTools) {\n const tool = toolRegistry.getTool(toolName) as\n | { isMutation?: boolean; isDestructive?: boolean | ((input: unknown) => boolean) }\n | undefined\n if (!tool || tool.isMutation !== true) continue\n if (effective === 'read-only') {\n blocked.push(toolName)\n continue\n }\n if (effective === 'confirm-required') {\n gated.push(toolName)\n continue\n }\n // destructive-confirm-required\n if (typeof tool.isDestructive === 'function') {\n conditional.push(toolName)\n } else if (tool.isDestructive === true) {\n gated.push(toolName)\n } else {\n direct.push(toolName)\n }\n }\n\n if (\n direct.length === 0 &&\n gated.length === 0 &&\n conditional.length === 0 &&\n blocked.length === 0\n ) {\n // Read-only agent with no mutation tools \u2014 no runtime policy block needed.\n return null\n }\n if (direct.length > 0) {\n lines.push('')\n lines.push(\n `Tools that WILL RUN DIRECTLY (no approval card, no pending action) under the effective policy: ${direct.join(', ')}.`,\n )\n lines.push(\n 'When you call any of these and the call returns successfully, the change has ALREADY BEEN APPLIED. Report it in the past tense (\"Updated \u2026\", \"Added \u2026\", \"Created \u2026\"). Do NOT tell the operator the action is \"pending your approval\" or \"awaiting confirmation\" \u2014 that would be a false statement under the current policy.',\n )\n }\n if (gated.length > 0) {\n lines.push('')\n lines.push(\n `Tools that REQUIRE APPROVAL under the effective policy: ${gated.join(', ')}.`,\n )\n lines.push(\n 'When you call any of these, the dispatcher returns an \"awaiting confirmation\" envelope and renders an inline approval card. Tell the operator the change is pending their confirmation; do NOT claim it has been applied.',\n )\n }\n if (conditional.length > 0) {\n lines.push('')\n lines.push(\n `Tools whose approval requirement DEPENDS ON THE INPUT under the effective policy: ${conditional.join(', ')}.`,\n )\n lines.push(\n 'These multi-operation tools gate ONLY the destructive branches (typically `operation: \"delete\"` or similar). Read the tool result envelope: if it carries `status: \"pending-confirmation\"` then the change is pending \u2014 tell the operator it needs their approval. If it carries direct success data, the change has ALREADY BEEN APPLIED \u2014 report it in the past tense. Never assume one branch behaves like another.',\n )\n }\n if (blocked.length > 0) {\n lines.push('')\n lines.push(\n `Tools that are BLOCKED under the effective policy (read-only): ${blocked.join(', ')}.`,\n )\n lines.push(\n 'Calls to these tools are refused before the handler runs. Do not attempt them; instead direct the operator to the matching backoffice page or to switch the tenant policy if they have permission.',\n )\n }\n lines.push('')\n lines.push(\n 'This RUNTIME policy block always wins over any conflicting \"approval card\" language earlier in the prompt \u2014 the static prompt is written for the most restrictive case but real behavior depends on the per-call policy described here.',\n )\n return lines.join('\\n')\n}\n\nfunction appendRuntimeMutationPolicy(\n systemPrompt: string,\n agent: { id: string; mutationPolicy?: string | null; allowedTools: string[] },\n mutationPolicyOverride: string | null,\n): string {\n const block = buildRuntimeMutationPolicySection(agent, mutationPolicyOverride)\n if (!block) return systemPrompt\n return `${systemPrompt}\\n\\n${block}`\n}\n\n/**\n * Server-side helper that runs an Open Mercato agent in chat mode via the\n * Vercel AI SDK and returns a streaming `Response` ready to be emitted from a\n * route handler. Shares the same policy gate and tool resolution path as the\n * HTTP dispatcher \u2014 a caller using this helper can never bypass the agent's\n * `requiredFeatures`, `allowedTools`, `executionMode`, or `mutationPolicy`.\n *\n * Attachment-to-model conversion (Step 3.7): resolved\n * {@link AiResolvedAttachmentPart}s are materialized inline as AI SDK v6\n * `FileUIPart` entries on the last user message (images/PDFs) and as a\n * structured `[ATTACHMENTS]` block appended to the system prompt (text\n * extracts + metadata-only summaries). The existing `attachmentIds`\n * pass-through into `resolveAiAgentTools` is preserved \u2014 Step 3.6 parity\n * invariant #7 still holds.\n */\nexport async function runAiAgentText(input: RunAiAgentTextInput): Promise<Response> {\n const mutationPolicyOverride = await resolveMutationPolicyOverride(\n input.agentId,\n input.container,\n input.authContext.tenantId,\n input.authContext.organizationId,\n )\n const { agent, tools } = await resolveAiAgentTools({\n agentId: input.agentId,\n authContext: input.authContext,\n pageContext: input.pageContext,\n attachmentIds: input.attachmentIds,\n mutationPolicyOverride,\n container: input.container,\n conversationId: input.conversationId ?? null,\n })\n\n const resolvedAttachments = await resolveAttachmentPartsForAgent({\n agent,\n attachmentIds: input.attachmentIds,\n authContext: input.authContext,\n container: input.container,\n })\n\n const baseSystemPrompt = await composeSystemPrompt(\n agent,\n input.pageContext,\n input.container,\n input.authContext.tenantId,\n input.authContext.organizationId,\n )\n const systemPrompt = appendRuntimeMutationPolicy(\n appendAttachmentSummary(baseSystemPrompt, resolvedAttachments),\n agent,\n mutationPolicyOverride,\n )\n\n const { model } = resolveAgentModel(agent, input.modelOverride)\n const normalizedMessages = ensureUiMessageShape(input.messages)\n const hydratedMessages = attachAttachmentsToMessages(normalizedMessages, resolvedAttachments)\n const modelMessages = await convertToModelMessages(hydratedMessages)\n // Default to 10 agentic steps when the agent does not declare maxSteps.\n // Without stopWhen the AI SDK runs a single model call and never executes\n // tool calls, which makes every tool-using query return an empty stream.\n const effectiveMaxSteps = typeof agent.maxSteps === 'number' && agent.maxSteps > 0\n ? agent.maxSteps\n : 10\n const stopWhen = stepCountIs(effectiveMaxSteps)\n\n const streamArgs: Parameters<typeof streamText>[0] = {\n model,\n system: systemPrompt,\n messages: modelMessages,\n tools,\n stopWhen,\n }\n\n const result = streamText(streamArgs)\n return result.toUIMessageStreamResponse({\n sendReasoning: true,\n headers: {\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n },\n })\n}\n\n/**\n * Runtime override for the structured-output schema used by {@link runAiAgentObject}.\n * When the agent itself declares no `output` block, the caller MUST supply this;\n * otherwise the helper rejects with {@link AgentPolicyError} code\n * `execution_mode_not_supported`.\n */\nexport interface RunAiAgentObjectOutputOverride<TSchema = ZodTypeAny> {\n schemaName: string\n schema: TSchema\n /**\n * `'generate'` (default) calls AI SDK `generateObject` and resolves to the\n * parsed object. `'stream'` calls `streamObject` and returns the SDK's\n * streaming handle so callers can consume partial objects / text deltas.\n */\n mode?: 'generate' | 'stream'\n}\n\nexport interface RunAiAgentObjectInput<TSchema = ZodTypeAny> {\n agentId: string\n /**\n * Accepts either a bare user prompt (wrapped as `[{ role: 'user', content }]`)\n * or a prebuilt `UIMessage[]` array \u2014 matches the source spec's\n * `RunAiAgentObjectInput` contract (\u00A71149\u20131160).\n */\n input: string | UIMessage[]\n attachmentIds?: string[]\n pageContext?: AgentRequestPageContext\n /**\n * Same Phase-1 shim as {@link RunAiAgentTextInput.authContext}. Required until\n * a global request-context resolver lands (Phase 4).\n */\n authContext: AiChatRequestContext\n modelOverride?: string\n output?: RunAiAgentObjectOutputOverride<TSchema>\n debug?: boolean\n container?: AwilixContainer\n}\n\nexport type RunAiAgentObjectGenerateResult<TSchema> = {\n mode: 'generate'\n object: TSchema\n finishReason?: string\n usage?: { inputTokens?: number; outputTokens?: number }\n}\n\nexport type RunAiAgentObjectStreamResult<TSchema> = {\n mode: 'stream'\n /** Full parsed object once the stream completes. */\n object: Promise<TSchema>\n /** Async iterator of partial (progressively hydrated) objects. */\n partialObjectStream: AsyncIterable<Partial<TSchema>>\n /** Async iterator of the raw text deltas the model emitted. */\n textStream: AsyncIterable<string>\n finishReason?: Promise<string | undefined>\n usage?: Promise<{ inputTokens?: number; outputTokens?: number } | undefined>\n}\n\nexport type RunAiAgentObjectResult<TSchema> =\n | RunAiAgentObjectGenerateResult<TSchema>\n | RunAiAgentObjectStreamResult<TSchema>\n\nfunction normalizeObjectMessages(input: string | UIMessage[]): UIMessage[] {\n if (typeof input === 'string') {\n return [\n {\n id: 'user-input',\n role: 'user',\n parts: [{ type: 'text', text: input }],\n } as unknown as UIMessage,\n ]\n }\n return input\n}\n\nfunction resolveStructuredOutput<TSchema>(\n agent: AiAgentDefinition,\n override: RunAiAgentObjectOutputOverride<TSchema> | undefined,\n): { schemaName: string; schema: unknown; mode: 'generate' | 'stream' } {\n if (override) {\n return {\n schemaName: override.schemaName,\n schema: override.schema as unknown,\n mode: override.mode ?? 'generate',\n }\n }\n const declared = agent.output as AiAgentStructuredOutput | undefined\n if (!declared) {\n throw new AgentPolicyError(\n 'execution_mode_not_supported',\n `Agent \"${agent.id}\" does not declare a structured-output schema; pass runAiAgentObject({ output }) or declare agent.output.`,\n )\n }\n return {\n schemaName: declared.schemaName,\n schema: declared.schema as unknown,\n mode: declared.mode ?? 'generate',\n }\n}\n\n/**\n * Server-side helper that runs an Open Mercato agent in structured-output mode\n * via the Vercel AI SDK. Shares the same policy gate, tool resolution path,\n * system-prompt composition, and model resolution as {@link runAiAgentText} \u2014\n * object-mode and chat-mode CANNOT diverge.\n *\n * Attachment-to-model conversion (Step 3.7): resolved\n * {@link AiResolvedAttachmentPart}s are materialized inline as AI SDK v6\n * `FileUIPart` entries on the last user message (images/PDFs) and as a\n * structured `[ATTACHMENTS]` block appended to the system prompt (text\n * extracts + metadata-only summaries). Matches {@link runAiAgentText} byte-\n * for-byte so the Step 3.6 parity contract is preserved.\n */\nexport async function runAiAgentObject<TSchema = unknown>(\n input: RunAiAgentObjectInput<TSchema>,\n): Promise<RunAiAgentObjectResult<TSchema>> {\n const mutationPolicyOverride = await resolveMutationPolicyOverride(\n input.agentId,\n input.container,\n input.authContext.tenantId,\n input.authContext.organizationId,\n )\n const { agent, tools } = await resolveAiAgentTools({\n agentId: input.agentId,\n authContext: input.authContext,\n pageContext: input.pageContext,\n attachmentIds: input.attachmentIds,\n requestedExecutionMode: 'object',\n mutationPolicyOverride,\n container: input.container,\n })\n\n const resolvedOutput = resolveStructuredOutput(agent, input.output)\n\n const resolvedAttachments = await resolveAttachmentPartsForAgent({\n agent,\n attachmentIds: input.attachmentIds,\n authContext: input.authContext,\n container: input.container,\n })\n\n const baseSystemPrompt = await composeSystemPrompt(\n agent,\n input.pageContext,\n input.container,\n input.authContext.tenantId,\n input.authContext.organizationId,\n )\n const systemPrompt = appendRuntimeMutationPolicy(\n appendAttachmentSummary(baseSystemPrompt, resolvedAttachments),\n agent,\n mutationPolicyOverride,\n )\n\n const { model } = resolveAgentModel(agent, input.modelOverride)\n const normalizedMessages = ensureUiMessageShape(normalizeObjectMessages(input.input))\n const hydratedMessages = attachAttachmentsToMessages(\n normalizedMessages,\n resolvedAttachments,\n )\n const modelMessages = await convertToModelMessages(hydratedMessages)\n const stopWhen = typeof agent.maxSteps === 'number' && agent.maxSteps > 0\n ? stepCountIs(agent.maxSteps)\n : undefined\n\n if (resolvedOutput.mode === 'stream') {\n const streamArgs: Parameters<typeof streamObject>[0] = {\n model,\n system: systemPrompt,\n messages: modelMessages,\n schema: resolvedOutput.schema as never,\n schemaName: resolvedOutput.schemaName,\n }\n const result = streamObject(streamArgs) as unknown as {\n object: Promise<TSchema>\n partialObjectStream: AsyncIterable<Partial<TSchema>>\n textStream: AsyncIterable<string>\n finishReason?: Promise<string | undefined>\n usage?: Promise<{ inputTokens?: number; outputTokens?: number } | undefined>\n }\n return {\n mode: 'stream',\n object: result.object,\n partialObjectStream: result.partialObjectStream,\n textStream: result.textStream,\n finishReason: result.finishReason,\n usage: result.usage,\n }\n }\n\n const generateArgs: Parameters<typeof generateObject>[0] = {\n model,\n system: systemPrompt,\n messages: modelMessages,\n schema: resolvedOutput.schema as never,\n schemaName: resolvedOutput.schemaName,\n }\n if (stopWhen) {\n // generateObject shares `CallSettings` with generateText; stopWhen is ignored\n // by the typed surface but harmless for providers that respect it. Tools\n // flow through the system prompt only in object mode today \u2014 the whitelist\n // has already been resolved via `resolveAiAgentTools` above, even if we\n // don't hand it to generateObject.\n ;(generateArgs as Record<string, unknown>).stopWhen = stopWhen\n }\n void tools\n\n const result = await generateObject(generateArgs)\n return {\n mode: 'generate',\n object: (result as { object: unknown }).object as TSchema,\n finishReason: (result as { finishReason?: string }).finishReason,\n usage: (result as { usage?: { inputTokens?: number; outputTokens?: number } }).usage,\n }\n}\n\nexport { AgentPolicyError }\n"],
|
|
5
|
-
"mappings": "AAGA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,2BAA2B;
|
|
4
|
+
"sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { LanguageModel, UIMessage } from 'ai'\nimport {\n convertToModelMessages,\n generateObject,\n stepCountIs,\n streamObject,\n streamText,\n} from 'ai'\nimport type { ZodTypeAny } from 'zod'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { resolveAiProviderIdFromEnv } from '@open-mercato/shared/lib/ai/opencode-provider'\nimport type {\n AiAgentDefinition,\n AiAgentPageContextInput,\n AiAgentStructuredOutput,\n} from './ai-agent-definition'\nimport type {\n AiChatRequestContext,\n AiResolvedAttachmentPart,\n} from './attachment-bridge-types'\nimport { resolveAiAgentTools, AgentPolicyError } from './agent-tools'\nimport { resolveEffectiveMutationPolicy } from './agent-policy'\nimport { toolRegistry } from './tool-registry'\nimport {\n attachmentPartsToUiFileParts,\n resolveAttachmentPartsForAgent,\n summarizeAttachmentPartsForPrompt,\n} from './attachment-parts'\nimport { AiAgentPromptOverrideRepository } from '../data/repositories/AiAgentPromptOverrideRepository'\nimport { AiAgentMutationPolicyOverrideRepository } from '../data/repositories/AiAgentMutationPolicyOverrideRepository'\nimport { composeSystemPromptWithOverride } from './prompt-override-merge'\nimport { isKnownMutationPolicy } from './agent-policy'\nimport type { AiAgentMutationPolicy } from './ai-agent-definition'\n\n// Ensure built-in LLM providers are registered. Side-effect import; identical to\n// what `./ai-sdk.ts` consumers already rely on.\nimport './llm-bootstrap'\n\nexport interface AgentRequestPageContext {\n pageId?: string | null\n entityType?: string | null\n recordId?: string | null\n [key: string]: unknown\n}\n\nexport interface RunAiAgentTextInput {\n agentId: string\n messages: UIMessage[]\n attachmentIds?: string[]\n pageContext?: AgentRequestPageContext\n debug?: boolean\n /**\n * Phase 1 exposes the caller-supplied auth context directly on the helper\n * input. Phase 4 may wrap this behind a thinner public API once a global\n * request-context resolver exists. Helpers running inside the HTTP\n * dispatcher receive the same `AiChatRequestContext` used by `checkAgentPolicy`.\n */\n authContext: AiChatRequestContext\n /**\n * Optional per-call model id override that wins over `agent.defaultModel`.\n * The production model-factory extraction lives in Step 5.1; this Step\n * accepts a literal model id string so the Phase 1 runtime already honors\n * `agent.defaultModel` without inventing a new indirection layer.\n */\n modelOverride?: string\n /**\n * Optional DI container used by `resolvePageContext` callbacks. When omitted\n * and the agent declares a `resolvePageContext`, hydration is skipped with a\n * warning (callbacks that need database/DI cannot run safely without one).\n */\n container?: AwilixContainer\n /**\n * Optional stable chat-turn conversation id forwarded from `<AiChat>`.\n * Bridged into the Step 5.6 `prepareMutation` idempotency hash so repeated\n * turns within the same chat collapse onto the same pending action. When\n * omitted, the idempotency hash falls back to `null` which still preserves\n * per-tenant/org uniqueness within the TTL window.\n */\n conversationId?: string | null\n}\n\ninterface ResolvedAgentModel {\n model: LanguageModel\n modelId: string\n providerId: string\n}\n\nfunction resolveAgentModel(\n agent: AiAgentDefinition,\n modelOverride: string | undefined,\n): ResolvedAgentModel {\n const env = process.env\n // Honor the operator-selected provider (new OM_AI_PROVIDER, with legacy\n // OPENCODE_PROVIDER as the BC fallback) so deployments that have stale\n // keys for other providers still hit the intended target.\n const omProvider = (env.OM_AI_PROVIDER ?? '').trim()\n const opencodeProvider = (env.OPENCODE_PROVIDER ?? '').trim()\n const providerHint = omProvider.length > 0 || opencodeProvider.length > 0\n ? [resolveAiProviderIdFromEnv(env)]\n : undefined\n const provider = llmProviderRegistry.resolveFirstConfigured(\n providerHint ? { order: providerHint } : undefined,\n )\n if (!provider) {\n throw new Error(\n 'No LLM provider is configured. Set OM_AI_PROVIDER (or the legacy OPENCODE_PROVIDER) plus a matching API key such as OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY, then restart the app. See https://docs.openmercato.com/framework/ai-assistant/overview.',\n )\n }\n const apiKey = provider.resolveApiKey()\n if (!apiKey) {\n throw new Error(\n `LLM provider \"${provider.id}\" is advertised as configured but resolveApiKey() returned empty.`,\n )\n }\n const globalEnvModel = ((env.OM_AI_MODEL ?? env.OPENCODE_MODEL) ?? '').trim()\n const modelId =\n (modelOverride && modelOverride.trim().length > 0 ? modelOverride : undefined) ??\n (globalEnvModel.length > 0 ? globalEnvModel : undefined) ??\n agent.defaultModel ??\n provider.defaultModel\n const model = provider.createModel({ modelId, apiKey }) as LanguageModel\n return { model, modelId, providerId: provider.id }\n}\n\n/**\n * Composes the effective system prompt for a run. When the agent declares a\n * `resolvePageContext` callback AND the incoming request carries both\n * `entityType` and `recordId`, the callback is invoked and its return value\n * is appended to `agent.systemPrompt`. Throwing callbacks are caught and\n * logged without failing the request \u2014 the spec allows hydration to be\n * best-effort until Step 5.2 wires a stricter contract.\n */\nexport async function composeSystemPrompt(\n agent: AiAgentDefinition,\n pageContext: AgentRequestPageContext | undefined,\n container: AwilixContainer | undefined,\n tenantId: string | null,\n organizationId: string | null,\n): Promise<string> {\n const baseFromOverride = await resolveBaseSystemPromptWithOverride(\n agent,\n container,\n tenantId,\n organizationId,\n )\n const resolve = agent.resolvePageContext\n if (!resolve) return baseFromOverride\n const entityType = pageContext?.entityType\n const recordId = pageContext?.recordId\n if (typeof entityType !== 'string' || entityType.length === 0) return baseFromOverride\n if (typeof recordId !== 'string' || recordId.length === 0) return baseFromOverride\n if (!container) {\n console.warn(\n `[AI Agents] Agent \"${agent.id}\" declares resolvePageContext but no container was passed to runAiAgentText; skipping hydration.`,\n )\n return baseFromOverride\n }\n const hydrationInput: AiAgentPageContextInput = {\n entityType,\n recordId,\n container,\n tenantId,\n organizationId,\n }\n try {\n const hydrated = await resolve(hydrationInput)\n if (typeof hydrated === 'string' && hydrated.trim().length > 0) {\n return `${baseFromOverride}\\n\\n${hydrated}`\n }\n } catch (error) {\n console.error(\n `[AI Agents] resolvePageContext for agent \"${agent.id}\" failed; continuing without hydration:`,\n error,\n )\n }\n return baseFromOverride\n}\n\n/**\n * Fetches the latest tenant-scoped prompt override for `agent` (if any) and\n * layers it onto the built-in `systemPrompt` via the additive merge helper.\n *\n * BC + fail-open: every failure mode \u2014 missing container, missing `em`\n * registration, repository throw, missing migration \u2014 is logged at `warn`\n * and falls back to `agent.systemPrompt`. A chat turn MUST never fail on\n * override lookup (per Step 5.3 spec: \"If the repo call throws, log and\n * fall back to the built-in prompt \u2014 never fail the chat request\").\n */\nasync function resolveBaseSystemPromptWithOverride(\n agent: AiAgentDefinition,\n container: AwilixContainer | undefined,\n tenantId: string | null,\n organizationId: string | null,\n): Promise<string> {\n const base = agent.systemPrompt\n if (!tenantId || !container) return base\n let em: EntityManager | null = null\n try {\n em = container.resolve<EntityManager>('em')\n } catch {\n em = null\n }\n if (!em) return base\n try {\n const repo = new AiAgentPromptOverrideRepository(em)\n const latest = await repo.getLatest(agent.id, {\n tenantId,\n organizationId: organizationId ?? null,\n })\n if (!latest || !latest.sections || Object.keys(latest.sections).length === 0) {\n return base\n }\n return composeSystemPromptWithOverride(base, { sections: latest.sections })\n } catch (error) {\n console.warn(\n `[AI Agents] Prompt-override lookup failed for agent \"${agent.id}\"; falling back to built-in prompt.`,\n error,\n )\n return base\n }\n}\n\n/**\n * Looks up the tenant-scoped `mutationPolicy` override for `agentId` (Step\n * 5.4). Fails SAFE: any repo error, missing container, missing `em`\n * registration, or corrupt enum value returns `null`, which causes the\n * runtime to fall back to the agent's code-declared policy. A chat turn\n * MUST never fail on override lookup.\n */\nasync function resolveMutationPolicyOverride(\n agentId: string,\n container: AwilixContainer | undefined,\n tenantId: string | null,\n organizationId: string | null,\n): Promise<AiAgentMutationPolicy | null> {\n if (!tenantId || !container) return null\n let em: EntityManager | null = null\n try {\n em = container.resolve<EntityManager>('em')\n } catch {\n em = null\n }\n if (!em) return null\n try {\n const repo = new AiAgentMutationPolicyOverrideRepository(em)\n const row = await repo.get(agentId, { tenantId, organizationId: organizationId ?? null })\n if (!row) return null\n const raw = row.mutationPolicy\n if (!isKnownMutationPolicy(raw)) {\n console.warn(\n `[AI Agents] Ignoring corrupt mutationPolicy override row for agent \"${agentId}\": \"${raw}\". Falling back to code-declared policy.`,\n )\n return null\n }\n return raw\n } catch (error) {\n console.warn(\n `[AI Agents] mutationPolicy override lookup failed for agent \"${agentId}\"; falling back to code-declared policy.`,\n error,\n )\n return null\n }\n}\n\n/**\n * Normalizes simple `{ role, content }` chat messages into the AI SDK\n * `UIMessage` shape that `convertToModelMessages` requires. When the\n * incoming message already carries a `parts` array it is left untouched;\n * otherwise a single `TextUIPart` is synthesized from `content`.\n */\nfunction ensureUiMessageShape(messages: UIMessage[]): UIMessage[] {\n return messages.map((message, index) => {\n const raw = message as unknown as { id?: string; role?: string; content?: string; parts?: unknown[] }\n if (Array.isArray(raw.parts) && raw.parts.length > 0) {\n // Already has parts \u2014 only ensure `id` is present\n return { ...message, id: raw.id ?? `msg-${index}` } as UIMessage\n }\n const textContent = typeof raw.content === 'string' ? raw.content : ''\n return {\n id: raw.id ?? `msg-${index}`,\n role: raw.role ?? 'user',\n parts: [{ type: 'text', text: textContent }],\n } as unknown as UIMessage\n })\n}\n\n/**\n * Appends AI SDK v6 `FileUIPart` entries to the last user message in the\n * request so resolved attachment bytes / signed URLs reach the model. Pure\n * helper so chat-mode and object-mode share identical behavior \u2014 any\n * divergence here breaks the Step 3.6 parity contract.\n */\nfunction attachAttachmentsToMessages(\n messages: UIMessage[],\n parts: readonly AiResolvedAttachmentPart[],\n): UIMessage[] {\n if (parts.length === 0) return messages\n const fileParts = attachmentPartsToUiFileParts(parts)\n if (fileParts.length === 0) return messages\n const next = messages.slice()\n let lastUserIndex = -1\n for (let index = next.length - 1; index >= 0; index -= 1) {\n const candidate = next[index] as unknown as { role?: string }\n if (candidate?.role === 'user') {\n lastUserIndex = index\n break\n }\n }\n if (lastUserIndex === -1) {\n next.push({\n id: 'ai-runtime-attachments',\n role: 'user',\n parts: fileParts as unknown as UIMessage['parts'],\n } as unknown as UIMessage)\n return next\n }\n const source = next[lastUserIndex] as unknown as { parts?: unknown[] }\n const existingParts = Array.isArray(source.parts) ? source.parts : []\n next[lastUserIndex] = {\n ...(next[lastUserIndex] as object),\n parts: [...existingParts, ...fileParts],\n } as UIMessage\n return next\n}\n\nfunction appendAttachmentSummary(\n systemPrompt: string,\n parts: readonly AiResolvedAttachmentPart[],\n): string {\n const summary = summarizeAttachmentPartsForPrompt(parts)\n if (!summary) return systemPrompt\n return `${systemPrompt}\\n\\n${summary}`\n}\n\n/**\n * Builds a runtime \"MUTATION POLICY (RUNTIME)\" block describing the\n * EFFECTIVE policy for this turn \u2014 what the model should expect when it\n * calls each whitelisted mutation tool. Generated dynamically because:\n *\n * - the agent's static prompt cannot know which per-tenant override is\n * in force (`destructive-confirm-required` flips most writes to\n * run-direct) and would otherwise mislead the operator with stale\n * \"this requires approval\" copy;\n * - the per-tool `isDestructive` flag determines whether each\n * whitelisted write goes through the approval card or runs inline.\n *\n * Without this block, the model parrots its hardcoded \"always route\n * through the approval card\" prompt language and tells the user \"your\n * change is awaiting approval\" when in fact the dispatcher already\n * applied the change directly. The injected block flips the model to\n * report results accurately (\"applied\", \"pending your approval\", or\n * \"blocked because read-only\") tool-by-tool.\n */\nfunction buildRuntimeMutationPolicySection(\n agent: { id: string; mutationPolicy?: string | null; allowedTools: string[] },\n mutationPolicyOverride: string | null,\n): string | null {\n const effective = resolveEffectiveMutationPolicy(\n (agent.mutationPolicy ?? null) as never,\n (mutationPolicyOverride ?? null) as never,\n agent.id,\n )\n const lines: string[] = []\n lines.push('MUTATION POLICY (RUNTIME)')\n lines.push(`Declared agent policy: ${agent.mutationPolicy ?? 'read-only'}.`)\n if (mutationPolicyOverride && mutationPolicyOverride !== agent.mutationPolicy) {\n lines.push(`Tenant override active: ${mutationPolicyOverride}.`)\n }\n lines.push(`Effective policy: ${effective}.`)\n\n // Bucket the agent's allowlisted tools into \"gated\" / \"direct\" / \"conditional\"\n // / \"blocked\" so the model can phrase outcomes correctly per tool.\n // `conditional` covers tools whose `isDestructive` is a predicate function:\n // their gate-vs-direct decision depends on the per-call input (e.g.\n // `customers.manage_deal_comment` gates only its delete branch under\n // `destructive-confirm-required`).\n const direct: string[] = []\n const gated: string[] = []\n const conditional: string[] = []\n const blocked: string[] = []\n for (const toolName of agent.allowedTools) {\n const tool = toolRegistry.getTool(toolName) as\n | { isMutation?: boolean; isDestructive?: boolean | ((input: unknown) => boolean) }\n | undefined\n if (!tool || tool.isMutation !== true) continue\n if (effective === 'read-only') {\n blocked.push(toolName)\n continue\n }\n if (effective === 'confirm-required') {\n gated.push(toolName)\n continue\n }\n // destructive-confirm-required\n if (typeof tool.isDestructive === 'function') {\n conditional.push(toolName)\n } else if (tool.isDestructive === true) {\n gated.push(toolName)\n } else {\n direct.push(toolName)\n }\n }\n\n if (\n direct.length === 0 &&\n gated.length === 0 &&\n conditional.length === 0 &&\n blocked.length === 0\n ) {\n // Read-only agent with no mutation tools \u2014 no runtime policy block needed.\n return null\n }\n if (direct.length > 0) {\n lines.push('')\n lines.push(\n `Tools that WILL RUN DIRECTLY (no approval card, no pending action) under the effective policy: ${direct.join(', ')}.`,\n )\n lines.push(\n 'When you call any of these and the call returns successfully, the change has ALREADY BEEN APPLIED. Report it in the past tense (\"Updated \u2026\", \"Added \u2026\", \"Created \u2026\"). Do NOT tell the operator the action is \"pending your approval\" or \"awaiting confirmation\" \u2014 that would be a false statement under the current policy.',\n )\n }\n if (gated.length > 0) {\n lines.push('')\n lines.push(\n `Tools that REQUIRE APPROVAL under the effective policy: ${gated.join(', ')}.`,\n )\n lines.push(\n 'When you call any of these, the dispatcher returns an \"awaiting confirmation\" envelope and renders an inline approval card. Tell the operator the change is pending their confirmation; do NOT claim it has been applied.',\n )\n }\n if (conditional.length > 0) {\n lines.push('')\n lines.push(\n `Tools whose approval requirement DEPENDS ON THE INPUT under the effective policy: ${conditional.join(', ')}.`,\n )\n lines.push(\n 'These multi-operation tools gate ONLY the destructive branches (typically `operation: \"delete\"` or similar). Read the tool result envelope: if it carries `status: \"pending-confirmation\"` then the change is pending \u2014 tell the operator it needs their approval. If it carries direct success data, the change has ALREADY BEEN APPLIED \u2014 report it in the past tense. Never assume one branch behaves like another.',\n )\n }\n if (blocked.length > 0) {\n lines.push('')\n lines.push(\n `Tools that are BLOCKED under the effective policy (read-only): ${blocked.join(', ')}.`,\n )\n lines.push(\n 'Calls to these tools are refused before the handler runs. Do not attempt them; instead direct the operator to the matching backoffice page or to switch the tenant policy if they have permission.',\n )\n }\n lines.push('')\n lines.push(\n 'This RUNTIME policy block always wins over any conflicting \"approval card\" language earlier in the prompt \u2014 the static prompt is written for the most restrictive case but real behavior depends on the per-call policy described here.',\n )\n return lines.join('\\n')\n}\n\nfunction appendRuntimeMutationPolicy(\n systemPrompt: string,\n agent: { id: string; mutationPolicy?: string | null; allowedTools: string[] },\n mutationPolicyOverride: string | null,\n): string {\n const block = buildRuntimeMutationPolicySection(agent, mutationPolicyOverride)\n if (!block) return systemPrompt\n return `${systemPrompt}\\n\\n${block}`\n}\n\n/**\n * Server-side helper that runs an Open Mercato agent in chat mode via the\n * Vercel AI SDK and returns a streaming `Response` ready to be emitted from a\n * route handler. Shares the same policy gate and tool resolution path as the\n * HTTP dispatcher \u2014 a caller using this helper can never bypass the agent's\n * `requiredFeatures`, `allowedTools`, `executionMode`, or `mutationPolicy`.\n *\n * Attachment-to-model conversion (Step 3.7): resolved\n * {@link AiResolvedAttachmentPart}s are materialized inline as AI SDK v6\n * `FileUIPart` entries on the last user message (images/PDFs) and as a\n * structured `[ATTACHMENTS]` block appended to the system prompt (text\n * extracts + metadata-only summaries). The existing `attachmentIds`\n * pass-through into `resolveAiAgentTools` is preserved \u2014 Step 3.6 parity\n * invariant #7 still holds.\n */\nexport async function runAiAgentText(input: RunAiAgentTextInput): Promise<Response> {\n const mutationPolicyOverride = await resolveMutationPolicyOverride(\n input.agentId,\n input.container,\n input.authContext.tenantId,\n input.authContext.organizationId,\n )\n const { agent, tools } = await resolveAiAgentTools({\n agentId: input.agentId,\n authContext: input.authContext,\n pageContext: input.pageContext,\n attachmentIds: input.attachmentIds,\n mutationPolicyOverride,\n container: input.container,\n conversationId: input.conversationId ?? null,\n })\n\n const resolvedAttachments = await resolveAttachmentPartsForAgent({\n agent,\n attachmentIds: input.attachmentIds,\n authContext: input.authContext,\n container: input.container,\n })\n\n const baseSystemPrompt = await composeSystemPrompt(\n agent,\n input.pageContext,\n input.container,\n input.authContext.tenantId,\n input.authContext.organizationId,\n )\n const systemPrompt = appendRuntimeMutationPolicy(\n appendAttachmentSummary(baseSystemPrompt, resolvedAttachments),\n agent,\n mutationPolicyOverride,\n )\n\n const { model } = resolveAgentModel(agent, input.modelOverride)\n const normalizedMessages = ensureUiMessageShape(input.messages)\n const hydratedMessages = attachAttachmentsToMessages(normalizedMessages, resolvedAttachments)\n const modelMessages = await convertToModelMessages(hydratedMessages)\n // Default to 10 agentic steps when the agent does not declare maxSteps.\n // Without stopWhen the AI SDK runs a single model call and never executes\n // tool calls, which makes every tool-using query return an empty stream.\n const effectiveMaxSteps = typeof agent.maxSteps === 'number' && agent.maxSteps > 0\n ? agent.maxSteps\n : 10\n const stopWhen = stepCountIs(effectiveMaxSteps)\n\n const streamArgs: Parameters<typeof streamText>[0] = {\n model,\n system: systemPrompt,\n messages: modelMessages,\n tools,\n stopWhen,\n }\n\n const result = streamText(streamArgs)\n return result.toUIMessageStreamResponse({\n sendReasoning: true,\n headers: {\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n },\n })\n}\n\n/**\n * Runtime override for the structured-output schema used by {@link runAiAgentObject}.\n * When the agent itself declares no `output` block, the caller MUST supply this;\n * otherwise the helper rejects with {@link AgentPolicyError} code\n * `execution_mode_not_supported`.\n */\nexport interface RunAiAgentObjectOutputOverride<TSchema = ZodTypeAny> {\n schemaName: string\n schema: TSchema\n /**\n * `'generate'` (default) calls AI SDK `generateObject` and resolves to the\n * parsed object. `'stream'` calls `streamObject` and returns the SDK's\n * streaming handle so callers can consume partial objects / text deltas.\n */\n mode?: 'generate' | 'stream'\n}\n\nexport interface RunAiAgentObjectInput<TSchema = ZodTypeAny> {\n agentId: string\n /**\n * Accepts either a bare user prompt (wrapped as `[{ role: 'user', content }]`)\n * or a prebuilt `UIMessage[]` array \u2014 matches the source spec's\n * `RunAiAgentObjectInput` contract (\u00A71149\u20131160).\n */\n input: string | UIMessage[]\n attachmentIds?: string[]\n pageContext?: AgentRequestPageContext\n /**\n * Same Phase-1 shim as {@link RunAiAgentTextInput.authContext}. Required until\n * a global request-context resolver lands (Phase 4).\n */\n authContext: AiChatRequestContext\n modelOverride?: string\n output?: RunAiAgentObjectOutputOverride<TSchema>\n debug?: boolean\n container?: AwilixContainer\n}\n\nexport type RunAiAgentObjectGenerateResult<TSchema> = {\n mode: 'generate'\n object: TSchema\n finishReason?: string\n usage?: { inputTokens?: number; outputTokens?: number }\n}\n\nexport type RunAiAgentObjectStreamResult<TSchema> = {\n mode: 'stream'\n /** Full parsed object once the stream completes. */\n object: Promise<TSchema>\n /** Async iterator of partial (progressively hydrated) objects. */\n partialObjectStream: AsyncIterable<Partial<TSchema>>\n /** Async iterator of the raw text deltas the model emitted. */\n textStream: AsyncIterable<string>\n finishReason?: Promise<string | undefined>\n usage?: Promise<{ inputTokens?: number; outputTokens?: number } | undefined>\n}\n\nexport type RunAiAgentObjectResult<TSchema> =\n | RunAiAgentObjectGenerateResult<TSchema>\n | RunAiAgentObjectStreamResult<TSchema>\n\nfunction normalizeObjectMessages(input: string | UIMessage[]): UIMessage[] {\n if (typeof input === 'string') {\n return [\n {\n id: 'user-input',\n role: 'user',\n parts: [{ type: 'text', text: input }],\n } as unknown as UIMessage,\n ]\n }\n return input\n}\n\nfunction resolveStructuredOutput<TSchema>(\n agent: AiAgentDefinition,\n override: RunAiAgentObjectOutputOverride<TSchema> | undefined,\n): { schemaName: string; schema: unknown; mode: 'generate' | 'stream' } {\n if (override) {\n return {\n schemaName: override.schemaName,\n schema: override.schema as unknown,\n mode: override.mode ?? 'generate',\n }\n }\n const declared = agent.output as AiAgentStructuredOutput | undefined\n if (!declared) {\n throw new AgentPolicyError(\n 'execution_mode_not_supported',\n `Agent \"${agent.id}\" does not declare a structured-output schema; pass runAiAgentObject({ output }) or declare agent.output.`,\n )\n }\n return {\n schemaName: declared.schemaName,\n schema: declared.schema as unknown,\n mode: declared.mode ?? 'generate',\n }\n}\n\n/**\n * Server-side helper that runs an Open Mercato agent in structured-output mode\n * via the Vercel AI SDK. Shares the same policy gate, tool resolution path,\n * system-prompt composition, and model resolution as {@link runAiAgentText} \u2014\n * object-mode and chat-mode CANNOT diverge.\n *\n * Attachment-to-model conversion (Step 3.7): resolved\n * {@link AiResolvedAttachmentPart}s are materialized inline as AI SDK v6\n * `FileUIPart` entries on the last user message (images/PDFs) and as a\n * structured `[ATTACHMENTS]` block appended to the system prompt (text\n * extracts + metadata-only summaries). Matches {@link runAiAgentText} byte-\n * for-byte so the Step 3.6 parity contract is preserved.\n */\nexport async function runAiAgentObject<TSchema = unknown>(\n input: RunAiAgentObjectInput<TSchema>,\n): Promise<RunAiAgentObjectResult<TSchema>> {\n const mutationPolicyOverride = await resolveMutationPolicyOverride(\n input.agentId,\n input.container,\n input.authContext.tenantId,\n input.authContext.organizationId,\n )\n const { agent, tools } = await resolveAiAgentTools({\n agentId: input.agentId,\n authContext: input.authContext,\n pageContext: input.pageContext,\n attachmentIds: input.attachmentIds,\n requestedExecutionMode: 'object',\n mutationPolicyOverride,\n container: input.container,\n })\n\n const resolvedOutput = resolveStructuredOutput(agent, input.output)\n\n const resolvedAttachments = await resolveAttachmentPartsForAgent({\n agent,\n attachmentIds: input.attachmentIds,\n authContext: input.authContext,\n container: input.container,\n })\n\n const baseSystemPrompt = await composeSystemPrompt(\n agent,\n input.pageContext,\n input.container,\n input.authContext.tenantId,\n input.authContext.organizationId,\n )\n const systemPrompt = appendRuntimeMutationPolicy(\n appendAttachmentSummary(baseSystemPrompt, resolvedAttachments),\n agent,\n mutationPolicyOverride,\n )\n\n const { model } = resolveAgentModel(agent, input.modelOverride)\n const normalizedMessages = ensureUiMessageShape(normalizeObjectMessages(input.input))\n const hydratedMessages = attachAttachmentsToMessages(\n normalizedMessages,\n resolvedAttachments,\n )\n const modelMessages = await convertToModelMessages(hydratedMessages)\n const stopWhen = typeof agent.maxSteps === 'number' && agent.maxSteps > 0\n ? stepCountIs(agent.maxSteps)\n : undefined\n\n if (resolvedOutput.mode === 'stream') {\n const streamArgs: Parameters<typeof streamObject>[0] = {\n model,\n system: systemPrompt,\n messages: modelMessages,\n schema: resolvedOutput.schema as never,\n schemaName: resolvedOutput.schemaName,\n }\n const result = streamObject(streamArgs) as unknown as {\n object: Promise<TSchema>\n partialObjectStream: AsyncIterable<Partial<TSchema>>\n textStream: AsyncIterable<string>\n finishReason?: Promise<string | undefined>\n usage?: Promise<{ inputTokens?: number; outputTokens?: number } | undefined>\n }\n return {\n mode: 'stream',\n object: result.object,\n partialObjectStream: result.partialObjectStream,\n textStream: result.textStream,\n finishReason: result.finishReason,\n usage: result.usage,\n }\n }\n\n const generateArgs: Parameters<typeof generateObject>[0] = {\n model,\n system: systemPrompt,\n messages: modelMessages,\n schema: resolvedOutput.schema as never,\n schemaName: resolvedOutput.schemaName,\n }\n if (stopWhen) {\n // generateObject shares `CallSettings` with generateText; stopWhen is ignored\n // by the typed surface but harmless for providers that respect it. Tools\n // flow through the system prompt only in object mode today \u2014 the whitelist\n // has already been resolved via `resolveAiAgentTools` above, even if we\n // don't hand it to generateObject.\n ;(generateArgs as Record<string, unknown>).stopWhen = stopWhen\n }\n void tools\n\n const result = await generateObject(generateArgs)\n return {\n mode: 'generate',\n object: (result as { object: unknown }).object as TSchema,\n finishReason: (result as { finishReason?: string }).finishReason,\n usage: (result as { usage?: { inputTokens?: number; outputTokens?: number } }).usage,\n }\n}\n\nexport { AgentPolicyError }\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["result"]
|
|
7
7
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { llmProviderRegistry } from "@open-mercato/shared/lib/ai/llm-provider-registry";
|
|
2
|
+
import { resolveAiProviderIdFromEnv } from "@open-mercato/shared/lib/ai/opencode-provider";
|
|
2
3
|
class AiModelFactoryError extends Error {
|
|
3
4
|
constructor(code, message) {
|
|
4
5
|
super(message);
|
|
@@ -11,19 +12,50 @@ function normalizeOverride(value) {
|
|
|
11
12
|
const trimmed = value.trim();
|
|
12
13
|
return trimmed.length > 0 ? trimmed : null;
|
|
13
14
|
}
|
|
15
|
+
function readProviderOrderFromEnv(env) {
|
|
16
|
+
const raw = normalizeOverride(env.OM_AI_PROVIDER) ?? normalizeOverride(env.OPENCODE_PROVIDER);
|
|
17
|
+
if (!raw) return void 0;
|
|
18
|
+
const resolved = resolveAiProviderIdFromEnv(env);
|
|
19
|
+
return [resolved];
|
|
20
|
+
}
|
|
21
|
+
function readGlobalModelFromEnv(env) {
|
|
22
|
+
return normalizeOverride(env.OM_AI_MODEL) ?? normalizeOverride(env.OPENCODE_MODEL);
|
|
23
|
+
}
|
|
14
24
|
function moduleEnvVarName(moduleId) {
|
|
25
|
+
return `OM_AI_${moduleId.toUpperCase()}_MODEL`;
|
|
26
|
+
}
|
|
27
|
+
function legacyModuleEnvVarName(moduleId) {
|
|
15
28
|
return `${moduleId.toUpperCase()}_AI_MODEL`;
|
|
16
29
|
}
|
|
30
|
+
function readModuleEnvOverride(env, moduleId) {
|
|
31
|
+
return normalizeOverride(env[moduleEnvVarName(moduleId)]) ?? normalizeOverride(env[legacyModuleEnvVarName(moduleId)]);
|
|
32
|
+
}
|
|
33
|
+
function parseSlashShorthand(token, registry) {
|
|
34
|
+
const slashIndex = token.indexOf("/");
|
|
35
|
+
if (slashIndex < 0) return { providerHint: null, modelId: token };
|
|
36
|
+
const before = token.slice(0, slashIndex);
|
|
37
|
+
const after = token.slice(slashIndex + 1);
|
|
38
|
+
if (!before || !after) return { providerHint: null, modelId: token };
|
|
39
|
+
if (!registry.get) return { providerHint: null, modelId: token };
|
|
40
|
+
const provider = registry.get(before);
|
|
41
|
+
if (!provider) return { providerHint: null, modelId: token };
|
|
42
|
+
return { providerHint: before, modelId: after };
|
|
43
|
+
}
|
|
17
44
|
function createModelFactory(_container, deps = {}) {
|
|
18
45
|
const registry = deps.registry ?? llmProviderRegistry;
|
|
19
46
|
const env = deps.env ?? process.env;
|
|
20
47
|
return {
|
|
21
48
|
resolveModel(input) {
|
|
22
|
-
const
|
|
49
|
+
const globalModelEnv = readGlobalModelFromEnv(env);
|
|
50
|
+
const globalModelParsed = globalModelEnv ? parseSlashShorthand(globalModelEnv, registry) : null;
|
|
51
|
+
const slashProviderHint = globalModelParsed?.providerHint ?? null;
|
|
52
|
+
const providerOrderFromEnv = readProviderOrderFromEnv(env);
|
|
53
|
+
const order = slashProviderHint ? [slashProviderHint, ...providerOrderFromEnv ?? []] : providerOrderFromEnv;
|
|
54
|
+
const provider = registry.resolveFirstConfigured({ env, order });
|
|
23
55
|
if (!provider) {
|
|
24
56
|
throw new AiModelFactoryError(
|
|
25
57
|
"no_provider_configured",
|
|
26
|
-
"No LLM provider is configured. Set OPENCODE_PROVIDER plus a matching API key such as
|
|
58
|
+
"No LLM provider is configured. Set OM_AI_PROVIDER (or the legacy OPENCODE_PROVIDER) plus a matching API key such as OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY, then restart the app. See https://docs.openmercato.com/framework/ai-assistant/overview."
|
|
27
59
|
);
|
|
28
60
|
}
|
|
29
61
|
const apiKey = provider.resolveApiKey(env);
|
|
@@ -34,8 +66,9 @@ function createModelFactory(_container, deps = {}) {
|
|
|
34
66
|
);
|
|
35
67
|
}
|
|
36
68
|
const callerOverride = normalizeOverride(input.callerOverride);
|
|
37
|
-
const moduleEnvOverride = input.moduleId && input.moduleId.length > 0 ?
|
|
69
|
+
const moduleEnvOverride = input.moduleId && input.moduleId.length > 0 ? readModuleEnvOverride(env, input.moduleId) : null;
|
|
38
70
|
const agentDefault = normalizeOverride(input.agentDefaultModel);
|
|
71
|
+
const envDefaultModel = globalModelParsed?.modelId ?? globalModelEnv;
|
|
39
72
|
let modelId;
|
|
40
73
|
let source;
|
|
41
74
|
if (callerOverride) {
|
|
@@ -47,6 +80,9 @@ function createModelFactory(_container, deps = {}) {
|
|
|
47
80
|
} else if (agentDefault) {
|
|
48
81
|
modelId = agentDefault;
|
|
49
82
|
source = "agent_default";
|
|
83
|
+
} else if (envDefaultModel) {
|
|
84
|
+
modelId = envDefaultModel;
|
|
85
|
+
source = "env_default";
|
|
50
86
|
} else {
|
|
51
87
|
modelId = provider.defaultModel;
|
|
52
88
|
source = "provider_default";
|
|
@@ -63,6 +99,7 @@ function createModelFactory(_container, deps = {}) {
|
|
|
63
99
|
}
|
|
64
100
|
export {
|
|
65
101
|
AiModelFactoryError,
|
|
66
|
-
createModelFactory
|
|
102
|
+
createModelFactory,
|
|
103
|
+
parseSlashShorthand
|
|
67
104
|
};
|
|
68
105
|
//# sourceMappingURL=model-factory.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/model-factory.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Shared AI model factory (Phase 3 WS-A \u2014 Step 5.1).\n *\n * Consolidates the previously-per-module model-creation plumbing (inbox_ops's\n * `llmProvider.ts`, the agent-runtime's inline `resolveAgentModel`) behind a\n * single DI-friendly port. Every AI-runtime caller (chat, object, inbox-ops\n * extraction, future agents) resolves the `LanguageModelV1` it hands to the\n * Vercel AI SDK through `createModelFactory(container).resolveModel(...)` so\n * all of them share one resolution order:\n *\n * 1. `callerOverride` (non-empty string) \u2014 highest precedence, e.g. the\n * `modelOverride` field on `runAiAgentText`/`runAiAgentObject`.\n * 2. Env variable `<MODULE>_AI_MODEL` (uppercased `moduleId`) when\n * `moduleId` is provided. Example: `INBOX_OPS_AI_MODEL=claude-haiku-4-5`,\n * `CATALOG_AI_MODEL=gpt-4o-mini`.\n * 3. `agentDefaultModel` \u2014 typically `AiAgentDefinition.defaultModel`.\n * 4. The configured provider's own default model id\n * (`provider.defaultModel`).\n *\n * Resolution walks the `llmProviderRegistry`'s `resolveFirstConfigured()`\n * output so it honors the same env-driven provider discovery that existing\n * callers already rely on. The factory throws {@link AiModelFactoryError}\n * when no provider is configured \u2014 every current call site already expects\n * the throw (see the bare `throw new Error('No LLM provider is configured...')`\n * in `agent-runtime.ts` prior to this Step).\n *\n * @see packages/shared/src/lib/ai/llm-provider-registry.ts\n * @see packages/ai-assistant/src/modules/ai_assistant/lib/agent-runtime.ts\n * @see packages/core/src/modules/inbox_ops/lib/llmProvider.ts\n */\n\nimport type { AwilixContainer } from 'awilix'\nimport type { EnvLookup, LlmProvider } from '@open-mercato/shared/lib/ai/llm-provider'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\n\n/**\n * Minimal AI SDK LanguageModel shape \u2014 the factory exposes the protocol-\n * agnostic `unknown`-typed return from {@link LlmProvider.createModel} under a\n * dedicated alias so callers can document intent without importing the AI SDK\n * here. Call sites that hand the result to `generateText` / `streamText` /\n * `generateObject` / `streamObject` continue to cast to the SDK's\n * `LanguageModelV1` / `LanguageModel` union exactly as they already do.\n */\nexport type AiModelInstance = unknown\n\n/**\n * Input accepted by {@link AiModelFactory.resolveModel}. All fields are\n * optional \u2014 passing an empty input resolves the provider default.\n */\nexport interface AiModelFactoryInput {\n /**\n * Owning module id (matches `Module.id`). When set, the factory checks\n * `<MODULE>_AI_MODEL` (uppercased) as the env-override source. Example:\n * `moduleId: 'inbox_ops'` \u2192 env var `INBOX_OPS_AI_MODEL`.\n */\n moduleId?: string\n /**\n * Agent-level default, typically `AiAgentDefinition.defaultModel`. Used\n * when neither `callerOverride` nor the module env override is present.\n */\n agentDefaultModel?: string\n /**\n * Per-call override (e.g. `runAiAgentText({ modelOverride })`). Wins over\n * every other source when it is a non-empty trimmed string. Empty strings\n * are treated as \"no override\" so the next source in the chain wins \u2014\n * callers MUST NOT need a separate \"clear override\" API.\n */\n callerOverride?: string\n}\n\n/**\n * Materialized output returned by {@link AiModelFactory.resolveModel}.\n */\nexport interface AiModelResolution {\n /**\n * Concrete AI SDK model instance ready to pass to\n * `generateText`/`streamText`/`generateObject`/`streamObject`. Typed as\n * {@link AiModelInstance} to avoid coupling this port to a specific SDK\n * major version.\n */\n model: AiModelInstance\n /** Resolved upstream model id (e.g. `claude-haiku-4-5-20251001`). */\n modelId: string\n /** Stable provider id from {@link LlmProvider.id}. */\n providerId: string\n /**\n * Which source won resolution. Useful for logs and tests; never exposed\n * as a public contract beyond these four enum values.\n */\n source: 'caller_override' | 'module_env' | 'agent_default' | 'provider_default'\n}\n\n/**\n * Port exposed by {@link createModelFactory}. Stateless \u2014 the factory\n * re-reads the registry + env on every `resolveModel` call so hot-reload\n * and test overrides work without needing factory re-creation.\n */\nexport interface AiModelFactory {\n resolveModel(input: AiModelFactoryInput): AiModelResolution\n}\n\n/**\n * Typed error thrown by the factory when it cannot materialize a model.\n *\n * `code` is a stable string union so downstream callers can branch without\n * parsing error messages. `AiModelFactoryError`s bubble through\n * `runAiAgentText`/`runAiAgentObject` unchanged \u2014 the agent runtime does\n * NOT catch them, matching the pre-Step-5.1 behavior of the inline\n * resolver.\n */\nexport type AiModelFactoryErrorCode =\n | 'no_provider_configured'\n | 'api_key_missing'\n\nexport class AiModelFactoryError extends Error {\n readonly code: AiModelFactoryErrorCode\n\n constructor(code: AiModelFactoryErrorCode, message: string) {\n super(message)\n this.name = 'AiModelFactoryError'\n this.code = code\n }\n}\n\n/**\n * Internal dependencies of the factory. Exposed for tests only; production\n * callers rely on the defaults wired by {@link createModelFactory}.\n */\nexport interface CreateModelFactoryDependencies {\n /**\n * Registry used to resolve the first configured provider. Defaults to the\n * singleton `llmProviderRegistry`.\n */\n registry?: { resolveFirstConfigured: (options?: { env?: EnvLookup }) => LlmProvider | null }\n /** Env lookup for `<MODULE>_AI_MODEL` + provider credentials. */\n env?: EnvLookup\n}\n\nfunction normalizeOverride(value: string | undefined): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction moduleEnvVarName(moduleId: string): string {\n return `${moduleId.toUpperCase()}_AI_MODEL`\n}\n\n/**\n * Creates an {@link AiModelFactory} bound to the DI container. The container\n * reference is accepted for API symmetry with other runtime helpers (and so\n * future work can read provider overrides registered on the container); the\n * current implementation only needs the registry + env. No breaking change\n * when later implementations DO consult the container.\n */\nexport function createModelFactory(\n _container: AwilixContainer,\n deps: CreateModelFactoryDependencies = {},\n): AiModelFactory {\n const registry = deps.registry ?? llmProviderRegistry\n const env = deps.env ?? process.env\n\n return {\n resolveModel(input: AiModelFactoryInput): AiModelResolution {\n const provider = registry.resolveFirstConfigured({ env })\n if (!provider) {\n throw new AiModelFactoryError(\n 'no_provider_configured',\n 'No LLM provider is configured. Set OPENCODE_PROVIDER plus a matching API key such as ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY, then restart the app. See https://docs.openmercato.com/framework/ai-assistant/overview.',\n )\n }\n const apiKey = provider.resolveApiKey(env)\n if (!apiKey) {\n throw new AiModelFactoryError(\n 'api_key_missing',\n `LLM provider \"${provider.id}\" is advertised as configured but resolveApiKey() returned empty.`,\n )\n }\n\n const callerOverride = normalizeOverride(input.callerOverride)\n const moduleEnvOverride =\n input.moduleId && input.moduleId.length > 0\n ? normalizeOverride(env[moduleEnvVarName(input.moduleId)])\n : null\n const agentDefault = normalizeOverride(input.agentDefaultModel)\n\n let modelId: string\n let source: AiModelResolution['source']\n if (callerOverride) {\n modelId = callerOverride\n source = 'caller_override'\n } else if (moduleEnvOverride) {\n modelId = moduleEnvOverride\n source = 'module_env'\n } else if (agentDefault) {\n modelId = agentDefault\n source = 'agent_default'\n } else {\n modelId = provider.defaultModel\n source = 'provider_default'\n }\n\n const model = provider.createModel({ modelId, apiKey })\n return {\n model,\n modelId,\n providerId: provider.id,\n source,\n }\n },\n }\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * Shared AI model factory.\n *\n * Consolidates the previously-per-module model-creation plumbing (inbox_ops's\n * `llmProvider.ts`, the agent-runtime's inline `resolveAgentModel`) behind a\n * single DI-friendly port. Every AI-runtime caller (chat, object, inbox-ops\n * extraction, future agents) resolves the `LanguageModelV1` it hands to the\n * Vercel AI SDK through `createModelFactory(container).resolveModel(...)` so\n * all of them share one resolution order:\n *\n * 1. `callerOverride` (non-empty string) \u2014 highest precedence, e.g. the\n * `modelOverride` field on `runAiAgentText`/`runAiAgentObject`.\n * 2. Env variable `OM_AI_<MODULE>_MODEL` (uppercased `moduleId`) when\n * `moduleId` is provided. Example:\n * `OM_AI_INBOX_OPS_MODEL=claude-haiku-4-5`,\n * `OM_AI_CATALOG_MODEL=gpt-4o-mini`. The legacy\n * `<MODULE>_AI_MODEL` form (e.g. `INBOX_OPS_AI_MODEL`) is read as a\n * backward-compatibility fallback when the canonical name is unset.\n * 3. `agentDefaultModel` \u2014 typically `AiAgentDefinition.defaultModel`.\n * 4. Global env `OM_AI_MODEL` (canonical) with `OPENCODE_MODEL` kept as\n * a backward-compatibility fallback. Accepts either a plain model id\n * (`gpt-5-mini`) or a slash-qualified id (`openai/gpt-5-mini`).\n * Slash qualifiers consume the provider axis at the same step \u2014 a\n * higher-priority provider source still wins, but a lower-priority\n * one cannot overwrite a slash-qualified model.\n * 5. The configured provider's own default model id\n * (`provider.defaultModel`).\n *\n * Resolution walks the `llmProviderRegistry`'s `resolveFirstConfigured()`\n * output. The walk's `order` argument is seeded from (in priority order):\n *\n * 1. The slash-qualified provider hint extracted from `OM_AI_MODEL` \u2014\n * consumes the provider axis for this resolution.\n * 2. `OM_AI_PROVIDER` (canonical) with `OPENCODE_PROVIDER` as a\n * backward-compatibility fallback \u2014 names a registered provider id;\n * falls through transparently when the named provider is\n * registered-but-unconfigured.\n *\n * The factory throws {@link AiModelFactoryError} when no provider is\n * configured \u2014 every current call site already expects the throw (see the\n * bare `throw new Error('No LLM provider is configured...')` in\n * `agent-runtime.ts` prior to the consolidation).\n *\n * @see packages/shared/src/lib/ai/llm-provider-registry.ts\n * @see packages/ai-assistant/src/modules/ai_assistant/lib/agent-runtime.ts\n * @see packages/core/src/modules/inbox_ops/lib/llmProvider.ts\n */\n\nimport type { AwilixContainer } from 'awilix'\nimport type { EnvLookup, LlmProvider } from '@open-mercato/shared/lib/ai/llm-provider'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { resolveAiProviderIdFromEnv } from '@open-mercato/shared/lib/ai/opencode-provider'\n\n/**\n * Minimal AI SDK LanguageModel shape \u2014 the factory exposes the protocol-\n * agnostic `unknown`-typed return from {@link LlmProvider.createModel} under a\n * dedicated alias so callers can document intent without importing the AI SDK\n * here. Call sites that hand the result to `generateText` / `streamText` /\n * `generateObject` / `streamObject` continue to cast to the SDK's\n * `LanguageModelV1` / `LanguageModel` union exactly as they already do.\n */\nexport type AiModelInstance = unknown\n\n/**\n * Input accepted by {@link AiModelFactory.resolveModel}. All fields are\n * optional \u2014 passing an empty input resolves the provider default.\n */\nexport interface AiModelFactoryInput {\n /**\n * Owning module id (matches `Module.id`). When set, the factory checks\n * `OM_AI_<MODULE>_MODEL` (uppercased) as the env-override source, with\n * the legacy `<MODULE>_AI_MODEL` form honored as a backward-compatibility\n * fallback. Example: `moduleId: 'inbox_ops'` \u2192 canonical env var\n * `OM_AI_INBOX_OPS_MODEL` (legacy `INBOX_OPS_AI_MODEL`).\n */\n moduleId?: string\n /**\n * Agent-level default, typically `AiAgentDefinition.defaultModel`. Used\n * when neither `callerOverride` nor the module env override is present.\n */\n agentDefaultModel?: string\n /**\n * Per-call override (e.g. `runAiAgentText({ modelOverride })`). Wins over\n * every other source when it is a non-empty trimmed string. Empty strings\n * are treated as \"no override\" so the next source in the chain wins \u2014\n * callers MUST NOT need a separate \"clear override\" API.\n */\n callerOverride?: string\n}\n\n/**\n * Materialized output returned by {@link AiModelFactory.resolveModel}.\n */\nexport interface AiModelResolution {\n /**\n * Concrete AI SDK model instance ready to pass to\n * `generateText`/`streamText`/`generateObject`/`streamObject`. Typed as\n * {@link AiModelInstance} to avoid coupling this port to a specific SDK\n * major version.\n */\n model: AiModelInstance\n /** Resolved upstream model id (e.g. `claude-haiku-4-5-20251001`). */\n modelId: string\n /** Stable provider id from {@link LlmProvider.id}. */\n providerId: string\n /**\n * Which source won resolution. Useful for logs and tests; never exposed\n * as a public contract beyond these enum values.\n *\n * - `env_default` indicates `OM_AI_MODEL` (preferred) or the legacy\n * `OPENCODE_MODEL` fallback supplied the model id.\n */\n source:\n | 'caller_override'\n | 'module_env'\n | 'agent_default'\n | 'env_default'\n | 'provider_default'\n}\n\n/**\n * Port exposed by {@link createModelFactory}. Stateless \u2014 the factory\n * re-reads the registry + env on every `resolveModel` call so hot-reload\n * and test overrides work without needing factory re-creation.\n */\nexport interface AiModelFactory {\n resolveModel(input: AiModelFactoryInput): AiModelResolution\n}\n\n/**\n * Typed error thrown by the factory when it cannot materialize a model.\n *\n * `code` is a stable string union so downstream callers can branch without\n * parsing error messages. `AiModelFactoryError`s bubble through\n * `runAiAgentText`/`runAiAgentObject` unchanged \u2014 the agent runtime does\n * NOT catch them, matching the pre-consolidation behavior of the inline\n * resolver.\n */\nexport type AiModelFactoryErrorCode =\n | 'no_provider_configured'\n | 'api_key_missing'\n\nexport class AiModelFactoryError extends Error {\n readonly code: AiModelFactoryErrorCode\n\n constructor(code: AiModelFactoryErrorCode, message: string) {\n super(message)\n this.name = 'AiModelFactoryError'\n this.code = code\n }\n}\n\n/**\n * Subset of {@link import('@open-mercato/shared/lib/ai/llm-provider-registry').LlmProviderRegistry}\n * the factory consumes. Defined locally so test doubles only need to mock\n * the methods the factory actually calls.\n */\nexport interface AiModelFactoryRegistry {\n resolveFirstConfigured(options?: {\n env?: EnvLookup\n order?: readonly string[]\n }): LlmProvider | null\n /**\n * Optional registry lookup used by the slash-shorthand parser to validate\n * a provider hint. When absent, slash parsing is disabled and the entire\n * model token is treated as a model id (mirrors the pre-Phase-0\n * behavior).\n */\n get?(id: string): LlmProvider | null\n}\n\n/**\n * Internal dependencies of the factory. Exposed for tests only; production\n * callers rely on the defaults wired by {@link createModelFactory}.\n */\nexport interface CreateModelFactoryDependencies {\n /**\n * Registry used to resolve the first configured provider. Defaults to the\n * singleton `llmProviderRegistry`. Implementations MAY honor the optional\n * `order` argument to prefer the operator-selected provider.\n */\n registry?: AiModelFactoryRegistry\n /** Env lookup for `<MODULE>_AI_MODEL` + provider credentials. */\n env?: EnvLookup\n}\n\nfunction normalizeOverride(value: string | undefined): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\n/**\n * Reads the operator-selected provider id from the unified env vars.\n * Returns `null` when neither `OM_AI_PROVIDER` nor the legacy\n * `OPENCODE_PROVIDER` resolves to a known provider \u2014 in that case the\n * registry falls back to its default registration walk.\n */\nfunction readProviderOrderFromEnv(env: EnvLookup): readonly string[] | undefined {\n const raw = normalizeOverride(env.OM_AI_PROVIDER) ?? normalizeOverride(env.OPENCODE_PROVIDER)\n if (!raw) return undefined\n // Reuse the shared resolver so unknown ids fall back through both keys.\n // When the raw value is unknown the resolver returns the default; passing\n // that through as an explicit hint is still safe because the registry\n // only honors registered + configured providers.\n const resolved = resolveAiProviderIdFromEnv(env)\n return [resolved]\n}\n\n/**\n * Reads the global model hint from the unified env vars. `OM_AI_MODEL`\n * wins over the legacy `OPENCODE_MODEL`.\n */\nfunction readGlobalModelFromEnv(env: EnvLookup): string | null {\n return normalizeOverride(env.OM_AI_MODEL) ?? normalizeOverride(env.OPENCODE_MODEL)\n}\n\n/** Canonical per-module model env. Example: `OM_AI_INBOX_OPS_MODEL`. */\nfunction moduleEnvVarName(moduleId: string): string {\n return `OM_AI_${moduleId.toUpperCase()}_MODEL`\n}\n\n/**\n * Legacy per-module model env (pre-OM_AI_* rename). Example:\n * `INBOX_OPS_AI_MODEL`. Read as a backward-compatibility fallback only.\n */\nfunction legacyModuleEnvVarName(moduleId: string): string {\n return `${moduleId.toUpperCase()}_AI_MODEL`\n}\n\nfunction readModuleEnvOverride(env: EnvLookup, moduleId: string): string | null {\n return (\n normalizeOverride(env[moduleEnvVarName(moduleId)]) ??\n normalizeOverride(env[legacyModuleEnvVarName(moduleId)])\n )\n}\n\n/**\n * Splits a slash-qualified model token (e.g. `openai/gpt-5-mini`) into\n * `{ providerHint, modelId }` when the prefix matches a registered provider\n * id, otherwise returns the entire token as the model id and a null hint.\n *\n * The registry-membership guard avoids mis-splitting model ids that already\n * contain slashes (DeepInfra: `meta-llama/Llama-3.3-70B-Instruct-Turbo`,\n * `zai-org/GLM-5.1`). When the registry does not expose `get`, slash\n * parsing is disabled \u2014 callers without a configured registry behave as if\n * the entire token were a plain model id.\n *\n * Exported for test coverage; production callers go through\n * {@link createModelFactory}.\n */\nexport function parseSlashShorthand(\n token: string,\n registry: Pick<AiModelFactoryRegistry, 'get'>,\n): { providerHint: string | null; modelId: string } {\n const slashIndex = token.indexOf('/')\n if (slashIndex < 0) return { providerHint: null, modelId: token }\n const before = token.slice(0, slashIndex)\n const after = token.slice(slashIndex + 1)\n if (!before || !after) return { providerHint: null, modelId: token }\n if (!registry.get) return { providerHint: null, modelId: token }\n const provider = registry.get(before)\n if (!provider) return { providerHint: null, modelId: token }\n return { providerHint: before, modelId: after }\n}\n\n/**\n * Creates an {@link AiModelFactory} bound to the DI container. The container\n * reference is accepted for API symmetry with other runtime helpers (and so\n * future work can read provider overrides registered on the container); the\n * current implementation only needs the registry + env. No breaking change\n * when later implementations DO consult the container.\n */\nexport function createModelFactory(\n _container: AwilixContainer,\n deps: CreateModelFactoryDependencies = {},\n): AiModelFactory {\n const registry: AiModelFactoryRegistry = deps.registry ?? llmProviderRegistry\n const env = deps.env ?? process.env\n\n return {\n resolveModel(input: AiModelFactoryInput): AiModelResolution {\n // OM_AI_MODEL is canonical; the legacy OPENCODE_MODEL is read as a\n // backward-compatibility fallback through readGlobalModelFromEnv.\n const globalModelEnv = readGlobalModelFromEnv(env)\n // Slash-qualified env-model values consume the provider axis at the\n // env-default step. Phase 1 of the per-axis-overrides spec generalizes\n // the parser to every model-axis source.\n const globalModelParsed = globalModelEnv\n ? parseSlashShorthand(globalModelEnv, registry)\n : null\n const slashProviderHint = globalModelParsed?.providerHint ?? null\n const providerOrderFromEnv = readProviderOrderFromEnv(env)\n const order = slashProviderHint\n ? [slashProviderHint, ...(providerOrderFromEnv ?? [])]\n : providerOrderFromEnv\n\n const provider = registry.resolveFirstConfigured({ env, order })\n if (!provider) {\n throw new AiModelFactoryError(\n 'no_provider_configured',\n 'No LLM provider is configured. Set OM_AI_PROVIDER (or the legacy OPENCODE_PROVIDER) plus a matching API key such as OPENAI_API_KEY, ANTHROPIC_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY, then restart the app. See https://docs.openmercato.com/framework/ai-assistant/overview.',\n )\n }\n const apiKey = provider.resolveApiKey(env)\n if (!apiKey) {\n throw new AiModelFactoryError(\n 'api_key_missing',\n `LLM provider \"${provider.id}\" is advertised as configured but resolveApiKey() returned empty.`,\n )\n }\n\n const callerOverride = normalizeOverride(input.callerOverride)\n const moduleEnvOverride =\n input.moduleId && input.moduleId.length > 0\n ? readModuleEnvOverride(env, input.moduleId)\n : null\n const agentDefault = normalizeOverride(input.agentDefaultModel)\n // The slash parser already split the global model token; use the\n // post-parse model id so `OM_AI_MODEL=openai/gpt-5-mini` resolves\n // model `gpt-5-mini` against provider `openai`.\n const envDefaultModel = globalModelParsed?.modelId ?? globalModelEnv\n\n let modelId: string\n let source: AiModelResolution['source']\n if (callerOverride) {\n modelId = callerOverride\n source = 'caller_override'\n } else if (moduleEnvOverride) {\n modelId = moduleEnvOverride\n source = 'module_env'\n } else if (agentDefault) {\n modelId = agentDefault\n source = 'agent_default'\n } else if (envDefaultModel) {\n modelId = envDefaultModel\n source = 'env_default'\n } else {\n modelId = provider.defaultModel\n source = 'provider_default'\n }\n\n const model = provider.createModel({ modelId, apiKey })\n return {\n model,\n modelId,\n providerId: provider.id,\n source,\n }\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|