@open-mercato/ai-assistant 0.7.1-develop.7123.1.25365e718d → 0.7.1-develop.7130.1.fef2396fd8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +1 -1
  3. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +1 -1
  4. package/dist/modules/ai_assistant/api/ai/agents/route.js +1 -1
  5. package/dist/modules/ai_assistant/api/ai/agents/route.js.map +1 -1
  6. package/dist/modules/ai_assistant/api/ai/chat/route.js +1 -1
  7. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +1 -1
  8. package/dist/modules/ai_assistant/api/route/route.js +1 -1
  9. package/dist/modules/ai_assistant/api/route/route.js.map +1 -1
  10. package/dist/modules/ai_assistant/api/settings/allowlist/route.js +1 -1
  11. package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +1 -1
  12. package/dist/modules/ai_assistant/api/settings/route.js +1 -1
  13. package/dist/modules/ai_assistant/api/settings/route.js.map +1 -1
  14. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +1 -1
  15. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +1 -1
  16. package/dist/modules/ai_assistant/lib/agent-registry.js +1 -1
  17. package/dist/modules/ai_assistant/lib/agent-registry.js.map +1 -1
  18. package/dist/modules/ai_assistant/lib/agent-runtime.js +1 -2
  19. package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
  20. package/dist/modules/ai_assistant/lib/chat-config.js +1 -2
  21. package/dist/modules/ai_assistant/lib/chat-config.js.map +2 -2
  22. package/dist/modules/ai_assistant/lib/llm-registry.js +8 -0
  23. package/dist/modules/ai_assistant/lib/llm-registry.js.map +7 -0
  24. package/dist/modules/ai_assistant/lib/model-factory.js +1 -1
  25. package/dist/modules/ai_assistant/lib/model-factory.js.map +1 -1
  26. package/package.json +6 -6
  27. package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +1 -1
  28. package/src/modules/ai_assistant/api/ai/agents/__tests__/route.test.ts +69 -0
  29. package/src/modules/ai_assistant/api/ai/agents/route.ts +1 -1
  30. package/src/modules/ai_assistant/api/ai/chat/route.ts +1 -1
  31. package/src/modules/ai_assistant/api/route/route.ts +1 -1
  32. package/src/modules/ai_assistant/api/settings/allowlist/route.ts +1 -1
  33. package/src/modules/ai_assistant/api/settings/route.ts +1 -1
  34. package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +1 -1
  35. package/src/modules/ai_assistant/lib/__tests__/llm-registry-imports.test.ts +42 -0
  36. package/src/modules/ai_assistant/lib/agent-registry.ts +1 -1
  37. package/src/modules/ai_assistant/lib/agent-runtime.ts +1 -5
  38. package/src/modules/ai_assistant/lib/chat-config.ts +1 -4
  39. package/src/modules/ai_assistant/lib/llm-registry.ts +27 -0
  40. package/src/modules/ai_assistant/lib/model-factory.ts +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/chat-config.ts"],
4
- "sourcesContent": ["import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport type { LlmProvider } from '@open-mercato/shared/lib/ai/llm-provider'\n// Side-effect: ensures the registry is populated with built-in adapters\n// and OpenAI-compatible presets before this module's getters run.\nimport './llm-bootstrap'\n\n// Types\n//\n// `ChatProviderId` was previously a narrow literal union of three ids\n// (`'anthropic' | 'openai' | 'google'`). After the ports & adapters\n// refactor the registry accepts any stable id string, so this type\n// becomes `string`. Backward-compatibility note: downstream callers that\n// used exhaustive switches on the old union must add a `default:` branch.\n// See `.ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md`.\nexport type ChatProviderId = string\n\nexport type ChatModelInfo = {\n id: string\n name: string\n contextWindow: number\n}\n\nexport type ChatProviderInfo = {\n name: string\n envKeyRequired: string\n defaultModel: string\n models: ChatModelInfo[]\n}\n\nexport type ChatProviderConfig = {\n providerId: ChatProviderId\n model: string\n updatedAt: string\n}\n\n// Constants\nexport const CHAT_CONFIG_KEY = 'chat_provider'\n\nfunction providerToChatInfo(provider: LlmProvider): ChatProviderInfo {\n return {\n name: provider.name,\n envKeyRequired: provider.envKeys[0],\n defaultModel: provider.defaultModel,\n models: provider.defaultModels.map((m) => ({\n id: m.id,\n name: m.name,\n contextWindow: m.contextWindow,\n })),\n }\n}\n\n/**\n * `CHAT_PROVIDERS` is a dynamic getter that returns all providers\n * registered with `llmProviderRegistry`. The shape\n * (`Record<string, ChatProviderInfo>`) is preserved so existing code that\n * indexed the map with a string literal (`CHAT_PROVIDERS['anthropic']`)\n * keeps working \u2014 it is now a runtime lookup against the registry.\n */\nexport const CHAT_PROVIDERS: Record<string, ChatProviderInfo> = new Proxy(\n {} as Record<string, ChatProviderInfo>,\n {\n get(_target, prop: string): ChatProviderInfo | undefined {\n if (typeof prop !== 'string') return undefined\n const provider = llmProviderRegistry.get(prop)\n return provider ? providerToChatInfo(provider) : undefined\n },\n has(_target, prop: string): boolean {\n if (typeof prop !== 'string') return false\n return llmProviderRegistry.get(prop) !== null\n },\n ownKeys(): string[] {\n return llmProviderRegistry.list().map((p) => p.id)\n },\n getOwnPropertyDescriptor(_target, prop: string): PropertyDescriptor | undefined {\n if (typeof prop !== 'string') return undefined\n const provider = llmProviderRegistry.get(prop)\n if (!provider) return undefined\n return {\n enumerable: true,\n configurable: true,\n value: providerToChatInfo(provider),\n }\n },\n },\n)\n\nexport const DEFAULT_CHAT_CONFIG: Omit<ChatProviderConfig, 'updatedAt'> = {\n providerId: 'openai',\n get model(): string {\n // Lazy resolution so the bootstrap has a chance to register providers\n // before the default is computed.\n const provider = llmProviderRegistry.get('openai')\n return provider?.defaultModel ?? 'gpt-5-mini'\n },\n}\n\n// Provider configuration checks\nexport function isProviderConfigured(providerId: ChatProviderId): boolean {\n const provider = llmProviderRegistry.get(providerId)\n return provider?.isConfigured() ?? false\n}\n\nexport function getConfiguredProviders(): ChatProviderId[] {\n return llmProviderRegistry\n .listConfigured()\n .map((p) => p.id)\n}\n\n// Config resolution\ntype Resolver = {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport async function resolveChatConfig(\n resolver: Resolver,\n options?: { defaultValue?: ChatProviderConfig | null }\n): Promise<ChatProviderConfig | null> {\n const fallback = options?.defaultValue ?? null\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n return fallback\n }\n try {\n const value = await service.getValue<ChatProviderConfig>('ai_assistant', CHAT_CONFIG_KEY, { defaultValue: fallback })\n return value\n } catch {\n return fallback\n }\n}\n\nexport async function saveChatConfig(\n resolver: Resolver,\n config: Omit<ChatProviderConfig, 'updatedAt'>\n): Promise<ChatProviderConfig> {\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n throw new Error('Configuration service unavailable')\n }\n const fullConfig: ChatProviderConfig = {\n ...config,\n updatedAt: new Date().toISOString(),\n }\n await service.setValue('ai_assistant', CHAT_CONFIG_KEY, fullConfig)\n return fullConfig\n}\n\nexport function createDefaultConfig(): ChatProviderConfig {\n return {\n providerId: DEFAULT_CHAT_CONFIG.providerId,\n model: DEFAULT_CHAT_CONFIG.model,\n updatedAt: new Date().toISOString(),\n }\n}\n\n// Get model info by ID\nexport function getModelInfo(providerId: ChatProviderId, modelId: string): ChatModelInfo | null {\n const provider = CHAT_PROVIDERS[providerId]\n if (!provider) return null\n return provider.models.find((m) => m.id === modelId) ?? null\n}\n\n// Format context window for display\nexport function formatContextWindow(contextWindow: number): string {\n if (contextWindow >= 1000000) {\n return `${(contextWindow / 1000000).toFixed(1)}M`\n }\n return `${(contextWindow / 1000).toFixed(0)}K`\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { llmProviderRegistry } from './llm-registry'\nimport type { LlmProvider } from '@open-mercato/shared/lib/ai/llm-provider'\n\n// Types\n//\n// `ChatProviderId` was previously a narrow literal union of three ids\n// (`'anthropic' | 'openai' | 'google'`). After the ports & adapters\n// refactor the registry accepts any stable id string, so this type\n// becomes `string`. Backward-compatibility note: downstream callers that\n// used exhaustive switches on the old union must add a `default:` branch.\n// See `.ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md`.\nexport type ChatProviderId = string\n\nexport type ChatModelInfo = {\n id: string\n name: string\n contextWindow: number\n}\n\nexport type ChatProviderInfo = {\n name: string\n envKeyRequired: string\n defaultModel: string\n models: ChatModelInfo[]\n}\n\nexport type ChatProviderConfig = {\n providerId: ChatProviderId\n model: string\n updatedAt: string\n}\n\n// Constants\nexport const CHAT_CONFIG_KEY = 'chat_provider'\n\nfunction providerToChatInfo(provider: LlmProvider): ChatProviderInfo {\n return {\n name: provider.name,\n envKeyRequired: provider.envKeys[0],\n defaultModel: provider.defaultModel,\n models: provider.defaultModels.map((m) => ({\n id: m.id,\n name: m.name,\n contextWindow: m.contextWindow,\n })),\n }\n}\n\n/**\n * `CHAT_PROVIDERS` is a dynamic getter that returns all providers\n * registered with `llmProviderRegistry`. The shape\n * (`Record<string, ChatProviderInfo>`) is preserved so existing code that\n * indexed the map with a string literal (`CHAT_PROVIDERS['anthropic']`)\n * keeps working \u2014 it is now a runtime lookup against the registry.\n */\nexport const CHAT_PROVIDERS: Record<string, ChatProviderInfo> = new Proxy(\n {} as Record<string, ChatProviderInfo>,\n {\n get(_target, prop: string): ChatProviderInfo | undefined {\n if (typeof prop !== 'string') return undefined\n const provider = llmProviderRegistry.get(prop)\n return provider ? providerToChatInfo(provider) : undefined\n },\n has(_target, prop: string): boolean {\n if (typeof prop !== 'string') return false\n return llmProviderRegistry.get(prop) !== null\n },\n ownKeys(): string[] {\n return llmProviderRegistry.list().map((p) => p.id)\n },\n getOwnPropertyDescriptor(_target, prop: string): PropertyDescriptor | undefined {\n if (typeof prop !== 'string') return undefined\n const provider = llmProviderRegistry.get(prop)\n if (!provider) return undefined\n return {\n enumerable: true,\n configurable: true,\n value: providerToChatInfo(provider),\n }\n },\n },\n)\n\nexport const DEFAULT_CHAT_CONFIG: Omit<ChatProviderConfig, 'updatedAt'> = {\n providerId: 'openai',\n get model(): string {\n // Lazy resolution so the bootstrap has a chance to register providers\n // before the default is computed.\n const provider = llmProviderRegistry.get('openai')\n return provider?.defaultModel ?? 'gpt-5-mini'\n },\n}\n\n// Provider configuration checks\nexport function isProviderConfigured(providerId: ChatProviderId): boolean {\n const provider = llmProviderRegistry.get(providerId)\n return provider?.isConfigured() ?? false\n}\n\nexport function getConfiguredProviders(): ChatProviderId[] {\n return llmProviderRegistry\n .listConfigured()\n .map((p) => p.id)\n}\n\n// Config resolution\ntype Resolver = {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport async function resolveChatConfig(\n resolver: Resolver,\n options?: { defaultValue?: ChatProviderConfig | null }\n): Promise<ChatProviderConfig | null> {\n const fallback = options?.defaultValue ?? null\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n return fallback\n }\n try {\n const value = await service.getValue<ChatProviderConfig>('ai_assistant', CHAT_CONFIG_KEY, { defaultValue: fallback })\n return value\n } catch {\n return fallback\n }\n}\n\nexport async function saveChatConfig(\n resolver: Resolver,\n config: Omit<ChatProviderConfig, 'updatedAt'>\n): Promise<ChatProviderConfig> {\n let service: ModuleConfigService\n try {\n service = resolver.resolve<ModuleConfigService>('moduleConfigService')\n } catch {\n throw new Error('Configuration service unavailable')\n }\n const fullConfig: ChatProviderConfig = {\n ...config,\n updatedAt: new Date().toISOString(),\n }\n await service.setValue('ai_assistant', CHAT_CONFIG_KEY, fullConfig)\n return fullConfig\n}\n\nexport function createDefaultConfig(): ChatProviderConfig {\n return {\n providerId: DEFAULT_CHAT_CONFIG.providerId,\n model: DEFAULT_CHAT_CONFIG.model,\n updatedAt: new Date().toISOString(),\n }\n}\n\n// Get model info by ID\nexport function getModelInfo(providerId: ChatProviderId, modelId: string): ChatModelInfo | null {\n const provider = CHAT_PROVIDERS[providerId]\n if (!provider) return null\n return provider.models.find((m) => m.id === modelId) ?? null\n}\n\n// Format context window for display\nexport function formatContextWindow(contextWindow: number): string {\n if (contextWindow >= 1000000) {\n return `${(contextWindow / 1000000).toFixed(1)}M`\n }\n return `${(contextWindow / 1000).toFixed(0)}K`\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,8 @@
1
+ import "./llm-bootstrap.js";
2
+ import {
3
+ llmProviderRegistry
4
+ } from "@open-mercato/shared/lib/ai/llm-provider-registry";
5
+ export {
6
+ llmProviderRegistry
7
+ };
8
+ //# sourceMappingURL=llm-registry.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/lib/llm-registry.ts"],
4
+ "sourcesContent": ["/**\n * Bootstrapped access to the shared `llmProviderRegistry` singleton.\n *\n * The registry in `@open-mercato/shared` is a plain module-level singleton\n * with no self-registration: built-in adapters and OpenAI-compatible presets\n * only exist after `./llm-bootstrap` has been evaluated. A module that reads\n * the shared singleton directly therefore sees an empty registry whenever it\n * happens to be the first thing a cold process loads, and\n * `resolveFirstConfigured()` reports \"no provider configured\" even though the\n * environment carries valid credentials.\n *\n * Every module inside `ai_assistant` imports the registry from here so that\n * bootstrap is guaranteed by the import graph rather than by which route the\n * user happens to hit first. `lib/__tests__/llm-registry-imports.test.ts`\n * enforces that rule.\n *\n * @see ./llm-bootstrap.ts\n * @see packages/shared/src/lib/ai/llm-provider-registry.ts\n */\n\nimport './llm-bootstrap'\n\nexport {\n llmProviderRegistry,\n type LlmProviderRegistry,\n type ResolveFirstConfiguredOptions,\n} from '@open-mercato/shared/lib/ai/llm-provider-registry'\n"],
5
+ "mappings": "AAoBA,OAAO;AAEP;AAAA,EACE;AAAA,OAGK;",
6
+ "names": []
7
+ }
@@ -1,5 +1,5 @@
1
1
  import { createLogger } from "@open-mercato/shared/lib/logger";
2
- import { llmProviderRegistry } from "@open-mercato/shared/lib/ai/llm-provider-registry";
2
+ import { llmProviderRegistry } from "./llm-registry.js";
3
3
  import {
4
4
  intersectAllowlists,
5
5
  canonicalProviderId,
@@ -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.\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 * Accepts a slash-qualified `<provider>/<model>` shorthand (Phase 1).\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 * Accepts a slash-qualified shorthand (Phase 1).\n * 3. `agentDefaultModel` \u2014 typically `AiAgentDefinition.defaultModel`.\n * Accepts a slash-qualified `<provider>/<model>` shorthand (Phase 1).\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. When the\n * selected model source carries a provider hint, the provider/model pair\n * is atomic: an unconfigured hinted provider fails instead of sending\n * the model id to a different configured provider.\n * 5. The configured provider's own default model id\n * (`provider.defaultModel`).\n *\n * Every model-axis source is parsed through {@link parseSlashShorthand}.\n * Resolution walks the chain top-down and takes the first non-null hint as\n * the registry-walk seed. If the winning model source also supplies the\n * winning provider hint \u2014 either through `<provider>/<model>` or through the\n * same-source provider field/env var \u2014 that pair is resolved exactly and\n * never mixed with a fallback provider.\n *\n * Provider-axis seed order (highest priority first):\n * 1. Slash-prefix from `callerOverride` (Phase 1).\n * 2. `providerOverride` \u2014 request-time provider override (Phase 1).\n * 3. Slash-prefix from `OM_AI_<MODULE>_MODEL` (legacy `<MODULE>_AI_MODEL`) (Phase 1).\n * 4. `OM_AI_<MODULE>_PROVIDER` env (legacy `<MODULE>_AI_PROVIDER`) (Phase 1).\n * 5. Slash-prefix from `agentDefaultModel` (Phase 1).\n * 6. `agentDefaultProvider` \u2014 `AiAgentDefinition.defaultProvider` (Phase 1).\n * 7. Slash-prefix from `OM_AI_MODEL` (legacy `OPENCODE_MODEL`) (Phase 0).\n * 8. `OM_AI_PROVIDER` (legacy `OPENCODE_PROVIDER`) (Phase 0).\n *\n * The `OM_AI_*` env knobs are canonical; the legacy `OPENCODE_PROVIDER` /\n * `OPENCODE_MODEL` envs stay bound to the OpenCode Code Mode stack and are\n * also honored as backward-compatibility fallbacks here.\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 { createLogger } from '@open-mercato/shared/lib/logger'\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 {\n intersectAllowlists,\n canonicalProviderId,\n isModelAllowedForProviderInEffective,\n isProviderAllowedInEffective,\n providerIdAliases,\n type EffectiveAllowlist,\n type TenantAllowlistSnapshot,\n} from './model-allowlist'\n\nconst logger = createLogger('ai_assistant')\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 * Also enables the `OM_AI_<MODULE>_PROVIDER` env axis (legacy\n * `<MODULE>_AI_PROVIDER` honored as a backward-compatibility fallback).\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 * Accepts a slash-qualified `<provider>/<model>` shorthand (Phase 1).\n */\n agentDefaultModel?: string\n /**\n * Agent-level default provider, typically `AiAgentDefinition.defaultProvider`.\n * Named provider id. When paired with an agent default model, the pair is\n * resolved exactly and fails if the provider is unconfigured. When used as\n * a provider preference without an agent default model, it can fall through\n * to the next configured provider. Sits between `OM_AI_<MODULE>_PROVIDER`\n * and the global `OM_AI_PROVIDER` in the provider-axis seed list above.\n *\n * Phase 1 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n agentDefaultProvider?: 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 * Request-time provider override \u2014 wins for the provider axis at the same\n * priority as `callerOverride` for the model axis. A non-empty string\n * that does not match any registered provider id is silently ignored and\n * the factory falls through to the next provider source.\n *\n * Phase 1 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n providerOverride?: string\n /**\n * Agent-level default base URL, typically `AiAgentDefinition.defaultBaseUrl`.\n * Sits between the `<MODULE>_AI_BASE_URL` env var and the preset's own\n * `baseURLEnvKeys` in the resolution chain.\n *\n * Phase 2 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n agentDefaultBaseUrl?: string\n /**\n * Per-call base URL override that wins over every other source. Intended\n * for programmatic callers only \u2014 the HTTP query-param baseUrl and the\n * AI_RUNTIME_BASEURL_ALLOWLIST arrive in Phase 4a.\n *\n * Phase 2 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n baseUrlOverride?: string\n /**\n * Per-tenant default loaded from `ai_agent_runtime_overrides` by the agent\n * runtime (best-effort, fail-open). Sits at step 3 of the resolution chain\n * between the caller/request override (step 1\u20132) and the module-env axis\n * (step 4).\n *\n * Honored ONLY when `allowRuntimeOverride !== false` on the agent definition\n * (checked via `resolveAllowRuntimeOverride`). The agent runtime is\n * responsible for hydration \u2014 the factory does NOT load the row itself.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n tenantOverride?: {\n providerId?: string | null\n modelId?: string | null\n baseURL?: string | null\n }\n /**\n * Per-request override forwarded from the HTTP dispatcher query params\n * (`?provider=`, `?model=`, `?baseUrl=`). Sits at step 1 of the resolution\n * chain \u2014 wins over everything else for that turn.\n *\n * Honored ONLY when `allowRuntimeOverride !== false` on the agent (checked\n * via `resolveAllowRuntimeOverride`). The dispatcher validates all three\n * values before setting this input.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n requestOverride?: {\n providerId?: string | null\n modelId?: string | null\n baseURL?: string | null\n }\n /**\n * When false, steps 1 (requestOverride) and 3 (tenantOverride) of the\n * resolution chain are skipped. Agents that pin a specific model for\n * correctness reasons set `AiAgentDefinition.allowRuntimeOverride = false`.\n * Default behavior (omitted) is permissive (= true).\n *\n * Canonical field (renamed from `allowRuntimeModelOverride` in Phase 4 of\n * spec `2026-04-28-ai-agents-agentic-loop-controls`). The deprecated alias\n * `allowRuntimeModelOverride` is still accepted via the resolution helper\n * {@link resolveAllowRuntimeOverride}.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n allowRuntimeOverride?: boolean\n /**\n * @deprecated Use `allowRuntimeOverride` instead.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n allowRuntimeModelOverride?: boolean\n /**\n * Optional tenant allowlist snapshot (Phase 1780-6). When supplied, the\n * factory clips the resolved (provider, model) to the intersection of the\n * env allowlist (`OM_AI_AVAILABLE_*`) and this tenant allowlist. Pass `null`\n * or omit to fall back to env-only enforcement.\n *\n * The settings PUT route validates writes against the env allowlist before\n * persisting, so the snapshot here is trusted to be a subset of env. The\n * factory still defends against drift (env tightened after write) by\n * intersecting at resolution time.\n */\n tenantAllowlist?: TenantAllowlistSnapshot | null\n /**\n * Optional opaque end-user identifier (tenant-salted HMAC). When set and the\n * resolved provider implements {@link LlmProvider.mapEndUserIdentifier}, the\n * factory returns the provider-specific `providerOptions` fragment on the\n * resolution so the runtime can attach it to the model call. No-op for\n * providers without a mapping or when no identifier is supplied.\n *\n * @see .ai/specs/2026-06-04-ai-input-moderation-and-safety-identifiers.md\n */\n endUserIdentifier?: 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 | 'request_override'\n | 'caller_override'\n | 'tenant_override'\n | 'module_env'\n | 'agent_default'\n | 'env_default'\n | 'provider_default'\n | 'allowlist_fallback'\n /**\n * Resolved base URL passed to the adapter (if any). Undefined when the\n * adapter will use its built-in default. Included for observability and\n * test assertions; never exposed over HTTP (Phase 4a adds the allowlist).\n */\n baseURL?: string\n /**\n * Populated when the env-driven OM_AI_AVAILABLE_PROVIDERS /\n * OM_AI_AVAILABLE_MODELS_<PROVIDER> allowlist rejected the originally\n * resolved (provider, model) and the factory fell back to a safe pair.\n * Includes the rejected ids and a human-readable reason so the UI / logs\n * can surface why the runtime did not honor the requested combination.\n */\n allowlistFallback?: {\n originalProviderId: string\n originalModelId: string\n reason: string\n }\n /**\n * Provider-specific `providerOptions` fragment produced by mapping the input\n * {@link AiModelFactoryInput.endUserIdentifier} through the resolved\n * provider's {@link LlmProvider.mapEndUserIdentifier}. Undefined when no\n * identifier was supplied or the provider has no mapping. The runtime merges\n * this into the AI SDK `providerOptions` for the model call.\n */\n providerOptions?: Record<string, unknown>\n /**\n * Mirror of the resolved provider's {@link LlmProvider.supportsInputModeration}\n * capability flag. The runtime uses this to decide whether to run the input\n * pre-moderation gate before the model call.\n */\n supportsInputModeration?: boolean\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 * Optional registry enumeration used by the Phase 1780-6 allowlist\n * intersection so the env model lists are pre-loaded for every provider\n * (and not just the resolved one). Test doubles MAY omit this \u2014 the\n * factory still defends correctly by also seeding the resolved provider's\n * id directly into `intersectAllowlists(...)`.\n */\n list?(): readonly LlmProvider[]\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 `OM_AI_<MODULE>_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 readGlobalProviderFromEnv(\n env: EnvLookup,\n registry: Pick<AiModelFactoryRegistry, 'get'>,\n): string | null {\n const candidates = [normalizeOverride(env.OM_AI_PROVIDER), normalizeOverride(env.OPENCODE_PROVIDER)]\n for (const candidate of candidates) {\n if (!candidate) continue\n if (!registry.get) return providerIdAliases(candidate)[0] ?? candidate\n for (const alias of providerIdAliases(candidate)) {\n if (registry.get(alias)) return alias\n }\n }\n return null\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 moduleModelEnvVarName(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 legacyModuleModelEnvVarName(moduleId: string): string {\n return `${moduleId.toUpperCase()}_AI_MODEL`\n}\n\nfunction readModuleModelEnvOverride(env: EnvLookup, moduleId: string): string | null {\n return (\n normalizeOverride(env[moduleModelEnvVarName(moduleId)]) ??\n normalizeOverride(env[legacyModuleModelEnvVarName(moduleId)])\n )\n}\n\n/** Canonical per-module provider env. Example: `OM_AI_INBOX_OPS_PROVIDER`. */\nfunction moduleProviderEnvVarName(moduleId: string): string {\n return `OM_AI_${moduleId.toUpperCase()}_PROVIDER`\n}\n\n/**\n * Legacy per-module provider env (pre-OM_AI_* rename). Example:\n * `INBOX_OPS_AI_PROVIDER`. Read as a backward-compatibility fallback only.\n */\nfunction legacyModuleProviderEnvVarName(moduleId: string): string {\n return `${moduleId.toUpperCase()}_AI_PROVIDER`\n}\n\nfunction readModuleProviderEnvOverride(env: EnvLookup, moduleId: string): string | null {\n return (\n normalizeOverride(env[moduleProviderEnvVarName(moduleId)]) ??\n normalizeOverride(env[legacyModuleProviderEnvVarName(moduleId)])\n )\n}\n\nfunction normalizeProviderHint(\n providerId: string | null,\n registry: AiModelFactoryRegistry,\n): string | null {\n if (!providerId) return null\n const knownProviderIds = registry.list?.().map((provider) => provider.id) ?? []\n if (knownProviderIds.length > 0) {\n return canonicalProviderId(providerId, knownProviderIds)\n }\n return providerIdAliases(providerId)[0] ?? providerId\n}\n\nfunction resolveRequiredProvider(\n providerId: string,\n registry: AiModelFactoryRegistry,\n env: EnvLookup,\n): LlmProvider | null {\n const resolved = registry.resolveFirstConfigured({ env, order: [providerId] })\n if (resolved?.id === providerId) return resolved\n\n const direct = registry.get?.(providerId) ?? null\n if (direct) return direct.isConfigured(env) ? direct : null\n return null\n}\n\nfunction requiredProviderMessage(providerId: string, registry: AiModelFactoryRegistry, env: EnvLookup): string {\n const provider = registry.get?.(providerId) ?? null\n const envKey = provider?.getConfiguredEnvKey?.(env)\n const credentialHint = envKey\n ? ` Set ${envKey} to use this provider.`\n : ' Configure the matching provider API key to use this provider.'\n return `The resolved model is pinned to provider \"${providerId}\", but that provider is not configured.${credentialHint} The runtime refuses to send provider-specific model ids to a different provider.`\n}\n\nfunction moduleBaseUrlEnvVarName(moduleId: string): string {\n return `${moduleId.toUpperCase()}_AI_BASE_URL`\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/** Drops one leading `${providerId}/` segment from `token` when present. */\nfunction stripLeadingProviderPrefix(token: string, providerId: string): string {\n const prefix = `${providerId}/`\n return token.startsWith(prefix) ? token.slice(prefix.length) : token\n}\n\n/**\n * True when `providerId` names a registered provider that (a) is an\n * OpenAI-compatible gateway using `vendor/model` ids and (b) is configured in\n * `env`. Only a *configured* gateway suppresses slash-splitting \u2014 an\n * unconfigured gateway hint must not steal a model token from a native adapter\n * that could still serve it.\n */\nfunction isConfiguredVendorPrefixGateway(\n providerId: string | null,\n registry: Pick<AiModelFactoryRegistry, 'get'>,\n env: EnvLookup,\n): boolean {\n if (!providerId || !registry.get) return false\n const provider = registry.get(providerId)\n return provider?.usesVendorPrefixedModelIds === true && provider.isConfigured(env)\n}\n\n/**\n * Parses one resolution tier's model token, honoring a same-tier gateway\n * provider hint. When the tier explicitly selects a *configured* vendor-prefix\n * gateway, the token's leading `vendor/` is part of the gateway model id (not a\n * native-provider pin): the gateway becomes the provider hint and at most one\n * leading `${gateway}/` prefix is stripped (so the documented\n * `openrouter/anthropic/\u2026` workaround still resolves without doubling).\n * Otherwise it falls back to {@link parseSlashShorthand}.\n *\n * Exported for test coverage; production callers go through\n * {@link createModelFactory}.\n */\nexport function parseTierModel(\n token: string,\n tierProviderHint: string | null,\n registry: Pick<AiModelFactoryRegistry, 'get'>,\n env: EnvLookup,\n): { providerHint: string | null; modelId: string } {\n if (tierProviderHint && isConfiguredVendorPrefixGateway(tierProviderHint, registry, env)) {\n return {\n providerHint: tierProviderHint,\n modelId: stripLeadingProviderPrefix(token, tierProviderHint),\n }\n }\n return parseSlashShorthand(token, registry)\n}\n\n/**\n * Resolves the effective `allowRuntimeOverride` flag from an input that may\n * carry either the new canonical name (`allowRuntimeOverride`) or the\n * deprecated alias (`allowRuntimeModelOverride`). The canonical name wins\n * when both are present. Returns `true` (permissive) when neither is set.\n *\n * Exported for test coverage.\n */\nexport function resolveAllowRuntimeOverride(input: {\n allowRuntimeOverride?: boolean\n allowRuntimeModelOverride?: boolean\n}): boolean {\n if (input.allowRuntimeOverride !== undefined) return input.allowRuntimeOverride !== false\n if (input.allowRuntimeModelOverride !== undefined) return input.allowRuntimeModelOverride !== false\n return true\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 const hasModule = typeof input.moduleId === 'string' && input.moduleId.length > 0\n // When allowRuntimeOverride (or its deprecated alias allowRuntimeModelOverride)\n // is explicitly false, skip steps 1 (requestOverride) and 3 (tenantOverride).\n const runtimeOverridesAllowed = resolveAllowRuntimeOverride(input)\n\n // --- Step 1: requestOverride (HTTP query params) \u2014 gated by flag ---\n const requestModelRaw = runtimeOverridesAllowed\n ? normalizeOverride(input.requestOverride?.modelId ?? undefined)\n : null\n const requestProviderRaw = runtimeOverridesAllowed\n ? normalizeOverride(input.requestOverride?.providerId ?? undefined)\n : null\n const requestBaseUrlRaw = runtimeOverridesAllowed\n ? normalizeOverride(input.requestOverride?.baseURL ?? undefined)\n : null\n\n // --- Step 2: callerOverride (programmatic) ---\n const callerRaw = normalizeOverride(input.callerOverride)\n\n // --- Step 3: tenantOverride (DB row) \u2014 gated by flag ---\n const tenantModelRaw = runtimeOverridesAllowed\n ? normalizeOverride(input.tenantOverride?.modelId ?? undefined)\n : null\n const tenantProviderRaw = runtimeOverridesAllowed\n ? normalizeOverride(input.tenantOverride?.providerId ?? undefined)\n : null\n const tenantBaseUrlRaw = runtimeOverridesAllowed\n ? normalizeOverride(input.tenantOverride?.baseURL ?? undefined)\n : null\n\n // --- Steps 4+: env / agent / global ---\n const moduleModelRaw = hasModule\n ? readModuleModelEnvOverride(env, input.moduleId!)\n : null\n const agentModelRaw = normalizeOverride(input.agentDefaultModel)\n // OM_AI_MODEL is canonical; the legacy OPENCODE_MODEL is read as a\n // backward-compatibility fallback through readGlobalModelFromEnv.\n const globalModelRaw = readGlobalModelFromEnv(env)\n\n const providerOverrideRaw = normalizeOverride(input.providerOverride)\n const moduleProviderRaw = hasModule\n ? readModuleProviderEnvOverride(env, input.moduleId!)\n : null\n const agentDefaultProviderRaw = normalizeOverride(input.agentDefaultProvider)\n // OM_AI_PROVIDER is canonical; the legacy OPENCODE_PROVIDER is read as\n // a backward-compatibility fallback through readGlobalProviderFromEnv.\n const globalProviderRaw = readGlobalProviderFromEnv(env, registry)\n\n const requestProviderHint = normalizeProviderHint(requestProviderRaw, registry)\n const providerOverrideHint = normalizeProviderHint(providerOverrideRaw, registry)\n const tenantProviderHint = normalizeProviderHint(tenantProviderRaw, registry)\n const moduleProviderHint = normalizeProviderHint(moduleProviderRaw, registry)\n const agentDefaultProviderHint = normalizeProviderHint(agentDefaultProviderRaw, registry)\n\n // Parse each model-axis source with its same-tier provider hint so a\n // configured vendor-prefix gateway (OpenRouter, Requesty, LiteLLM) keeps\n // its `vendor/model` id intact instead of pinning the native `vendor`.\n const requestModelParsed = requestModelRaw ? parseTierModel(requestModelRaw, requestProviderHint, registry, env) : null\n const callerParsed = callerRaw ? parseTierModel(callerRaw, providerOverrideHint, registry, env) : null\n const tenantModelParsed = tenantModelRaw ? parseTierModel(tenantModelRaw, tenantProviderHint, registry, env) : null\n const moduleModelParsed = moduleModelRaw ? parseTierModel(moduleModelRaw, moduleProviderHint, registry, env) : null\n const agentModelParsed = agentModelRaw ? parseTierModel(agentModelRaw, agentDefaultProviderHint, registry, env) : null\n const globalModelParsed = globalModelRaw ? parseTierModel(globalModelRaw, globalProviderRaw, registry, env) : null\n\n // Walk the provider-axis seed list: slash hint beats plain provider at\n // the same step. We keep only the first (highest-priority) non-null hint.\n const providerHintCandidates: Array<string | null> = [\n requestModelParsed?.providerHint ?? null,\n requestProviderHint,\n callerParsed?.providerHint ?? null,\n providerOverrideHint,\n tenantModelParsed?.providerHint ?? null,\n tenantProviderHint,\n moduleModelParsed?.providerHint ?? null,\n moduleProviderHint,\n agentModelParsed?.providerHint ?? null,\n agentDefaultProviderHint,\n globalModelParsed?.providerHint ?? null,\n globalProviderRaw,\n ]\n const orderHint = providerHintCandidates.find((hint) => hint !== null) ?? null\n const order = orderHint ? [orderHint] : undefined\n\n const pairPlainProviderIfWinning = (providerHint: string | null): string | null =>\n providerHint && providerHint === orderHint ? providerHint : null\n\n let modelId: string\n let source: AiModelResolution['source']\n let pairedProviderHint: string | null = null\n if (requestModelParsed) {\n modelId = requestModelParsed.modelId\n source = 'request_override'\n pairedProviderHint = requestModelParsed.providerHint ?? pairPlainProviderIfWinning(requestProviderHint)\n } else if (callerParsed) {\n modelId = callerParsed.modelId\n source = 'caller_override'\n pairedProviderHint = callerParsed.providerHint ?? pairPlainProviderIfWinning(providerOverrideHint)\n } else if (tenantModelParsed) {\n modelId = tenantModelParsed.modelId\n source = 'tenant_override'\n pairedProviderHint = tenantModelParsed.providerHint ?? pairPlainProviderIfWinning(tenantProviderHint)\n } else if (moduleModelParsed) {\n modelId = moduleModelParsed.modelId\n source = 'module_env'\n pairedProviderHint = moduleModelParsed.providerHint ?? pairPlainProviderIfWinning(moduleProviderHint)\n } else if (agentModelParsed) {\n modelId = agentModelParsed.modelId\n source = 'agent_default'\n pairedProviderHint = agentModelParsed.providerHint ?? pairPlainProviderIfWinning(agentDefaultProviderHint)\n } else if (globalModelParsed) {\n modelId = globalModelParsed.modelId\n source = 'env_default'\n pairedProviderHint = globalModelParsed.providerHint ?? pairPlainProviderIfWinning(globalProviderRaw)\n } else {\n modelId = ''\n source = 'provider_default'\n }\n\n // --- Provider-axis: walk from highest to lowest priority for the seed.\n // A slash-qualified hint from a model source wins over a plain provider\n // source at the same priority step. We walk top-down and take the first\n // non-null hint.\n const provider = pairedProviderHint\n ? resolveRequiredProvider(pairedProviderHint, registry, env)\n : registry.resolveFirstConfigured({ env, order })\n if (!provider) {\n if (pairedProviderHint) {\n throw new AiModelFactoryError(\n 'no_provider_configured',\n requiredProviderMessage(pairedProviderHint, registry, env),\n )\n }\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 // --- Model-axis: use the post-parse model id from the winning source.\n if (source === 'provider_default') {\n modelId = provider.defaultModel\n }\n\n // --- BaseURL-axis resolution (highest to lowest priority) ---\n // 1. requestOverride.baseURL (HTTP dispatcher) \u2014 gated by allowRuntimeOverride\n // 2. baseUrlOverride (programmatic caller)\n // 3. tenantOverride.baseURL (DB row) \u2014 gated by allowRuntimeOverride\n // 4. <MODULE>_AI_BASE_URL env\n // 5. agentDefaultBaseUrl\n // Steps 6-7 (preset env + preset default) are handled inside the adapter's\n // createModel when no explicit baseURL is passed.\n const resolvedBaseURL = requestBaseUrlRaw\n ?? normalizeOverride(input.baseUrlOverride)\n ?? tenantBaseUrlRaw\n ?? (hasModule ? normalizeOverride(env[moduleBaseUrlEnvVarName(input.moduleId!)]) : null)\n ?? normalizeOverride(input.agentDefaultBaseUrl)\n ?? undefined\n\n // --- Allowlist enforcement (Phase 1780-5 + 1780-6) -------------------\n // OM_AI_AVAILABLE_PROVIDERS / OM_AI_AVAILABLE_MODELS_<PROVIDER> clip\n // the resolution to an operator-approved set. The optional tenant\n // allowlist snapshot narrows the env outer constraint further. If the\n // resolved pair isn't allowed, fall back to a safe (provider, model)\n // \u2014 never throw, so a stale tenant override or chat picker can't take\n // the runtime down. The fallback is logged so the operator can see\n // what happened.\n const registryProviderIds = registry.list?.()?.map((p) => p.id) ?? []\n const tenantProviderIds = input.tenantAllowlist\n ? Object.keys(input.tenantAllowlist.allowedModelsByProvider ?? {})\n : []\n const knownProviderIds = Array.from(\n new Set([provider.id, ...registryProviderIds, ...tenantProviderIds]),\n )\n const effectiveAllowlist = intersectAllowlists(\n env,\n knownProviderIds,\n input.tenantAllowlist ?? null,\n )\n const allowlistResult = enforceAllowlist({\n env,\n registry,\n resolved: { provider, modelId },\n agentDefaultProvider: agentDefaultProviderRaw,\n agentDefaultModel: agentModelParsed?.modelId ?? agentModelRaw,\n effective: effectiveAllowlist,\n })\n\n const finalProvider = allowlistResult.provider\n const finalModelId = allowlistResult.modelId\n const finalSource = allowlistResult.fallback ? 'allowlist_fallback' : source\n const finalApiKey = allowlistResult.fallback\n ? finalProvider.resolveApiKey(env)\n : apiKey\n if (!finalApiKey) {\n throw new AiModelFactoryError(\n 'api_key_missing',\n `LLM provider \"${finalProvider.id}\" is advertised as configured but resolveApiKey() returned empty.`,\n )\n }\n\n const endUserIdentifier = normalizeOverride(input.endUserIdentifier)\n const providerOptions =\n endUserIdentifier && finalProvider.mapEndUserIdentifier\n ? finalProvider.mapEndUserIdentifier(endUserIdentifier)\n : undefined\n\n const model = finalProvider.createModel({\n modelId: finalModelId,\n apiKey: finalApiKey,\n baseURL: resolvedBaseURL,\n ...(endUserIdentifier ? { endUserIdentifier } : {}),\n })\n return {\n model,\n modelId: finalModelId,\n providerId: finalProvider.id,\n source: finalSource,\n supportsInputModeration: finalProvider.supportsInputModeration === true,\n ...(resolvedBaseURL !== undefined ? { baseURL: resolvedBaseURL } : {}),\n ...(providerOptions !== undefined ? { providerOptions } : {}),\n ...(allowlistResult.fallback\n ? {\n allowlistFallback: {\n originalProviderId: provider.id,\n originalModelId: modelId,\n reason: allowlistResult.fallback,\n },\n }\n : {}),\n }\n },\n }\n}\n\ninterface EnforceAllowlistInput {\n env: EnvLookup\n registry: AiModelFactoryRegistry\n resolved: { provider: LlmProvider; modelId: string }\n agentDefaultProvider: string | null\n agentDefaultModel: string | null\n effective: EffectiveAllowlist\n}\n\ninterface EnforceAllowlistResult {\n provider: LlmProvider\n modelId: string\n /** Populated only when the resolved pair was rejected. */\n fallback: string | null\n}\n\n/**\n * Clips a resolved `(provider, model)` to what the effective allowlist\n * permits (env intersected with optional tenant allowlist).\n *\n * Order of fallback when the resolved provider is not allowed:\n * 1. The agent's `defaultProvider` (if allowed and configured).\n * 2. The first allowed provider that is also configured in the registry.\n *\n * Order of fallback when the model is not allowed for the resolved provider:\n * 1. The agent's `defaultModel` (if allowed for that provider).\n * 2. The provider's `defaultModel` (if allowed).\n * 3. The first model from the effective allowlist for that provider.\n *\n * Both fall-back paths emit a logger warning so the operator can see why the\n * runtime did not honor the requested combination. The function never throws.\n */\nfunction enforceAllowlist(input: EnforceAllowlistInput): EnforceAllowlistResult {\n const { registry, resolved, agentDefaultProvider, agentDefaultModel, effective } = input\n let provider = resolved.provider\n let modelId = resolved.modelId\n let fallback: string | null = null\n\n if (effective.providers !== null && !isProviderAllowedInEffective(effective, provider.id)) {\n const replacement = pickAllowedProvider({\n registry,\n agentDefaultProvider,\n effective,\n })\n if (replacement) {\n const source = effective.tenantOverridesActive\n ? 'the effective allowlist (env \u2229 tenant)'\n : 'OM_AI_AVAILABLE_PROVIDERS'\n fallback = `Provider \"${provider.id}\" is not in ${source}; using \"${replacement.id}\" instead.`\n logger.warn('Allowlist provider fallback', { reason: fallback })\n provider = replacement\n modelId = pickAllowedModel({\n provider,\n preferred: agentDefaultModel,\n effective,\n })\n }\n // If no replacement is configured we keep the resolved provider \u2014 the\n // throw at the api-key gate will surface the misconfiguration to the\n // operator instead of silently masking it.\n }\n\n if (!isModelAllowedForProviderInEffective(effective, provider.id, modelId)) {\n const replacementModel = pickAllowedModel({\n provider,\n preferred: agentDefaultModel,\n effective,\n })\n if (replacementModel !== modelId) {\n const source = effective.tenantOverridesActive\n ? `the effective allowlist (env \u2229 tenant) for \"${provider.id}\"`\n : `OM_AI_AVAILABLE_MODELS_${provider.id.toUpperCase()}`\n const reason = `Model \"${modelId}\" is not in ${source}; using \"${replacementModel}\" instead.`\n logger.warn('Allowlist model fallback', { reason })\n fallback = fallback ? `${fallback} ${reason}` : reason\n modelId = replacementModel\n }\n }\n\n return { provider, modelId, fallback }\n}\n\nfunction pickAllowedProvider(input: {\n registry: AiModelFactoryRegistry\n agentDefaultProvider: string | null\n effective: EffectiveAllowlist\n}): LlmProvider | null {\n const { registry, agentDefaultProvider, effective } = input\n if (agentDefaultProvider) {\n if (isProviderAllowedInEffective(effective, agentDefaultProvider)) {\n const provider = registry.get?.(agentDefaultProvider)\n if (provider && provider.isConfigured(process.env as EnvLookup)) return provider\n }\n }\n const allowed = effective.providers\n if (!allowed) return null\n for (const id of allowed) {\n const provider = registry.get?.(id)\n if (provider && provider.isConfigured(process.env as EnvLookup)) return provider\n }\n return null\n}\n\nfunction pickAllowedModel(input: {\n provider: LlmProvider\n preferred: string | null\n effective: EffectiveAllowlist\n}): string {\n const { provider, preferred, effective } = input\n const allowed = effective.modelsByProvider[provider.id]\n if (allowed === undefined) {\n return preferred && preferred.length > 0 ? preferred : provider.defaultModel\n }\n if (preferred && allowed.includes(preferred)) return preferred\n if (allowed.includes(provider.defaultModel)) return provider.defaultModel\n return allowed[0] ?? provider.defaultModel\n}\n"],
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 * Accepts a slash-qualified `<provider>/<model>` shorthand (Phase 1).\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 * Accepts a slash-qualified shorthand (Phase 1).\n * 3. `agentDefaultModel` \u2014 typically `AiAgentDefinition.defaultModel`.\n * Accepts a slash-qualified `<provider>/<model>` shorthand (Phase 1).\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. When the\n * selected model source carries a provider hint, the provider/model pair\n * is atomic: an unconfigured hinted provider fails instead of sending\n * the model id to a different configured provider.\n * 5. The configured provider's own default model id\n * (`provider.defaultModel`).\n *\n * Every model-axis source is parsed through {@link parseSlashShorthand}.\n * Resolution walks the chain top-down and takes the first non-null hint as\n * the registry-walk seed. If the winning model source also supplies the\n * winning provider hint \u2014 either through `<provider>/<model>` or through the\n * same-source provider field/env var \u2014 that pair is resolved exactly and\n * never mixed with a fallback provider.\n *\n * Provider-axis seed order (highest priority first):\n * 1. Slash-prefix from `callerOverride` (Phase 1).\n * 2. `providerOverride` \u2014 request-time provider override (Phase 1).\n * 3. Slash-prefix from `OM_AI_<MODULE>_MODEL` (legacy `<MODULE>_AI_MODEL`) (Phase 1).\n * 4. `OM_AI_<MODULE>_PROVIDER` env (legacy `<MODULE>_AI_PROVIDER`) (Phase 1).\n * 5. Slash-prefix from `agentDefaultModel` (Phase 1).\n * 6. `agentDefaultProvider` \u2014 `AiAgentDefinition.defaultProvider` (Phase 1).\n * 7. Slash-prefix from `OM_AI_MODEL` (legacy `OPENCODE_MODEL`) (Phase 0).\n * 8. `OM_AI_PROVIDER` (legacy `OPENCODE_PROVIDER`) (Phase 0).\n *\n * The `OM_AI_*` env knobs are canonical; the legacy `OPENCODE_PROVIDER` /\n * `OPENCODE_MODEL` envs stay bound to the OpenCode Code Mode stack and are\n * also honored as backward-compatibility fallbacks here.\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 { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { AwilixContainer } from 'awilix'\nimport type { EnvLookup, LlmProvider } from '@open-mercato/shared/lib/ai/llm-provider'\nimport { llmProviderRegistry } from './llm-registry'\nimport {\n intersectAllowlists,\n canonicalProviderId,\n isModelAllowedForProviderInEffective,\n isProviderAllowedInEffective,\n providerIdAliases,\n type EffectiveAllowlist,\n type TenantAllowlistSnapshot,\n} from './model-allowlist'\n\nconst logger = createLogger('ai_assistant')\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 * Also enables the `OM_AI_<MODULE>_PROVIDER` env axis (legacy\n * `<MODULE>_AI_PROVIDER` honored as a backward-compatibility fallback).\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 * Accepts a slash-qualified `<provider>/<model>` shorthand (Phase 1).\n */\n agentDefaultModel?: string\n /**\n * Agent-level default provider, typically `AiAgentDefinition.defaultProvider`.\n * Named provider id. When paired with an agent default model, the pair is\n * resolved exactly and fails if the provider is unconfigured. When used as\n * a provider preference without an agent default model, it can fall through\n * to the next configured provider. Sits between `OM_AI_<MODULE>_PROVIDER`\n * and the global `OM_AI_PROVIDER` in the provider-axis seed list above.\n *\n * Phase 1 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n agentDefaultProvider?: 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 * Request-time provider override \u2014 wins for the provider axis at the same\n * priority as `callerOverride` for the model axis. A non-empty string\n * that does not match any registered provider id is silently ignored and\n * the factory falls through to the next provider source.\n *\n * Phase 1 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n providerOverride?: string\n /**\n * Agent-level default base URL, typically `AiAgentDefinition.defaultBaseUrl`.\n * Sits between the `<MODULE>_AI_BASE_URL` env var and the preset's own\n * `baseURLEnvKeys` in the resolution chain.\n *\n * Phase 2 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n agentDefaultBaseUrl?: string\n /**\n * Per-call base URL override that wins over every other source. Intended\n * for programmatic callers only \u2014 the HTTP query-param baseUrl and the\n * AI_RUNTIME_BASEURL_ALLOWLIST arrive in Phase 4a.\n *\n * Phase 2 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n baseUrlOverride?: string\n /**\n * Per-tenant default loaded from `ai_agent_runtime_overrides` by the agent\n * runtime (best-effort, fail-open). Sits at step 3 of the resolution chain\n * between the caller/request override (step 1\u20132) and the module-env axis\n * (step 4).\n *\n * Honored ONLY when `allowRuntimeOverride !== false` on the agent definition\n * (checked via `resolveAllowRuntimeOverride`). The agent runtime is\n * responsible for hydration \u2014 the factory does NOT load the row itself.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n tenantOverride?: {\n providerId?: string | null\n modelId?: string | null\n baseURL?: string | null\n }\n /**\n * Per-request override forwarded from the HTTP dispatcher query params\n * (`?provider=`, `?model=`, `?baseUrl=`). Sits at step 1 of the resolution\n * chain \u2014 wins over everything else for that turn.\n *\n * Honored ONLY when `allowRuntimeOverride !== false` on the agent (checked\n * via `resolveAllowRuntimeOverride`). The dispatcher validates all three\n * values before setting this input.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n requestOverride?: {\n providerId?: string | null\n modelId?: string | null\n baseURL?: string | null\n }\n /**\n * When false, steps 1 (requestOverride) and 3 (tenantOverride) of the\n * resolution chain are skipped. Agents that pin a specific model for\n * correctness reasons set `AiAgentDefinition.allowRuntimeOverride = false`.\n * Default behavior (omitted) is permissive (= true).\n *\n * Canonical field (renamed from `allowRuntimeModelOverride` in Phase 4 of\n * spec `2026-04-28-ai-agents-agentic-loop-controls`). The deprecated alias\n * `allowRuntimeModelOverride` is still accepted via the resolution helper\n * {@link resolveAllowRuntimeOverride}.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n allowRuntimeOverride?: boolean\n /**\n * @deprecated Use `allowRuntimeOverride` instead.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n allowRuntimeModelOverride?: boolean\n /**\n * Optional tenant allowlist snapshot (Phase 1780-6). When supplied, the\n * factory clips the resolved (provider, model) to the intersection of the\n * env allowlist (`OM_AI_AVAILABLE_*`) and this tenant allowlist. Pass `null`\n * or omit to fall back to env-only enforcement.\n *\n * The settings PUT route validates writes against the env allowlist before\n * persisting, so the snapshot here is trusted to be a subset of env. The\n * factory still defends against drift (env tightened after write) by\n * intersecting at resolution time.\n */\n tenantAllowlist?: TenantAllowlistSnapshot | null\n /**\n * Optional opaque end-user identifier (tenant-salted HMAC). When set and the\n * resolved provider implements {@link LlmProvider.mapEndUserIdentifier}, the\n * factory returns the provider-specific `providerOptions` fragment on the\n * resolution so the runtime can attach it to the model call. No-op for\n * providers without a mapping or when no identifier is supplied.\n *\n * @see .ai/specs/2026-06-04-ai-input-moderation-and-safety-identifiers.md\n */\n endUserIdentifier?: 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 | 'request_override'\n | 'caller_override'\n | 'tenant_override'\n | 'module_env'\n | 'agent_default'\n | 'env_default'\n | 'provider_default'\n | 'allowlist_fallback'\n /**\n * Resolved base URL passed to the adapter (if any). Undefined when the\n * adapter will use its built-in default. Included for observability and\n * test assertions; never exposed over HTTP (Phase 4a adds the allowlist).\n */\n baseURL?: string\n /**\n * Populated when the env-driven OM_AI_AVAILABLE_PROVIDERS /\n * OM_AI_AVAILABLE_MODELS_<PROVIDER> allowlist rejected the originally\n * resolved (provider, model) and the factory fell back to a safe pair.\n * Includes the rejected ids and a human-readable reason so the UI / logs\n * can surface why the runtime did not honor the requested combination.\n */\n allowlistFallback?: {\n originalProviderId: string\n originalModelId: string\n reason: string\n }\n /**\n * Provider-specific `providerOptions` fragment produced by mapping the input\n * {@link AiModelFactoryInput.endUserIdentifier} through the resolved\n * provider's {@link LlmProvider.mapEndUserIdentifier}. Undefined when no\n * identifier was supplied or the provider has no mapping. The runtime merges\n * this into the AI SDK `providerOptions` for the model call.\n */\n providerOptions?: Record<string, unknown>\n /**\n * Mirror of the resolved provider's {@link LlmProvider.supportsInputModeration}\n * capability flag. The runtime uses this to decide whether to run the input\n * pre-moderation gate before the model call.\n */\n supportsInputModeration?: boolean\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 * Optional registry enumeration used by the Phase 1780-6 allowlist\n * intersection so the env model lists are pre-loaded for every provider\n * (and not just the resolved one). Test doubles MAY omit this \u2014 the\n * factory still defends correctly by also seeding the resolved provider's\n * id directly into `intersectAllowlists(...)`.\n */\n list?(): readonly LlmProvider[]\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 `OM_AI_<MODULE>_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 readGlobalProviderFromEnv(\n env: EnvLookup,\n registry: Pick<AiModelFactoryRegistry, 'get'>,\n): string | null {\n const candidates = [normalizeOverride(env.OM_AI_PROVIDER), normalizeOverride(env.OPENCODE_PROVIDER)]\n for (const candidate of candidates) {\n if (!candidate) continue\n if (!registry.get) return providerIdAliases(candidate)[0] ?? candidate\n for (const alias of providerIdAliases(candidate)) {\n if (registry.get(alias)) return alias\n }\n }\n return null\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 moduleModelEnvVarName(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 legacyModuleModelEnvVarName(moduleId: string): string {\n return `${moduleId.toUpperCase()}_AI_MODEL`\n}\n\nfunction readModuleModelEnvOverride(env: EnvLookup, moduleId: string): string | null {\n return (\n normalizeOverride(env[moduleModelEnvVarName(moduleId)]) ??\n normalizeOverride(env[legacyModuleModelEnvVarName(moduleId)])\n )\n}\n\n/** Canonical per-module provider env. Example: `OM_AI_INBOX_OPS_PROVIDER`. */\nfunction moduleProviderEnvVarName(moduleId: string): string {\n return `OM_AI_${moduleId.toUpperCase()}_PROVIDER`\n}\n\n/**\n * Legacy per-module provider env (pre-OM_AI_* rename). Example:\n * `INBOX_OPS_AI_PROVIDER`. Read as a backward-compatibility fallback only.\n */\nfunction legacyModuleProviderEnvVarName(moduleId: string): string {\n return `${moduleId.toUpperCase()}_AI_PROVIDER`\n}\n\nfunction readModuleProviderEnvOverride(env: EnvLookup, moduleId: string): string | null {\n return (\n normalizeOverride(env[moduleProviderEnvVarName(moduleId)]) ??\n normalizeOverride(env[legacyModuleProviderEnvVarName(moduleId)])\n )\n}\n\nfunction normalizeProviderHint(\n providerId: string | null,\n registry: AiModelFactoryRegistry,\n): string | null {\n if (!providerId) return null\n const knownProviderIds = registry.list?.().map((provider) => provider.id) ?? []\n if (knownProviderIds.length > 0) {\n return canonicalProviderId(providerId, knownProviderIds)\n }\n return providerIdAliases(providerId)[0] ?? providerId\n}\n\nfunction resolveRequiredProvider(\n providerId: string,\n registry: AiModelFactoryRegistry,\n env: EnvLookup,\n): LlmProvider | null {\n const resolved = registry.resolveFirstConfigured({ env, order: [providerId] })\n if (resolved?.id === providerId) return resolved\n\n const direct = registry.get?.(providerId) ?? null\n if (direct) return direct.isConfigured(env) ? direct : null\n return null\n}\n\nfunction requiredProviderMessage(providerId: string, registry: AiModelFactoryRegistry, env: EnvLookup): string {\n const provider = registry.get?.(providerId) ?? null\n const envKey = provider?.getConfiguredEnvKey?.(env)\n const credentialHint = envKey\n ? ` Set ${envKey} to use this provider.`\n : ' Configure the matching provider API key to use this provider.'\n return `The resolved model is pinned to provider \"${providerId}\", but that provider is not configured.${credentialHint} The runtime refuses to send provider-specific model ids to a different provider.`\n}\n\nfunction moduleBaseUrlEnvVarName(moduleId: string): string {\n return `${moduleId.toUpperCase()}_AI_BASE_URL`\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/** Drops one leading `${providerId}/` segment from `token` when present. */\nfunction stripLeadingProviderPrefix(token: string, providerId: string): string {\n const prefix = `${providerId}/`\n return token.startsWith(prefix) ? token.slice(prefix.length) : token\n}\n\n/**\n * True when `providerId` names a registered provider that (a) is an\n * OpenAI-compatible gateway using `vendor/model` ids and (b) is configured in\n * `env`. Only a *configured* gateway suppresses slash-splitting \u2014 an\n * unconfigured gateway hint must not steal a model token from a native adapter\n * that could still serve it.\n */\nfunction isConfiguredVendorPrefixGateway(\n providerId: string | null,\n registry: Pick<AiModelFactoryRegistry, 'get'>,\n env: EnvLookup,\n): boolean {\n if (!providerId || !registry.get) return false\n const provider = registry.get(providerId)\n return provider?.usesVendorPrefixedModelIds === true && provider.isConfigured(env)\n}\n\n/**\n * Parses one resolution tier's model token, honoring a same-tier gateway\n * provider hint. When the tier explicitly selects a *configured* vendor-prefix\n * gateway, the token's leading `vendor/` is part of the gateway model id (not a\n * native-provider pin): the gateway becomes the provider hint and at most one\n * leading `${gateway}/` prefix is stripped (so the documented\n * `openrouter/anthropic/\u2026` workaround still resolves without doubling).\n * Otherwise it falls back to {@link parseSlashShorthand}.\n *\n * Exported for test coverage; production callers go through\n * {@link createModelFactory}.\n */\nexport function parseTierModel(\n token: string,\n tierProviderHint: string | null,\n registry: Pick<AiModelFactoryRegistry, 'get'>,\n env: EnvLookup,\n): { providerHint: string | null; modelId: string } {\n if (tierProviderHint && isConfiguredVendorPrefixGateway(tierProviderHint, registry, env)) {\n return {\n providerHint: tierProviderHint,\n modelId: stripLeadingProviderPrefix(token, tierProviderHint),\n }\n }\n return parseSlashShorthand(token, registry)\n}\n\n/**\n * Resolves the effective `allowRuntimeOverride` flag from an input that may\n * carry either the new canonical name (`allowRuntimeOverride`) or the\n * deprecated alias (`allowRuntimeModelOverride`). The canonical name wins\n * when both are present. Returns `true` (permissive) when neither is set.\n *\n * Exported for test coverage.\n */\nexport function resolveAllowRuntimeOverride(input: {\n allowRuntimeOverride?: boolean\n allowRuntimeModelOverride?: boolean\n}): boolean {\n if (input.allowRuntimeOverride !== undefined) return input.allowRuntimeOverride !== false\n if (input.allowRuntimeModelOverride !== undefined) return input.allowRuntimeModelOverride !== false\n return true\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 const hasModule = typeof input.moduleId === 'string' && input.moduleId.length > 0\n // When allowRuntimeOverride (or its deprecated alias allowRuntimeModelOverride)\n // is explicitly false, skip steps 1 (requestOverride) and 3 (tenantOverride).\n const runtimeOverridesAllowed = resolveAllowRuntimeOverride(input)\n\n // --- Step 1: requestOverride (HTTP query params) \u2014 gated by flag ---\n const requestModelRaw = runtimeOverridesAllowed\n ? normalizeOverride(input.requestOverride?.modelId ?? undefined)\n : null\n const requestProviderRaw = runtimeOverridesAllowed\n ? normalizeOverride(input.requestOverride?.providerId ?? undefined)\n : null\n const requestBaseUrlRaw = runtimeOverridesAllowed\n ? normalizeOverride(input.requestOverride?.baseURL ?? undefined)\n : null\n\n // --- Step 2: callerOverride (programmatic) ---\n const callerRaw = normalizeOverride(input.callerOverride)\n\n // --- Step 3: tenantOverride (DB row) \u2014 gated by flag ---\n const tenantModelRaw = runtimeOverridesAllowed\n ? normalizeOverride(input.tenantOverride?.modelId ?? undefined)\n : null\n const tenantProviderRaw = runtimeOverridesAllowed\n ? normalizeOverride(input.tenantOverride?.providerId ?? undefined)\n : null\n const tenantBaseUrlRaw = runtimeOverridesAllowed\n ? normalizeOverride(input.tenantOverride?.baseURL ?? undefined)\n : null\n\n // --- Steps 4+: env / agent / global ---\n const moduleModelRaw = hasModule\n ? readModuleModelEnvOverride(env, input.moduleId!)\n : null\n const agentModelRaw = normalizeOverride(input.agentDefaultModel)\n // OM_AI_MODEL is canonical; the legacy OPENCODE_MODEL is read as a\n // backward-compatibility fallback through readGlobalModelFromEnv.\n const globalModelRaw = readGlobalModelFromEnv(env)\n\n const providerOverrideRaw = normalizeOverride(input.providerOverride)\n const moduleProviderRaw = hasModule\n ? readModuleProviderEnvOverride(env, input.moduleId!)\n : null\n const agentDefaultProviderRaw = normalizeOverride(input.agentDefaultProvider)\n // OM_AI_PROVIDER is canonical; the legacy OPENCODE_PROVIDER is read as\n // a backward-compatibility fallback through readGlobalProviderFromEnv.\n const globalProviderRaw = readGlobalProviderFromEnv(env, registry)\n\n const requestProviderHint = normalizeProviderHint(requestProviderRaw, registry)\n const providerOverrideHint = normalizeProviderHint(providerOverrideRaw, registry)\n const tenantProviderHint = normalizeProviderHint(tenantProviderRaw, registry)\n const moduleProviderHint = normalizeProviderHint(moduleProviderRaw, registry)\n const agentDefaultProviderHint = normalizeProviderHint(agentDefaultProviderRaw, registry)\n\n // Parse each model-axis source with its same-tier provider hint so a\n // configured vendor-prefix gateway (OpenRouter, Requesty, LiteLLM) keeps\n // its `vendor/model` id intact instead of pinning the native `vendor`.\n const requestModelParsed = requestModelRaw ? parseTierModel(requestModelRaw, requestProviderHint, registry, env) : null\n const callerParsed = callerRaw ? parseTierModel(callerRaw, providerOverrideHint, registry, env) : null\n const tenantModelParsed = tenantModelRaw ? parseTierModel(tenantModelRaw, tenantProviderHint, registry, env) : null\n const moduleModelParsed = moduleModelRaw ? parseTierModel(moduleModelRaw, moduleProviderHint, registry, env) : null\n const agentModelParsed = agentModelRaw ? parseTierModel(agentModelRaw, agentDefaultProviderHint, registry, env) : null\n const globalModelParsed = globalModelRaw ? parseTierModel(globalModelRaw, globalProviderRaw, registry, env) : null\n\n // Walk the provider-axis seed list: slash hint beats plain provider at\n // the same step. We keep only the first (highest-priority) non-null hint.\n const providerHintCandidates: Array<string | null> = [\n requestModelParsed?.providerHint ?? null,\n requestProviderHint,\n callerParsed?.providerHint ?? null,\n providerOverrideHint,\n tenantModelParsed?.providerHint ?? null,\n tenantProviderHint,\n moduleModelParsed?.providerHint ?? null,\n moduleProviderHint,\n agentModelParsed?.providerHint ?? null,\n agentDefaultProviderHint,\n globalModelParsed?.providerHint ?? null,\n globalProviderRaw,\n ]\n const orderHint = providerHintCandidates.find((hint) => hint !== null) ?? null\n const order = orderHint ? [orderHint] : undefined\n\n const pairPlainProviderIfWinning = (providerHint: string | null): string | null =>\n providerHint && providerHint === orderHint ? providerHint : null\n\n let modelId: string\n let source: AiModelResolution['source']\n let pairedProviderHint: string | null = null\n if (requestModelParsed) {\n modelId = requestModelParsed.modelId\n source = 'request_override'\n pairedProviderHint = requestModelParsed.providerHint ?? pairPlainProviderIfWinning(requestProviderHint)\n } else if (callerParsed) {\n modelId = callerParsed.modelId\n source = 'caller_override'\n pairedProviderHint = callerParsed.providerHint ?? pairPlainProviderIfWinning(providerOverrideHint)\n } else if (tenantModelParsed) {\n modelId = tenantModelParsed.modelId\n source = 'tenant_override'\n pairedProviderHint = tenantModelParsed.providerHint ?? pairPlainProviderIfWinning(tenantProviderHint)\n } else if (moduleModelParsed) {\n modelId = moduleModelParsed.modelId\n source = 'module_env'\n pairedProviderHint = moduleModelParsed.providerHint ?? pairPlainProviderIfWinning(moduleProviderHint)\n } else if (agentModelParsed) {\n modelId = agentModelParsed.modelId\n source = 'agent_default'\n pairedProviderHint = agentModelParsed.providerHint ?? pairPlainProviderIfWinning(agentDefaultProviderHint)\n } else if (globalModelParsed) {\n modelId = globalModelParsed.modelId\n source = 'env_default'\n pairedProviderHint = globalModelParsed.providerHint ?? pairPlainProviderIfWinning(globalProviderRaw)\n } else {\n modelId = ''\n source = 'provider_default'\n }\n\n // --- Provider-axis: walk from highest to lowest priority for the seed.\n // A slash-qualified hint from a model source wins over a plain provider\n // source at the same priority step. We walk top-down and take the first\n // non-null hint.\n const provider = pairedProviderHint\n ? resolveRequiredProvider(pairedProviderHint, registry, env)\n : registry.resolveFirstConfigured({ env, order })\n if (!provider) {\n if (pairedProviderHint) {\n throw new AiModelFactoryError(\n 'no_provider_configured',\n requiredProviderMessage(pairedProviderHint, registry, env),\n )\n }\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 // --- Model-axis: use the post-parse model id from the winning source.\n if (source === 'provider_default') {\n modelId = provider.defaultModel\n }\n\n // --- BaseURL-axis resolution (highest to lowest priority) ---\n // 1. requestOverride.baseURL (HTTP dispatcher) \u2014 gated by allowRuntimeOverride\n // 2. baseUrlOverride (programmatic caller)\n // 3. tenantOverride.baseURL (DB row) \u2014 gated by allowRuntimeOverride\n // 4. <MODULE>_AI_BASE_URL env\n // 5. agentDefaultBaseUrl\n // Steps 6-7 (preset env + preset default) are handled inside the adapter's\n // createModel when no explicit baseURL is passed.\n const resolvedBaseURL = requestBaseUrlRaw\n ?? normalizeOverride(input.baseUrlOverride)\n ?? tenantBaseUrlRaw\n ?? (hasModule ? normalizeOverride(env[moduleBaseUrlEnvVarName(input.moduleId!)]) : null)\n ?? normalizeOverride(input.agentDefaultBaseUrl)\n ?? undefined\n\n // --- Allowlist enforcement (Phase 1780-5 + 1780-6) -------------------\n // OM_AI_AVAILABLE_PROVIDERS / OM_AI_AVAILABLE_MODELS_<PROVIDER> clip\n // the resolution to an operator-approved set. The optional tenant\n // allowlist snapshot narrows the env outer constraint further. If the\n // resolved pair isn't allowed, fall back to a safe (provider, model)\n // \u2014 never throw, so a stale tenant override or chat picker can't take\n // the runtime down. The fallback is logged so the operator can see\n // what happened.\n const registryProviderIds = registry.list?.()?.map((p) => p.id) ?? []\n const tenantProviderIds = input.tenantAllowlist\n ? Object.keys(input.tenantAllowlist.allowedModelsByProvider ?? {})\n : []\n const knownProviderIds = Array.from(\n new Set([provider.id, ...registryProviderIds, ...tenantProviderIds]),\n )\n const effectiveAllowlist = intersectAllowlists(\n env,\n knownProviderIds,\n input.tenantAllowlist ?? null,\n )\n const allowlistResult = enforceAllowlist({\n env,\n registry,\n resolved: { provider, modelId },\n agentDefaultProvider: agentDefaultProviderRaw,\n agentDefaultModel: agentModelParsed?.modelId ?? agentModelRaw,\n effective: effectiveAllowlist,\n })\n\n const finalProvider = allowlistResult.provider\n const finalModelId = allowlistResult.modelId\n const finalSource = allowlistResult.fallback ? 'allowlist_fallback' : source\n const finalApiKey = allowlistResult.fallback\n ? finalProvider.resolveApiKey(env)\n : apiKey\n if (!finalApiKey) {\n throw new AiModelFactoryError(\n 'api_key_missing',\n `LLM provider \"${finalProvider.id}\" is advertised as configured but resolveApiKey() returned empty.`,\n )\n }\n\n const endUserIdentifier = normalizeOverride(input.endUserIdentifier)\n const providerOptions =\n endUserIdentifier && finalProvider.mapEndUserIdentifier\n ? finalProvider.mapEndUserIdentifier(endUserIdentifier)\n : undefined\n\n const model = finalProvider.createModel({\n modelId: finalModelId,\n apiKey: finalApiKey,\n baseURL: resolvedBaseURL,\n ...(endUserIdentifier ? { endUserIdentifier } : {}),\n })\n return {\n model,\n modelId: finalModelId,\n providerId: finalProvider.id,\n source: finalSource,\n supportsInputModeration: finalProvider.supportsInputModeration === true,\n ...(resolvedBaseURL !== undefined ? { baseURL: resolvedBaseURL } : {}),\n ...(providerOptions !== undefined ? { providerOptions } : {}),\n ...(allowlistResult.fallback\n ? {\n allowlistFallback: {\n originalProviderId: provider.id,\n originalModelId: modelId,\n reason: allowlistResult.fallback,\n },\n }\n : {}),\n }\n },\n }\n}\n\ninterface EnforceAllowlistInput {\n env: EnvLookup\n registry: AiModelFactoryRegistry\n resolved: { provider: LlmProvider; modelId: string }\n agentDefaultProvider: string | null\n agentDefaultModel: string | null\n effective: EffectiveAllowlist\n}\n\ninterface EnforceAllowlistResult {\n provider: LlmProvider\n modelId: string\n /** Populated only when the resolved pair was rejected. */\n fallback: string | null\n}\n\n/**\n * Clips a resolved `(provider, model)` to what the effective allowlist\n * permits (env intersected with optional tenant allowlist).\n *\n * Order of fallback when the resolved provider is not allowed:\n * 1. The agent's `defaultProvider` (if allowed and configured).\n * 2. The first allowed provider that is also configured in the registry.\n *\n * Order of fallback when the model is not allowed for the resolved provider:\n * 1. The agent's `defaultModel` (if allowed for that provider).\n * 2. The provider's `defaultModel` (if allowed).\n * 3. The first model from the effective allowlist for that provider.\n *\n * Both fall-back paths emit a logger warning so the operator can see why the\n * runtime did not honor the requested combination. The function never throws.\n */\nfunction enforceAllowlist(input: EnforceAllowlistInput): EnforceAllowlistResult {\n const { registry, resolved, agentDefaultProvider, agentDefaultModel, effective } = input\n let provider = resolved.provider\n let modelId = resolved.modelId\n let fallback: string | null = null\n\n if (effective.providers !== null && !isProviderAllowedInEffective(effective, provider.id)) {\n const replacement = pickAllowedProvider({\n registry,\n agentDefaultProvider,\n effective,\n })\n if (replacement) {\n const source = effective.tenantOverridesActive\n ? 'the effective allowlist (env \u2229 tenant)'\n : 'OM_AI_AVAILABLE_PROVIDERS'\n fallback = `Provider \"${provider.id}\" is not in ${source}; using \"${replacement.id}\" instead.`\n logger.warn('Allowlist provider fallback', { reason: fallback })\n provider = replacement\n modelId = pickAllowedModel({\n provider,\n preferred: agentDefaultModel,\n effective,\n })\n }\n // If no replacement is configured we keep the resolved provider \u2014 the\n // throw at the api-key gate will surface the misconfiguration to the\n // operator instead of silently masking it.\n }\n\n if (!isModelAllowedForProviderInEffective(effective, provider.id, modelId)) {\n const replacementModel = pickAllowedModel({\n provider,\n preferred: agentDefaultModel,\n effective,\n })\n if (replacementModel !== modelId) {\n const source = effective.tenantOverridesActive\n ? `the effective allowlist (env \u2229 tenant) for \"${provider.id}\"`\n : `OM_AI_AVAILABLE_MODELS_${provider.id.toUpperCase()}`\n const reason = `Model \"${modelId}\" is not in ${source}; using \"${replacementModel}\" instead.`\n logger.warn('Allowlist model fallback', { reason })\n fallback = fallback ? `${fallback} ${reason}` : reason\n modelId = replacementModel\n }\n }\n\n return { provider, modelId, fallback }\n}\n\nfunction pickAllowedProvider(input: {\n registry: AiModelFactoryRegistry\n agentDefaultProvider: string | null\n effective: EffectiveAllowlist\n}): LlmProvider | null {\n const { registry, agentDefaultProvider, effective } = input\n if (agentDefaultProvider) {\n if (isProviderAllowedInEffective(effective, agentDefaultProvider)) {\n const provider = registry.get?.(agentDefaultProvider)\n if (provider && provider.isConfigured(process.env as EnvLookup)) return provider\n }\n }\n const allowed = effective.providers\n if (!allowed) return null\n for (const id of allowed) {\n const provider = registry.get?.(id)\n if (provider && provider.isConfigured(process.env as EnvLookup)) return provider\n }\n return null\n}\n\nfunction pickAllowedModel(input: {\n provider: LlmProvider\n preferred: string | null\n effective: EffectiveAllowlist\n}): string {\n const { provider, preferred, effective } = input\n const allowed = effective.modelsByProvider[provider.id]\n if (allowed === undefined) {\n return preferred && preferred.length > 0 ? preferred : provider.defaultModel\n }\n if (preferred && allowed.includes(preferred)) return preferred\n if (allowed.includes(provider.defaultModel)) return provider.defaultModel\n return allowed[0] ?? provider.defaultModel\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ai-assistant",
3
- "version": "0.7.1-develop.7123.1.25365e718d",
3
+ "version": "0.7.1-develop.7130.1.fef2396fd8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -99,16 +99,16 @@
99
99
  "zod-to-json-schema": "^3.25.2"
100
100
  },
101
101
  "peerDependencies": {
102
- "@open-mercato/shared": "0.7.1-develop.7123.1.25365e718d",
103
- "@open-mercato/ui": "0.7.1-develop.7123.1.25365e718d",
102
+ "@open-mercato/shared": "0.7.1-develop.7130.1.fef2396fd8",
103
+ "@open-mercato/ui": "0.7.1-develop.7130.1.fef2396fd8",
104
104
  "react": "^19.0.0",
105
105
  "react-dom": "^19.0.0",
106
106
  "zod": ">=3.23.0"
107
107
  },
108
108
  "devDependencies": {
109
- "@open-mercato/cli": "0.7.1-develop.7123.1.25365e718d",
110
- "@open-mercato/shared": "0.7.1-develop.7123.1.25365e718d",
111
- "@open-mercato/ui": "0.7.1-develop.7123.1.25365e718d",
109
+ "@open-mercato/cli": "0.7.1-develop.7130.1.fef2396fd8",
110
+ "@open-mercato/shared": "0.7.1-develop.7130.1.fef2396fd8",
111
+ "@open-mercato/ui": "0.7.1-develop.7130.1.fef2396fd8",
112
112
  "@types/react": "^19.2.17",
113
113
  "@types/react-dom": "^19.2.3",
114
114
  "react": "19.2.8",
@@ -6,7 +6,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
6
6
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
7
7
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
8
8
  import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
9
- import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
9
+ import { llmProviderRegistry } from '../../../../../lib/llm-registry'
10
10
  import { getAgent, loadAgentRegistry } from '../../../../../lib/agent-registry'
11
11
  import { hasRequiredFeatures } from '../../../../../lib/auth'
12
12
  import { createModelFactory, resolveAllowRuntimeOverride } from '../../../../../lib/model-factory'
@@ -0,0 +1,69 @@
1
+ import type { NextRequest } from 'next/server'
2
+
3
+ const authMock = jest.fn()
4
+ const createRequestContainerMock = jest.fn()
5
+ const loadAclMock = jest.fn()
6
+
7
+ jest.mock('@open-mercato/shared/lib/auth/server', () => ({
8
+ getAuthFromRequest: (...args: unknown[]) => authMock(...args),
9
+ }))
10
+
11
+ jest.mock('@open-mercato/shared/lib/di/container', () => ({
12
+ createRequestContainer: (...args: unknown[]) => createRequestContainerMock(...args),
13
+ }))
14
+
15
+ jest.mock('../../../../lib/agent-registry', () => ({
16
+ loadAgentRegistry: jest.fn(async () => undefined),
17
+ listAgents: jest.fn(() => []),
18
+ isAgentTaskPlanEnabled: jest.fn(() => false),
19
+ }))
20
+
21
+ import { GET } from '../route'
22
+
23
+ const ORIGINAL_ENV = process.env
24
+
25
+ function envWithoutProviderKeys(): NodeJS.ProcessEnv {
26
+ const stripped: NodeJS.ProcessEnv = {}
27
+ for (const [key, value] of Object.entries(ORIGINAL_ENV)) {
28
+ if (/API_KEY/i.test(key)) continue
29
+ stripped[key] = value
30
+ }
31
+ return stripped
32
+ }
33
+
34
+ async function callGet(): Promise<{ status: number; body: { aiConfigured?: boolean } }> {
35
+ const response = await GET({} as NextRequest)
36
+ return { status: response.status, body: await response.json() }
37
+ }
38
+
39
+ describe('GET /api/ai_assistant/ai/agents — aiConfigured', () => {
40
+ beforeEach(() => {
41
+ jest.clearAllMocks()
42
+ process.env = envWithoutProviderKeys()
43
+ authMock.mockResolvedValue({ sub: 'user-1', tenantId: 'tenant-1', orgId: 'org-1' })
44
+ loadAclMock.mockResolvedValue({ features: ['ai_assistant.view'], isSuperAdmin: false })
45
+ createRequestContainerMock.mockResolvedValue({
46
+ resolve: () => ({ loadAcl: loadAclMock }),
47
+ })
48
+ })
49
+
50
+ afterEach(() => {
51
+ process.env = ORIGINAL_ENV
52
+ })
53
+
54
+ it('reports aiConfigured true on a cold process when a provider key is set', async () => {
55
+ process.env.OPENAI_API_KEY = 'sk-test-key'
56
+
57
+ const { status, body } = await callGet()
58
+
59
+ expect(status).toBe(200)
60
+ expect(body.aiConfigured).toBe(true)
61
+ })
62
+
63
+ it('reports aiConfigured false when no provider key is set', async () => {
64
+ const { status, body } = await callGet()
65
+
66
+ expect(status).toBe(200)
67
+ expect(body.aiConfigured).toBe(false)
68
+ })
69
+ })
@@ -4,7 +4,7 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
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 type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
7
- import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
7
+ import { llmProviderRegistry } from '../../../lib/llm-registry'
8
8
  import { isAgentTaskPlanEnabled, listAgents, loadAgentRegistry } from '../../../lib/agent-registry'
9
9
  import { hasRequiredFeatures } from '../../../lib/auth'
10
10
  import { toolRegistry } from '../../../lib/tool-registry'
@@ -6,7 +6,7 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
6
6
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
7
7
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
8
8
  import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
9
- import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
9
+ import { llmProviderRegistry } from '../../../lib/llm-registry'
10
10
  import { loadAgentRegistry } from '../../../lib/agent-registry'
11
11
  import { checkAgentPolicy, type AgentPolicyDenyCode } from '../../../lib/agent-policy'
12
12
  import {
@@ -5,7 +5,7 @@ import { generateObject } from '../../lib/ai-sdk'
5
5
  import { z } from 'zod'
6
6
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
7
7
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
8
- import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
8
+ import { llmProviderRegistry } from '../../lib/llm-registry'
9
9
  import { resolveOpenCodeModel } from '@open-mercato/shared/lib/ai/opencode-provider'
10
10
  import { joinProviderModel } from '@open-mercato/shared/lib/ai/model-id'
11
11
  import {
@@ -6,7 +6,7 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
6
6
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
7
7
  import type { EntityManager } from '@mikro-orm/postgresql'
8
8
  import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
9
- import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
9
+ import { llmProviderRegistry } from '../../../lib/llm-registry'
10
10
  import { AiTenantModelAllowlistRepository } from '../../../data/repositories/AiTenantModelAllowlistRepository'
11
11
  import {
12
12
  canonicalProviderId,
@@ -8,7 +8,7 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
8
8
  import type { EntityManager } from '@mikro-orm/postgresql'
9
9
  import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
10
10
  import { findApiKeyBySecret } from '@open-mercato/core/modules/api_keys/services/apiKeyService'
11
- import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
11
+ import { llmProviderRegistry } from '../../lib/llm-registry'
12
12
  import { joinProviderModel } from '@open-mercato/shared/lib/ai/model-id'
13
13
  import {
14
14
  OPEN_CODE_PROVIDER_IDS,
@@ -1,5 +1,5 @@
1
1
  import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
2
- import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
2
+ import { llmProviderRegistry } from '../../lib/llm-registry'
3
3
  import { canonicalProviderId } from '../../lib/model-allowlist'
4
4
  import { AiAgentRuntimeOverride } from '../entities'
5
5
  import type { AiAgentLoopStopCondition } from '../../lib/ai-agent-definition'
@@ -0,0 +1,42 @@
1
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
2
+ import { join, relative, sep } from 'node:path'
3
+
4
+ const MODULE_ROOT = join(__dirname, '..', '..')
5
+
6
+ const ALLOWED_DIRECT_IMPORTERS = new Set([
7
+ join('lib', 'llm-bootstrap.ts'),
8
+ join('lib', 'llm-registry.ts'),
9
+ ])
10
+
11
+ const VALUE_IMPORT_PATTERN =
12
+ /import\s+\{[^}]*\bllmProviderRegistry\b[^}]*\}\s+from\s+'@open-mercato\/shared\/lib\/ai\/llm-provider-registry'/
13
+
14
+ function collectSourceFiles(dir: string, found: string[] = []): string[] {
15
+ for (const entry of readdirSync(dir)) {
16
+ const absolute = join(dir, entry)
17
+ if (statSync(absolute).isDirectory()) {
18
+ if (entry === '__tests__' || entry === 'node_modules') continue
19
+ collectSourceFiles(absolute, found)
20
+ continue
21
+ }
22
+ if (entry.endsWith('.ts') || entry.endsWith('.tsx')) found.push(absolute)
23
+ }
24
+ return found
25
+ }
26
+
27
+ describe('llmProviderRegistry import discipline', () => {
28
+ it('routes every registry consumer through the bootstrapped lib/llm-registry accessor', () => {
29
+ const offenders = collectSourceFiles(MODULE_ROOT)
30
+ .filter((absolute) => VALUE_IMPORT_PATTERN.test(readFileSync(absolute, 'utf8')))
31
+ .map((absolute) => relative(MODULE_ROOT, absolute))
32
+ .filter((relativePath) => !ALLOWED_DIRECT_IMPORTERS.has(relativePath))
33
+ .map(
34
+ (relativePath) =>
35
+ `${relativePath.split(sep).join('/')} imports llmProviderRegistry from ` +
36
+ '@open-mercato/shared/lib/ai/llm-provider-registry; import it from lib/llm-registry ' +
37
+ 'instead so ./llm-bootstrap runs before the registry is read',
38
+ )
39
+
40
+ expect(offenders).toEqual([])
41
+ })
42
+ })
@@ -1,5 +1,5 @@
1
1
  import { createLogger } from '@open-mercato/shared/lib/logger'
2
- import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
2
+ import { llmProviderRegistry } from './llm-registry'
3
3
  import type { AiAgentDefinition, AiAgentExtension, AiAgentSuggestion } from './ai-agent-definition'
4
4
  import {
5
5
  applyAgentOverrideMap,
@@ -28,7 +28,7 @@ import type { StopCondition } from 'ai'
28
28
  import type { ZodTypeAny } from 'zod'
29
29
  import { createModelFactory, resolveAllowRuntimeOverride } from './model-factory'
30
30
  import { computeEndUserIdentifier } from '@open-mercato/shared/lib/ai/safety-identifier'
31
- import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
31
+ import { llmProviderRegistry } from './llm-registry'
32
32
  import type { EnvLookup } from '@open-mercato/shared/lib/ai/llm-provider'
33
33
  import {
34
34
  AiModerationBlockedError,
@@ -79,10 +79,6 @@ import { recordTokenUsage } from './token-usage-recorder'
79
79
  import { injectTaskPlanIntoStream } from './task-plan-stream'
80
80
  import { TASK_PLAN_RUNTIME_PROMPT_SECTION } from './task-plan-labels'
81
81
 
82
- // Ensure built-in LLM providers are registered. Side-effect import; identical to
83
- // what `./ai-sdk.ts` consumers already rely on.
84
- import './llm-bootstrap'
85
-
86
82
  const logger = createLogger('ai_assistant').child({ component: 'agent-runtime' })
87
83
 
88
84
  export interface AgentRequestPageContext {
@@ -1,9 +1,6 @@
1
1
  import type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'
2
- import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
2
+ import { llmProviderRegistry } from './llm-registry'
3
3
  import type { LlmProvider } from '@open-mercato/shared/lib/ai/llm-provider'
4
- // Side-effect: ensures the registry is populated with built-in adapters
5
- // and OpenAI-compatible presets before this module's getters run.
6
- import './llm-bootstrap'
7
4
 
8
5
  // Types
9
6
  //
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Bootstrapped access to the shared `llmProviderRegistry` singleton.
3
+ *
4
+ * The registry in `@open-mercato/shared` is a plain module-level singleton
5
+ * with no self-registration: built-in adapters and OpenAI-compatible presets
6
+ * only exist after `./llm-bootstrap` has been evaluated. A module that reads
7
+ * the shared singleton directly therefore sees an empty registry whenever it
8
+ * happens to be the first thing a cold process loads, and
9
+ * `resolveFirstConfigured()` reports "no provider configured" even though the
10
+ * environment carries valid credentials.
11
+ *
12
+ * Every module inside `ai_assistant` imports the registry from here so that
13
+ * bootstrap is guaranteed by the import graph rather than by which route the
14
+ * user happens to hit first. `lib/__tests__/llm-registry-imports.test.ts`
15
+ * enforces that rule.
16
+ *
17
+ * @see ./llm-bootstrap.ts
18
+ * @see packages/shared/src/lib/ai/llm-provider-registry.ts
19
+ */
20
+
21
+ import './llm-bootstrap'
22
+
23
+ export {
24
+ llmProviderRegistry,
25
+ type LlmProviderRegistry,
26
+ type ResolveFirstConfiguredOptions,
27
+ } from '@open-mercato/shared/lib/ai/llm-provider-registry'
@@ -64,7 +64,7 @@
64
64
  import { createLogger } from '@open-mercato/shared/lib/logger'
65
65
  import type { AwilixContainer } from 'awilix'
66
66
  import type { EnvLookup, LlmProvider } from '@open-mercato/shared/lib/ai/llm-provider'
67
- import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
67
+ import { llmProviderRegistry } from './llm-registry'
68
68
  import {
69
69
  intersectAllowlists,
70
70
  canonicalProviderId,