@open-mercato/ai-assistant 0.6.7 → 0.6.8-develop.6874.1.982d6097d8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +12 -29
- package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.js +80 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.js +124 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js +30 -0
- package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +7 -0
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/moderation-flags/route.js +121 -0
- package/dist/modules/ai_assistant/api/moderation-flags/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js +10 -10
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/settings/route.js +34 -13
- package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +121 -15
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js +139 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.js +26 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +4 -4
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/data/entities.js +42 -0
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +3 -1
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiModerationFlagRepository.js +44 -0
- package/dist/modules/ai_assistant/data/repositories/AiModerationFlagRepository.js.map +7 -0
- package/dist/modules/ai_assistant/di.js +6 -2
- package/dist/modules/ai_assistant/di.js.map +2 -2
- package/dist/modules/ai_assistant/events.js +6 -0
- package/dist/modules/ai_assistant/events.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +42 -0
- package/dist/modules/ai_assistant/i18n/en.json +42 -0
- package/dist/modules/ai_assistant/i18n/es.json +42 -0
- package/dist/modules/ai_assistant/i18n/ko.json +527 -0
- package/dist/modules/ai_assistant/i18n/pl.json +42 -0
- package/dist/modules/ai_assistant/lib/agent-runtime.js +158 -5
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
- package/dist/modules/ai_assistant/lib/auth.js +6 -7
- package/dist/modules/ai_assistant/lib/auth.js.map +2 -2
- package/dist/modules/ai_assistant/lib/entity-graph.js +3 -3
- package/dist/modules/ai_assistant/lib/entity-graph.js.map +2 -2
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js +3 -0
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +2 -2
- package/dist/modules/ai_assistant/lib/llm-adapters/openai.js +8 -1
- package/dist/modules/ai_assistant/lib/llm-adapters/openai.js.map +2 -2
- package/dist/modules/ai_assistant/lib/model-factory.js +6 -1
- package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
- package/dist/modules/ai_assistant/lib/moderation-flag-recorder.js +43 -0
- package/dist/modules/ai_assistant/lib/moderation-flag-recorder.js.map +7 -0
- package/dist/modules/ai_assistant/lib/moderation-policy.js +24 -0
- package/dist/modules/ai_assistant/lib/moderation-policy.js.map +7 -0
- package/dist/modules/ai_assistant/lib/moderation.js +109 -0
- package/dist/modules/ai_assistant/lib/moderation.js.map +7 -0
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +2 -0
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-search.js +4 -12
- package/dist/modules/ai_assistant/lib/tool-search.js.map +2 -2
- package/dist/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.js +19 -0
- package/dist/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.js.map +7 -0
- package/generated/entities/ai_agent_runtime_override/index.ts +1 -0
- package/generated/entities/ai_moderation_flag/index.ts +9 -0
- package/generated/entities.ids.generated.ts +2 -1
- package/generated/entity-fields-registry.ts +13 -1
- package/package.json +13 -12
- package/src/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.ts +126 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.ts +161 -0
- package/src/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.ts +48 -0
- package/src/modules/ai_assistant/api/ai/chat/route.ts +10 -0
- package/src/modules/ai_assistant/api/moderation-flags/__tests__/route.test.ts +122 -0
- package/src/modules/ai_assistant/api/moderation-flags/route.ts +137 -0
- package/src/modules/ai_assistant/api/settings/__tests__/route.test.ts +58 -8
- package/src/modules/ai_assistant/api/settings/allowlist/route.ts +10 -12
- package/src/modules/ai_assistant/api/settings/route.ts +33 -10
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +161 -15
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +2 -2
- package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.tsx +186 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.ts +24 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.tsx +12 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +4 -4
- package/src/modules/ai_assistant/data/entities.ts +56 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +4 -0
- package/src/modules/ai_assistant/data/repositories/AiModerationFlagRepository.ts +79 -0
- package/src/modules/ai_assistant/di.ts +5 -1
- package/src/modules/ai_assistant/events.ts +21 -0
- package/src/modules/ai_assistant/i18n/de.json +42 -0
- package/src/modules/ai_assistant/i18n/en.json +42 -0
- package/src/modules/ai_assistant/i18n/es.json +42 -0
- package/src/modules/ai_assistant/i18n/ko.json +527 -0
- package/src/modules/ai_assistant/i18n/pl.json +42 -0
- package/src/modules/ai_assistant/lib/__tests__/auth.test.ts +2 -5
- package/src/modules/ai_assistant/lib/__tests__/input-moderation-gate.test.ts +121 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-anthropic.test.ts +13 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +41 -0
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +60 -0
- package/src/modules/ai_assistant/lib/__tests__/moderation-flag-recorder.test.ts +76 -0
- package/src/modules/ai_assistant/lib/__tests__/moderation-policy.test.ts +86 -0
- package/src/modules/ai_assistant/lib/__tests__/moderation.test.ts +114 -0
- package/src/modules/ai_assistant/lib/agent-runtime.ts +259 -0
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +10 -0
- package/src/modules/ai_assistant/lib/auth.ts +6 -15
- package/src/modules/ai_assistant/lib/entity-graph.ts +3 -3
- package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +10 -0
- package/src/modules/ai_assistant/lib/llm-adapters/openai.ts +32 -1
- package/src/modules/ai_assistant/lib/model-factory.ts +33 -0
- package/src/modules/ai_assistant/lib/moderation-flag-recorder.ts +75 -0
- package/src/modules/ai_assistant/lib/moderation-policy.ts +77 -0
- package/src/modules/ai_assistant/lib/moderation.ts +179 -0
- package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +2 -0
- package/src/modules/ai_assistant/lib/tool-search.ts +4 -17
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +388 -16
- package/src/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.ts +21 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/ai-agent-definition.ts"],
|
|
4
|
-
"sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { ZodTypeAny } from 'zod'\nimport type {\n PrepareStepFunction,\n GenerateTextOnStepFinishCallback,\n GenerateTextOnStepStartCallback,\n OnToolExecutionStartCallback,\n OnToolExecutionEndCallback,\n ToolCallRepairFunction,\n StopCondition,\n ToolChoice,\n ToolSet,\n} from 'ai'\n\nexport type AiAgentExecutionMode = 'chat' | 'object'\n\n/**\n * Selects the underlying Vercel AI SDK dispatch strategy for this agent.\n *\n * - `'stream-text'` (default): the runtime calls `streamText(...)` directly on\n * every turn. All loop primitives are supported: `prepareStep`, `stopWhen`,\n * `repairToolCall`, `activeTools`, `toolChoice`.\n *\n * - `'tool-loop-agent'`: the runtime constructs a `ToolLoopAgent`\n * (`Experimental_Agent`) once and dispatches via `agent.generate(...)` /\n * `agent.stream(...)` per turn. The wrapper-owned `prepareStep` (security-\n * critical for mutation-approval) is supplied at construction via\n * `settings.prepareStep`. `stopWhen` is similarly wired at construction.\n * The `prepareCall` hook is used for per-turn narrowing of `model`, `tools`,\n * `stopWhen`, `activeTools`, and `providerOptions`; `prepareStep` is NOT in\n * its `Pick` list and MUST NOT be threaded through it.\n *\n * Note: the current SDK version ships `experimental_repairToolCall` on\n * `ToolLoopAgentSettings`, so `repairToolCall` is technically reachable via\n * this engine. The `loop.repairToolCall` JSDoc retains a caveat reflecting\n * the spec's documented limitation, which was written against an earlier SDK\n * snapshot where the setting was absent \u2014 use with awareness that SDK\n * behaviour may differ across versions.\n *\n * Phase 5 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nexport type AiAgentExecutionEngine = 'stream-text' | 'tool-loop-agent'\n\n/**\n * A serializable stop condition for the agentic loop. The `kind` field\n * determines which Vercel AI SDK helper is used at runtime:\n * - `stepCount` \u2192 `stepCountIs(count)` \u2014 the loop stops after N steps.\n * - `hasToolCall` \u2192 `hasToolCall(toolName)` \u2014 the loop stops immediately\n * after the model emits a tool call for the named tool.\n * - `custom` \u2014 a raw `StopCondition<ToolSet>` predicate supplied in code.\n * NOT valid from JSON-only override sources (tenant DB overrides); only\n * accepted when declared directly in `agent.loop` or a `runAiAgentText`\n * caller override.\n *\n * Phase 0 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nexport type AiAgentLoopStopCondition =\n | { kind: 'stepCount'; count: number }\n | { kind: 'hasToolCall'; toolName: string }\n | { kind: 'custom'; stop: StopCondition<ToolSet> }\n\n/**\n * Budget limits for the agentic loop turn. When any limit is exceeded the\n * wrapper's `prepareStep`/`onStepFinish` aborts the turn via the per-turn\n * `AbortController` and the loop terminates with a `loop_budget_exceeded`\n * finish condition.\n *\n * Budget enforcement is implemented in Phase 1782-3; for Phases 0\u20132 the\n * fields are accepted and forwarded to the prepared-options bag but are not\n * actively enforced.\n *\n * Phase 0 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nexport interface AiAgentLoopBudget {\n /** Hard cap on tool calls across all steps in this turn. */\n maxToolCalls?: number\n /** Wall-clock cap (ms) per turn; runtime aborts via AbortController. */\n maxWallClockMs?: number\n /** Input+output token cap; aggregated from step `usage` fields. */\n maxTokens?: number\n}\n\n/**\n * First-class loop configuration for an AI agent. Supersedes the flat\n * `maxSteps` alias on `AiAgentDefinition`.\n *\n * All fields are optional; the runtime falls back to the wrapper default\n * (`{ maxSteps: 10 }` for chat, `{ maxSteps: undefined }` for object) when\n * neither the agent nor the caller supplies any loop config.\n *\n * Phase 0 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nexport interface AiAgentLoopConfig {\n /** Maximum number of agentic steps before the loop is forced to stop. */\n maxSteps?: number\n /**\n * Additional stop conditions. The wrapper ALWAYS composes these with\n * `stepCountIs(maxSteps ?? 10)` so a misconfigured `hasToolCall` for a\n * non-existent tool can never cause an infinite loop (R3 mitigation).\n */\n stopWhen?: AiAgentLoopStopCondition | AiAgentLoopStopCondition[]\n /**\n * Per-step preparation hook. The wrapper composes this with its own\n * security-critical `prepareStep` that re-asserts the tool allowlist and\n * mutation-approval wrapping per step.\n *\n * Only valid for chat agents. Rejected with `loop_unsupported_in_object_mode`\n * for object-mode agents.\n */\n prepareStep?: PrepareStepFunction<ToolSet>\n /**\n * Callback fired when a step finishes. The wrapper chains its own\n * aggregation callback (LoopTrace builder) before invoking this one.\n * Exceptions thrown by this callback are caught and logged but do not\n * abort the turn (matching the SDK's own contract).\n */\n onStepFinish?: GenerateTextOnStepFinishCallback<ToolSet>\n /**\n * Callback fired when a step starts. Forwarded to the AI SDK as\n * `experimental_onStepStart`.\n */\n onStepStart?: GenerateTextOnStepStartCallback<ToolSet>\n /**\n * Callback fired when a tool call starts. Forwarded to the AI SDK as\n * `experimental_onToolCallStart`.\n */\n onToolCallStart?: OnToolExecutionStartCallback<ToolSet>\n /**\n * Callback fired when a tool call finishes. Forwarded to the AI SDK as\n * `experimental_onToolCallFinish`.\n */\n onToolCallFinish?: OnToolExecutionEndCallback<ToolSet>\n /**\n * Tool-call repair function. Forwarded to the AI SDK as\n * `experimental_repairToolCall`.\n *\n * Only valid for chat agents. Rejected with `loop_unsupported_in_object_mode`\n * for object-mode agents.\n *\n * **Engine note**: this primitive is honored under `executionEngine: 'stream-text'`\n * (default). Agents on `'tool-loop-agent'` may not reliably support\n * `repairToolCall` across all SDK versions \u2014 if you require it, use the\n * default `stream-text` engine until support is confirmed stable on the\n * `ToolLoopAgent` class.\n *\n * Phase 5 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n repairToolCall?: ToolCallRepairFunction<ToolSet>\n /**\n * Narrow the active tool surface for each step. Names must be a subset of\n * `agent.allowedTools`; any names outside the allowlist are filtered out\n * with a `loop:active_tools_filtered` warning.\n *\n * Only valid for chat agents. Rejected with `loop_unsupported_in_object_mode`\n * for object-mode agents.\n */\n activeTools?: string[]\n /**\n * Tool choice strategy forwarded to the AI SDK on each step.\n *\n * Only valid for chat agents. Rejected with `loop_unsupported_in_object_mode`\n * for object-mode agents.\n */\n toolChoice?: ToolChoice<ToolSet>\n /** Budget caps for this loop turn. */\n budget?: AiAgentLoopBudget\n /**\n * When `false`, per-call `runAiAgentText({ loop })` / HTTP query-param\n * overrides are rejected with `AgentPolicyError` code\n * `loop_runtime_override_disabled`. Default is `true` (permissive).\n *\n * Agents that pin a loop policy for correctness reasons (e.g. a\n * `stopWhen: hasToolCall(...)` that must not be bypassed by callers)\n * should set this to `false`.\n */\n allowRuntimeOverride?: boolean\n /**\n * Kill switch \u2014 when `true`, the runtime forces `stopWhen: stepCountIs(1)` and\n * ignores all other loop config. Used by the per-tenant operator override to\n * collapse an agent to a single model call (no tool execution) without\n * disabling the agent entirely.\n *\n * Phase 3 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n disabled?: boolean\n}\n\n/**\n * Per-step record aggregated by the wrapper-owned `onStepFinish` hook into\n * `LoopTrace`. Each completed agentic step produces one record.\n *\n * Phase 4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nexport interface LoopStepRecord {\n stepIndex: number\n /** Model id resolved for this step (relevant when prepareStep swaps models). */\n modelId: string\n toolCalls: Array<{\n toolName: string\n args: unknown\n result?: unknown\n error?: { code: string; message: string }\n repairAttempted: boolean\n durationMs: number\n }>\n /** Raw assistant text emitted in this step. */\n textDelta: string\n usage: { inputTokens: number; outputTokens: number }\n finishReason: 'stop' | 'tool-calls' | 'length' | 'content-filter' | 'error'\n}\n\n/**\n * Per-turn trace aggregated by the wrapper-owned `buildLoopTraceCollector`.\n * Not persisted \u2014 in-memory only; surfaced via the dispatcher SSE stream and\n * the playground/`<AiChat>` debug panel.\n *\n * Phase 4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nexport interface LoopTrace {\n agentId: string\n /**\n * Stable per-conversation id that ties every turn together. Echoed back on\n * the SSE `loop-finish` event so clients can persist it for subsequent turns.\n *\n * Phase 6.2 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n sessionId: string\n turnId: string\n steps: LoopStepRecord[]\n stopReason:\n | 'step-count'\n | 'has-tool-call'\n | 'custom-stop'\n | 'budget-tokens'\n | 'budget-tool-calls'\n | 'budget-wall-clock'\n | 'tenant-disabled'\n | 'finish-reason'\n | 'abort'\n totalDurationMs: number\n totalUsage: { inputTokens: number; outputTokens: number }\n}\n\nexport type AiAgentMutationPolicy =\n | 'read-only'\n | 'confirm-required'\n | 'destructive-confirm-required'\n\nexport type AiAgentAcceptedMediaType = 'image' | 'pdf' | 'file'\n\nexport type AiAgentDataOperation = 'read' | 'search' | 'aggregate'\n\nexport interface AiAgentPageContextInput {\n entityType: string\n recordId: string\n container: AwilixContainer\n tenantId: string | null\n organizationId: string | null\n}\n\nexport interface AiAgentStructuredOutput<TSchema = ZodTypeAny> {\n schemaName: string\n schema: TSchema\n mode?: 'generate' | 'stream'\n}\n\nexport interface AiAgentDataCapabilities {\n entities?: string[]\n operations?: AiAgentDataOperation[]\n searchableFields?: string[]\n}\n\nexport interface AiAgentSuggestion {\n label: string\n prompt: string\n}\n\nexport interface AiAgentTaskPlanConfig {\n /**\n * Enables the optional visible planning helper for this agent. When true,\n * the runtime exposes `meta.update_task_plan` and injects prompt guidance\n * telling the model to set a user-visible plan before domain tools.\n *\n * Defaults to false. CRM/customer agents enable this by default in core;\n * other agents can opt in from their agent definition or extension config.\n */\n enabled?: boolean\n}\n\nexport interface AiAgentDefinition {\n id: string\n moduleId: string\n label: string\n description: string\n systemPrompt: string\n allowedTools: string[]\n suggestions?: AiAgentSuggestion[]\n executionMode?: AiAgentExecutionMode\n /**\n * Selects the underlying Vercel AI SDK dispatch strategy for this agent.\n * Defaults to `'stream-text'` \u2014 the existing behavior and the only engine\n * with unconditional full primitive coverage (`repairToolCall`, all loop\n * controls).\n *\n * Set to `'tool-loop-agent'` to use the `ToolLoopAgent` (`Experimental_Agent`)\n * class, which is closer to a semantic agent abstraction and receives upcoming\n * SDK features (multi-agent handoff, streaming approval responses) first.\n *\n * **Note on `repairToolCall`**: the current SDK version ships\n * `experimental_repairToolCall` on `ToolLoopAgentSettings`, so the primitive\n * is technically available. However, SDK behaviour is not guaranteed to be\n * identical across versions \u2014 prefer `'stream-text'` when `repairToolCall`\n * correctness is critical.\n *\n * This field is opt-in: omitting it leaves the existing `stream-text` path\n * completely unchanged.\n *\n * Phase 5 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n executionEngine?: AiAgentExecutionEngine\n /**\n * Optional provider id this agent prefers (e.g. `'openai'`, `'anthropic'`).\n * Must match a registered `LlmProvider.id`. When the named provider is\n * registered but unconfigured at runtime the factory falls through\n * transparently to the next configured provider.\n *\n * Phase 1 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n defaultProvider?: string\n /**\n * Optional model id fed through `createModelFactory` for this agent.\n * Accepts either a plain model id (`claude-haiku-4-5-20251001`) or a\n * slash-qualified `<provider>/<model>` shorthand (e.g. `openai/gpt-5-mini`).\n * When the slash form is used the prefix must match a registered provider id;\n * the registry-membership guard prevents mis-splitting model ids that already\n * contain slashes (DeepInfra: `meta-llama/Llama-3.3-70B-Instruct-Turbo`).\n *\n * A higher-priority provider source still wins over the slash hint, but a\n * lower-priority one cannot overwrite a slash-qualified model (cross-axis\n * tie-break rule from spec \u00A7Phase-1).\n *\n * Phase 0 and Phase 1 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n defaultModel?: string\n /**\n * Optional base URL this agent prefers for its chosen provider.\n * Sits between the `<MODULE>_AI_BASE_URL` env (step 2 of the public 5-step\n * baseURL hierarchy) and the preset env override (`baseURLEnvKeys`, step 4).\n * Only honoured by adapters that support baseURL (Anthropic Messages-\n * protocol relays, all OpenAI-compatible adapters, Google via\n * @ai-sdk/google \u22653.0). See `packages/ai-assistant/AGENTS.md` \u2192\n * \"baseURL override hierarchy\" for the full numbered chain.\n *\n * Phase 2 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n defaultBaseUrl?: string\n /**\n * When false, per-request HTTP overrides (query params `provider`, `model`,\n * `baseUrl`, `loopBudget`) and the per-tenant settings override stored in\n * `ai_agent_runtime_overrides` are both suppressed. Steps 1 and 3 of the\n * model-factory resolution chain are skipped for this agent, and the\n * `loopBudget` query parameter is ignored by the chat dispatcher.\n *\n * Default is `true` (permissive). Agents that pin a specific model for\n * correctness reasons (e.g. a structured-output agent whose JSON-mode schema\n * only works with one provider) should set this to `false`.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n * Renamed from `allowRuntimeModelOverride` in Phase 4 of spec\n * `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n allowRuntimeOverride?: boolean\n /**\n * @deprecated Use `allowRuntimeOverride` instead. This alias is kept for\n * one minor release and will be removed in a future version. The runtime\n * checks `allowRuntimeOverride` first; if absent it falls back to this field.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n allowRuntimeModelOverride?: boolean\n acceptedMediaTypes?: AiAgentAcceptedMediaType[]\n requiredFeatures?: string[]\n taskPlan?: AiAgentTaskPlanConfig\n uiParts?: string[]\n readOnly?: boolean\n mutationPolicy?: AiAgentMutationPolicy\n /**\n * @deprecated Use `loop.maxSteps` instead. Honored as alias when `loop` is\n * omitted. When both `maxSteps` and `loop.maxSteps` are specified, `loop.maxSteps`\n * wins. This field will be removed in a future minor release.\n *\n * Phase 0 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n maxSteps?: number\n /**\n * First-class agentic loop configuration. Supersedes the flat `maxSteps`\n * alias. The runtime walks a precedence chain (per-call override \u2192 tenant\n * DB override \u2192 this block \u2192 legacy `maxSteps` alias \u2192 wrapper default)\n * to resolve the effective loop config for each turn.\n *\n * Phase 0 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n loop?: AiAgentLoopConfig\n output?: AiAgentStructuredOutput\n resolvePageContext?: (ctx: AiAgentPageContextInput) => Promise<string | null>\n keywords?: string[]\n domain?: string\n dataCapabilities?: AiAgentDataCapabilities\n}\n\nexport interface AiAgentExtension {\n targetAgentId: string\n replaceAllowedTools?: string[]\n deleteAllowedTools?: string[]\n appendAllowedTools?: string[]\n taskPlan?: AiAgentTaskPlanConfig\n replaceSystemPrompt?: string\n appendSystemPrompt?: string\n replaceSuggestions?: AiAgentSuggestion[]\n deleteSuggestions?: string[]\n appendSuggestions?: AiAgentSuggestion[]\n /**\n * @deprecated Use `appendSuggestions` for new code. Preserved as the\n * original append-only field for backward compatibility.\n */\n suggestions?: AiAgentSuggestion[]\n}\n\nexport function defineAiAgent(definition: AiAgentDefinition): AiAgentDefinition {\n return definition\n}\n\nexport function defineAiAgentExtension(extension: AiAgentExtension): AiAgentExtension {\n return extension\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { ZodTypeAny } from 'zod'\nimport type {\n PrepareStepFunction,\n GenerateTextOnStepFinishCallback,\n GenerateTextOnStepStartCallback,\n OnToolExecutionStartCallback,\n OnToolExecutionEndCallback,\n ToolCallRepairFunction,\n StopCondition,\n ToolChoice,\n ToolSet,\n} from 'ai'\n\nexport type AiAgentExecutionMode = 'chat' | 'object'\n\n/**\n * Selects the underlying Vercel AI SDK dispatch strategy for this agent.\n *\n * - `'stream-text'` (default): the runtime calls `streamText(...)` directly on\n * every turn. All loop primitives are supported: `prepareStep`, `stopWhen`,\n * `repairToolCall`, `activeTools`, `toolChoice`.\n *\n * - `'tool-loop-agent'`: the runtime constructs a `ToolLoopAgent`\n * (`Experimental_Agent`) once and dispatches via `agent.generate(...)` /\n * `agent.stream(...)` per turn. The wrapper-owned `prepareStep` (security-\n * critical for mutation-approval) is supplied at construction via\n * `settings.prepareStep`. `stopWhen` is similarly wired at construction.\n * The `prepareCall` hook is used for per-turn narrowing of `model`, `tools`,\n * `stopWhen`, `activeTools`, and `providerOptions`; `prepareStep` is NOT in\n * its `Pick` list and MUST NOT be threaded through it.\n *\n * Note: the current SDK version ships `experimental_repairToolCall` on\n * `ToolLoopAgentSettings`, so `repairToolCall` is technically reachable via\n * this engine. The `loop.repairToolCall` JSDoc retains a caveat reflecting\n * the spec's documented limitation, which was written against an earlier SDK\n * snapshot where the setting was absent \u2014 use with awareness that SDK\n * behaviour may differ across versions.\n *\n * Phase 5 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nexport type AiAgentExecutionEngine = 'stream-text' | 'tool-loop-agent'\n\n/**\n * A serializable stop condition for the agentic loop. The `kind` field\n * determines which Vercel AI SDK helper is used at runtime:\n * - `stepCount` \u2192 `stepCountIs(count)` \u2014 the loop stops after N steps.\n * - `hasToolCall` \u2192 `hasToolCall(toolName)` \u2014 the loop stops immediately\n * after the model emits a tool call for the named tool.\n * - `custom` \u2014 a raw `StopCondition<ToolSet>` predicate supplied in code.\n * NOT valid from JSON-only override sources (tenant DB overrides); only\n * accepted when declared directly in `agent.loop` or a `runAiAgentText`\n * caller override.\n *\n * Phase 0 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nexport type AiAgentLoopStopCondition =\n | { kind: 'stepCount'; count: number }\n | { kind: 'hasToolCall'; toolName: string }\n | { kind: 'custom'; stop: StopCondition<ToolSet> }\n\n/**\n * Budget limits for the agentic loop turn. When any limit is exceeded the\n * wrapper's `prepareStep`/`onStepFinish` aborts the turn via the per-turn\n * `AbortController` and the loop terminates with a `loop_budget_exceeded`\n * finish condition.\n *\n * Budget enforcement is implemented in Phase 1782-3; for Phases 0\u20132 the\n * fields are accepted and forwarded to the prepared-options bag but are not\n * actively enforced.\n *\n * Phase 0 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nexport interface AiAgentLoopBudget {\n /** Hard cap on tool calls across all steps in this turn. */\n maxToolCalls?: number\n /** Wall-clock cap (ms) per turn; runtime aborts via AbortController. */\n maxWallClockMs?: number\n /** Input+output token cap; aggregated from step `usage` fields. */\n maxTokens?: number\n}\n\n/**\n * First-class loop configuration for an AI agent. Supersedes the flat\n * `maxSteps` alias on `AiAgentDefinition`.\n *\n * All fields are optional; the runtime falls back to the wrapper default\n * (`{ maxSteps: 10 }` for chat, `{ maxSteps: undefined }` for object) when\n * neither the agent nor the caller supplies any loop config.\n *\n * Phase 0 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nexport interface AiAgentLoopConfig {\n /** Maximum number of agentic steps before the loop is forced to stop. */\n maxSteps?: number\n /**\n * Additional stop conditions. The wrapper ALWAYS composes these with\n * `stepCountIs(maxSteps ?? 10)` so a misconfigured `hasToolCall` for a\n * non-existent tool can never cause an infinite loop (R3 mitigation).\n */\n stopWhen?: AiAgentLoopStopCondition | AiAgentLoopStopCondition[]\n /**\n * Per-step preparation hook. The wrapper composes this with its own\n * security-critical `prepareStep` that re-asserts the tool allowlist and\n * mutation-approval wrapping per step.\n *\n * Only valid for chat agents. Rejected with `loop_unsupported_in_object_mode`\n * for object-mode agents.\n */\n prepareStep?: PrepareStepFunction<ToolSet>\n /**\n * Callback fired when a step finishes. The wrapper chains its own\n * aggregation callback (LoopTrace builder) before invoking this one.\n * Exceptions thrown by this callback are caught and logged but do not\n * abort the turn (matching the SDK's own contract).\n */\n onStepFinish?: GenerateTextOnStepFinishCallback<ToolSet>\n /**\n * Callback fired when a step starts. Forwarded to the AI SDK as\n * `experimental_onStepStart`.\n */\n onStepStart?: GenerateTextOnStepStartCallback<ToolSet>\n /**\n * Callback fired when a tool call starts. Forwarded to the AI SDK as\n * `experimental_onToolCallStart`.\n */\n onToolCallStart?: OnToolExecutionStartCallback<ToolSet>\n /**\n * Callback fired when a tool call finishes. Forwarded to the AI SDK as\n * `experimental_onToolCallFinish`.\n */\n onToolCallFinish?: OnToolExecutionEndCallback<ToolSet>\n /**\n * Tool-call repair function. Forwarded to the AI SDK as\n * `experimental_repairToolCall`.\n *\n * Only valid for chat agents. Rejected with `loop_unsupported_in_object_mode`\n * for object-mode agents.\n *\n * **Engine note**: this primitive is honored under `executionEngine: 'stream-text'`\n * (default). Agents on `'tool-loop-agent'` may not reliably support\n * `repairToolCall` across all SDK versions \u2014 if you require it, use the\n * default `stream-text` engine until support is confirmed stable on the\n * `ToolLoopAgent` class.\n *\n * Phase 5 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n repairToolCall?: ToolCallRepairFunction<ToolSet>\n /**\n * Narrow the active tool surface for each step. Names must be a subset of\n * `agent.allowedTools`; any names outside the allowlist are filtered out\n * with a `loop:active_tools_filtered` warning.\n *\n * Only valid for chat agents. Rejected with `loop_unsupported_in_object_mode`\n * for object-mode agents.\n */\n activeTools?: string[]\n /**\n * Tool choice strategy forwarded to the AI SDK on each step.\n *\n * Only valid for chat agents. Rejected with `loop_unsupported_in_object_mode`\n * for object-mode agents.\n */\n toolChoice?: ToolChoice<ToolSet>\n /** Budget caps for this loop turn. */\n budget?: AiAgentLoopBudget\n /**\n * When `false`, per-call `runAiAgentText({ loop })` / HTTP query-param\n * overrides are rejected with `AgentPolicyError` code\n * `loop_runtime_override_disabled`. Default is `true` (permissive).\n *\n * Agents that pin a loop policy for correctness reasons (e.g. a\n * `stopWhen: hasToolCall(...)` that must not be bypassed by callers)\n * should set this to `false`.\n */\n allowRuntimeOverride?: boolean\n /**\n * Kill switch \u2014 when `true`, the runtime forces `stopWhen: stepCountIs(1)` and\n * ignores all other loop config. Used by the per-tenant operator override to\n * collapse an agent to a single model call (no tool execution) without\n * disabling the agent entirely.\n *\n * Phase 3 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n disabled?: boolean\n}\n\n/**\n * Per-step record aggregated by the wrapper-owned `onStepFinish` hook into\n * `LoopTrace`. Each completed agentic step produces one record.\n *\n * Phase 4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nexport interface LoopStepRecord {\n stepIndex: number\n /** Model id resolved for this step (relevant when prepareStep swaps models). */\n modelId: string\n toolCalls: Array<{\n toolName: string\n args: unknown\n result?: unknown\n error?: { code: string; message: string }\n repairAttempted: boolean\n durationMs: number\n }>\n /** Raw assistant text emitted in this step. */\n textDelta: string\n usage: { inputTokens: number; outputTokens: number }\n finishReason: 'stop' | 'tool-calls' | 'length' | 'content-filter' | 'error'\n}\n\n/**\n * Per-turn trace aggregated by the wrapper-owned `buildLoopTraceCollector`.\n * Not persisted \u2014 in-memory only; surfaced via the dispatcher SSE stream and\n * the playground/`<AiChat>` debug panel.\n *\n * Phase 4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nexport interface LoopTrace {\n agentId: string\n /**\n * Stable per-conversation id that ties every turn together. Echoed back on\n * the SSE `loop-finish` event so clients can persist it for subsequent turns.\n *\n * Phase 6.2 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n sessionId: string\n turnId: string\n steps: LoopStepRecord[]\n stopReason:\n | 'step-count'\n | 'has-tool-call'\n | 'custom-stop'\n | 'budget-tokens'\n | 'budget-tool-calls'\n | 'budget-wall-clock'\n | 'tenant-disabled'\n | 'finish-reason'\n | 'abort'\n totalDurationMs: number\n totalUsage: { inputTokens: number; outputTokens: number }\n}\n\nexport type AiAgentMutationPolicy =\n | 'read-only'\n | 'confirm-required'\n | 'destructive-confirm-required'\n\nexport type AiAgentAcceptedMediaType = 'image' | 'pdf' | 'file'\n\nexport type AiAgentDataOperation = 'read' | 'search' | 'aggregate'\n\nexport interface AiAgentPageContextInput {\n entityType: string\n recordId: string\n container: AwilixContainer\n tenantId: string | null\n organizationId: string | null\n}\n\nexport interface AiAgentStructuredOutput<TSchema = ZodTypeAny> {\n schemaName: string\n schema: TSchema\n mode?: 'generate' | 'stream'\n}\n\nexport interface AiAgentDataCapabilities {\n entities?: string[]\n operations?: AiAgentDataOperation[]\n searchableFields?: string[]\n}\n\nexport interface AiAgentSuggestion {\n label: string\n prompt: string\n}\n\nexport interface AiAgentTaskPlanConfig {\n /**\n * Enables the optional visible planning helper for this agent. When true,\n * the runtime exposes `meta.update_task_plan` and injects prompt guidance\n * telling the model to set a user-visible plan before domain tools.\n *\n * Defaults to false. CRM/customer agents enable this by default in core;\n * other agents can opt in from their agent definition or extension config.\n */\n enabled?: boolean\n}\n\nexport interface AiAgentDefinition {\n id: string\n moduleId: string\n label: string\n description: string\n systemPrompt: string\n allowedTools: string[]\n suggestions?: AiAgentSuggestion[]\n executionMode?: AiAgentExecutionMode\n /**\n * Selects the underlying Vercel AI SDK dispatch strategy for this agent.\n * Defaults to `'stream-text'` \u2014 the existing behavior and the only engine\n * with unconditional full primitive coverage (`repairToolCall`, all loop\n * controls).\n *\n * Set to `'tool-loop-agent'` to use the `ToolLoopAgent` (`Experimental_Agent`)\n * class, which is closer to a semantic agent abstraction and receives upcoming\n * SDK features (multi-agent handoff, streaming approval responses) first.\n *\n * **Note on `repairToolCall`**: the current SDK version ships\n * `experimental_repairToolCall` on `ToolLoopAgentSettings`, so the primitive\n * is technically available. However, SDK behaviour is not guaranteed to be\n * identical across versions \u2014 prefer `'stream-text'` when `repairToolCall`\n * correctness is critical.\n *\n * This field is opt-in: omitting it leaves the existing `stream-text` path\n * completely unchanged.\n *\n * Phase 5 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n executionEngine?: AiAgentExecutionEngine\n /**\n * Optional provider id this agent prefers (e.g. `'openai'`, `'anthropic'`).\n * Must match a registered `LlmProvider.id`. When the named provider is\n * registered but unconfigured at runtime the factory falls through\n * transparently to the next configured provider.\n *\n * Phase 1 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n defaultProvider?: string\n /**\n * Optional model id fed through `createModelFactory` for this agent.\n * Accepts either a plain model id (`claude-haiku-4-5-20251001`) or a\n * slash-qualified `<provider>/<model>` shorthand (e.g. `openai/gpt-5-mini`).\n * When the slash form is used the prefix must match a registered provider id;\n * the registry-membership guard prevents mis-splitting model ids that already\n * contain slashes (DeepInfra: `meta-llama/Llama-3.3-70B-Instruct-Turbo`).\n *\n * A higher-priority provider source still wins over the slash hint, but a\n * lower-priority one cannot overwrite a slash-qualified model (cross-axis\n * tie-break rule from spec \u00A7Phase-1).\n *\n * Phase 0 and Phase 1 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n defaultModel?: string\n /**\n * Optional base URL this agent prefers for its chosen provider.\n * Sits between the `<MODULE>_AI_BASE_URL` env (step 2 of the public 5-step\n * baseURL hierarchy) and the preset env override (`baseURLEnvKeys`, step 4).\n * Only honoured by adapters that support baseURL (Anthropic Messages-\n * protocol relays, all OpenAI-compatible adapters, Google via\n * @ai-sdk/google \u22653.0). See `packages/ai-assistant/AGENTS.md` \u2192\n * \"baseURL override hierarchy\" for the full numbered chain.\n *\n * Phase 2 of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n defaultBaseUrl?: string\n /**\n * When false, per-request HTTP overrides (query params `provider`, `model`,\n * `baseUrl`, `loopBudget`) and the per-tenant settings override stored in\n * `ai_agent_runtime_overrides` are both suppressed. Steps 1 and 3 of the\n * model-factory resolution chain are skipped for this agent, and the\n * `loopBudget` query parameter is ignored by the chat dispatcher.\n *\n * Default is `true` (permissive). Agents that pin a specific model for\n * correctness reasons (e.g. a structured-output agent whose JSON-mode schema\n * only works with one provider) should set this to `false`.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n * Renamed from `allowRuntimeModelOverride` in Phase 4 of spec\n * `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n allowRuntimeOverride?: boolean\n /**\n * @deprecated Use `allowRuntimeOverride` instead. This alias is kept for\n * one minor release and will be removed in a future version. The runtime\n * checks `allowRuntimeOverride` first; if absent it falls back to this field.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n allowRuntimeModelOverride?: boolean\n acceptedMediaTypes?: AiAgentAcceptedMediaType[]\n requiredFeatures?: string[]\n taskPlan?: AiAgentTaskPlanConfig\n uiParts?: string[]\n readOnly?: boolean\n mutationPolicy?: AiAgentMutationPolicy\n /**\n * Marks this agent as accepting untrusted end-user input (e.g. a customer\n * portal or public-widget surface). When `true`, input pre-moderation is\n * ENFORCED on every turn wherever the resolved chat provider supports it \u2014\n * tenants cannot disable it. Other agents default to off and are tenant\n * opt-in. Additive + optional; omitting it preserves today's behavior.\n *\n * Spec `2026-06-04-ai-input-moderation-and-safety-identifiers`.\n */\n untrustedInput?: boolean\n /**\n * @deprecated Use `loop.maxSteps` instead. Honored as alias when `loop` is\n * omitted. When both `maxSteps` and `loop.maxSteps` are specified, `loop.maxSteps`\n * wins. This field will be removed in a future minor release.\n *\n * Phase 0 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n maxSteps?: number\n /**\n * First-class agentic loop configuration. Supersedes the flat `maxSteps`\n * alias. The runtime walks a precedence chain (per-call override \u2192 tenant\n * DB override \u2192 this block \u2192 legacy `maxSteps` alias \u2192 wrapper default)\n * to resolve the effective loop config for each turn.\n *\n * Phase 0 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n loop?: AiAgentLoopConfig\n output?: AiAgentStructuredOutput\n resolvePageContext?: (ctx: AiAgentPageContextInput) => Promise<string | null>\n keywords?: string[]\n domain?: string\n dataCapabilities?: AiAgentDataCapabilities\n}\n\nexport interface AiAgentExtension {\n targetAgentId: string\n replaceAllowedTools?: string[]\n deleteAllowedTools?: string[]\n appendAllowedTools?: string[]\n taskPlan?: AiAgentTaskPlanConfig\n replaceSystemPrompt?: string\n appendSystemPrompt?: string\n replaceSuggestions?: AiAgentSuggestion[]\n deleteSuggestions?: string[]\n appendSuggestions?: AiAgentSuggestion[]\n /**\n * @deprecated Use `appendSuggestions` for new code. Preserved as the\n * original append-only field for backward compatibility.\n */\n suggestions?: AiAgentSuggestion[]\n}\n\nexport function defineAiAgent(definition: AiAgentDefinition): AiAgentDefinition {\n return definition\n}\n\nexport function defineAiAgentExtension(extension: AiAgentExtension): AiAgentExtension {\n return extension\n}\n"],
|
|
5
|
+
"mappings": "AAsbO,SAAS,cAAc,YAAkD;AAC9E,SAAO;AACT;AAEO,SAAS,uBAAuB,WAA+C;AACpF,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
2
|
-
import {
|
|
2
|
+
import { authorizeFeatures } from "@open-mercato/shared/security/featurePolicy";
|
|
3
3
|
const logger = createLogger("ai_assistant");
|
|
4
4
|
async function authenticateMcpRequest(apiKeySecret, container) {
|
|
5
5
|
if (!apiKeySecret || typeof apiKeySecret !== "string") {
|
|
@@ -47,12 +47,11 @@ async function authenticateMcpRequest(apiKeySecret, container) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
function hasRequiredFeatures(requiredFeatures, userFeatures, isSuperAdmin, rbacService) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
return hasAllFeatures(requiredFeatures, userFeatures);
|
|
50
|
+
void rbacService;
|
|
51
|
+
return authorizeFeatures(requiredFeatures ?? [], {
|
|
52
|
+
grantedFeatures: userFeatures,
|
|
53
|
+
unrestricted: isSuperAdmin
|
|
54
|
+
});
|
|
56
55
|
}
|
|
57
56
|
function extractApiKeyFromHeaders(headers) {
|
|
58
57
|
const getHeader = (name) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/auth.ts"],
|
|
4
|
-
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport {
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAI7B,SAAS,
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'\n\nconst logger = createLogger('ai_assistant')\n\n/**\n * Successful authentication result.\n */\nexport type McpAuthSuccess = {\n success: true\n keyId: string\n keyName: string\n tenantId: string | null\n organizationId: string | null\n userId: string\n features: string[]\n isSuperAdmin: boolean\n}\n\n/**\n * Failed authentication result.\n */\nexport type McpAuthFailure = {\n success: false\n error: string\n}\n\n/**\n * Result from MCP authentication.\n */\nexport type McpAuthResult = McpAuthSuccess | McpAuthFailure\n\n/**\n * Authenticate an MCP request using an API key.\n *\n * This function validates the API key secret and loads the associated\n * ACL (features, organizations, super admin status) from the key's roles.\n *\n * @param apiKeySecret - The full API key secret (e.g., 'omk_xxxx.yyyy...')\n * @param container - Awilix DI container with 'em' and 'rbacService'\n * @returns Authentication result with user context or error\n */\nexport async function authenticateMcpRequest(\n apiKeySecret: string,\n container: AwilixContainer\n): Promise<McpAuthResult> {\n if (!apiKeySecret || typeof apiKeySecret !== 'string') {\n return { success: false, error: 'API key is required' }\n }\n\n const trimmedSecret = apiKeySecret.trim()\n if (!trimmedSecret) {\n return { success: false, error: 'API key is required' }\n }\n\n if (!trimmedSecret.startsWith('omk_')) {\n return { success: false, error: 'Invalid API key format' }\n }\n\n try {\n const em = container.resolve('em') as EntityManager\n\n const { findApiKeyBySecret } = await import(\n '@open-mercato/core/modules/api_keys/services/apiKeyService'\n )\n\n const apiKey = await findApiKeyBySecret(em, trimmedSecret)\n\n if (!apiKey) {\n return { success: false, error: 'Invalid or expired API key' }\n }\n\n const userId = `api_key:${apiKey.id}`\n\n const rbacService = container.resolve('rbacService') as {\n loadAcl: (\n userId: string,\n scope: { tenantId: string | null; organizationId: string | null }\n ) => Promise<{\n isSuperAdmin: boolean\n features: string[]\n organizations: string[] | null\n }>\n }\n\n const acl = await rbacService.loadAcl(userId, {\n tenantId: apiKey.tenantId ?? null,\n organizationId: apiKey.organizationId ?? null,\n })\n\n try {\n apiKey.lastUsedAt = new Date()\n await em.persist(apiKey).flush()\n } catch {\n // Best-effort update; ignore write failures\n }\n\n return {\n success: true,\n keyId: apiKey.id,\n keyName: apiKey.name,\n tenantId: apiKey.tenantId ?? null,\n organizationId: apiKey.organizationId ?? null,\n userId,\n features: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n logger.error('MCP authentication failed', { err: error })\n return { success: false, error: 'Authentication failed' }\n }\n}\n\n/**\n * Check if user has the required features for a resource.\n *\n * Supports:\n * - Super admin bypass (always returns true)\n * - Direct feature match (e.g., 'customers.view')\n * - Global wildcard ('*' grants all features)\n * - Prefix wildcard (e.g., 'customers.*' grants 'customers.people.view' and the\n * bare 'customers' segment itself)\n *\n * @param requiredFeatures - List of features required for access\n * @param userFeatures - List of features the user has\n * @param isSuperAdmin - Whether the user is a super admin\n * @param rbacService - Optional RbacService to delegate feature matching\n * @returns True if user has access\n */\nexport function hasRequiredFeatures(\n requiredFeatures: string[] | undefined,\n userFeatures: string[],\n isSuperAdmin: boolean,\n rbacService?: RbacService\n): boolean {\n void rbacService\n return authorizeFeatures(requiredFeatures ?? [], {\n grantedFeatures: userFeatures,\n unrestricted: isSuperAdmin,\n })\n}\n\n/**\n * Extract API key from HTTP request headers.\n *\n * Supports two header formats:\n * - x-api-key: <secret>\n * - Authorization: ApiKey <secret>\n *\n * @param headers - Request headers (Map, Headers, or plain object)\n * @returns The API key secret or null if not found\n */\nexport function extractApiKeyFromHeaders(\n headers: Headers | Map<string, string> | Record<string, string | undefined>\n): string | null {\n const getHeader = (name: string): string | null => {\n if (headers instanceof Headers) {\n return headers.get(name)\n }\n if (headers instanceof Map) {\n return headers.get(name) ?? null\n }\n const value = headers[name] ?? headers[name.toLowerCase()]\n return typeof value === 'string' ? value : null\n }\n\n const xApiKey = getHeader('x-api-key')?.trim()\n if (xApiKey) {\n return xApiKey\n }\n\n const authHeader = getHeader('authorization')?.trim()\n if (authHeader && authHeader.toLowerCase().startsWith('apikey ')) {\n return authHeader.slice(7).trim()\n }\n\n return null\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAI7B,SAAS,yBAAyB;AAElC,MAAM,SAAS,aAAa,cAAc;AAuC1C,eAAsB,uBACpB,cACA,WACwB;AACxB,MAAI,CAAC,gBAAgB,OAAO,iBAAiB,UAAU;AACrD,WAAO,EAAE,SAAS,OAAO,OAAO,sBAAsB;AAAA,EACxD;AAEA,QAAM,gBAAgB,aAAa,KAAK;AACxC,MAAI,CAAC,eAAe;AAClB,WAAO,EAAE,SAAS,OAAO,OAAO,sBAAsB;AAAA,EACxD;AAEA,MAAI,CAAC,cAAc,WAAW,MAAM,GAAG;AACrC,WAAO,EAAE,SAAS,OAAO,OAAO,yBAAyB;AAAA,EAC3D;AAEA,MAAI;AACF,UAAM,KAAK,UAAU,QAAQ,IAAI;AAEjC,UAAM,EAAE,mBAAmB,IAAI,MAAM,OACnC,4DACF;AAEA,UAAM,SAAS,MAAM,mBAAmB,IAAI,aAAa;AAEzD,QAAI,CAAC,QAAQ;AACX,aAAO,EAAE,SAAS,OAAO,OAAO,6BAA6B;AAAA,IAC/D;AAEA,UAAM,SAAS,WAAW,OAAO,EAAE;AAEnC,UAAM,cAAc,UAAU,QAAQ,aAAa;AAWnD,UAAM,MAAM,MAAM,YAAY,QAAQ,QAAQ;AAAA,MAC5C,UAAU,OAAO,YAAY;AAAA,MAC7B,gBAAgB,OAAO,kBAAkB;AAAA,IAC3C,CAAC;AAED,QAAI;AACF,aAAO,aAAa,oBAAI,KAAK;AAC7B,YAAM,GAAG,QAAQ,MAAM,EAAE,MAAM;AAAA,IACjC,QAAQ;AAAA,IAER;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,OAAO;AAAA,MACd,SAAS,OAAO;AAAA,MAChB,UAAU,OAAO,YAAY;AAAA,MAC7B,gBAAgB,OAAO,kBAAkB;AAAA,MACzC;AAAA,MACA,UAAU,IAAI;AAAA,MACd,cAAc,IAAI;AAAA,IACpB;AAAA,EACF,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,WAAO,MAAM,6BAA6B,EAAE,KAAK,MAAM,CAAC;AACxD,WAAO,EAAE,SAAS,OAAO,OAAO,wBAAwB;AAAA,EAC1D;AACF;AAkBO,SAAS,oBACd,kBACA,cACA,cACA,aACS;AACT,OAAK;AACL,SAAO,kBAAkB,oBAAoB,CAAC,GAAG;AAAA,IAC/C,iBAAiB;AAAA,IACjB,cAAc;AAAA,EAChB,CAAC;AACH;AAYO,SAAS,yBACd,SACe;AACf,QAAM,YAAY,CAAC,SAAgC;AACjD,QAAI,mBAAmB,SAAS;AAC9B,aAAO,QAAQ,IAAI,IAAI;AAAA,IACzB;AACA,QAAI,mBAAmB,KAAK;AAC1B,aAAO,QAAQ,IAAI,IAAI,KAAK;AAAA,IAC9B;AACA,UAAM,QAAQ,QAAQ,IAAI,KAAK,QAAQ,KAAK,YAAY,CAAC;AACzD,WAAO,OAAO,UAAU,WAAW,QAAQ;AAAA,EAC7C;AAEA,QAAM,UAAU,UAAU,WAAW,GAAG,KAAK;AAC7C,MAAI,SAAS;AACX,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,UAAU,eAAe,GAAG,KAAK;AACpD,MAAI,cAAc,WAAW,YAAY,EAAE,WAAW,SAAS,GAAG;AAChE,WAAO,WAAW,MAAM,CAAC,EAAE,KAAK;AAAA,EAClC;AAEA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ReferenceKind } from "@mikro-orm/core";
|
|
2
|
+
import { listEntityMetadata } from "@open-mercato/shared/lib/db/entityMetadata";
|
|
2
3
|
let cachedGraph = null;
|
|
3
4
|
function mapReferenceKind(kind, mappedBy) {
|
|
4
5
|
switch (kind) {
|
|
@@ -20,11 +21,10 @@ function getSimpleTypeName(type) {
|
|
|
20
21
|
return type;
|
|
21
22
|
}
|
|
22
23
|
async function extractEntityGraph(orm) {
|
|
23
|
-
const
|
|
24
|
-
const allMetadata = metadata.getAll();
|
|
24
|
+
const allMetadata = listEntityMetadata(orm);
|
|
25
25
|
const nodes = [];
|
|
26
26
|
const edges = [];
|
|
27
|
-
for (const
|
|
27
|
+
for (const entityMeta of allMetadata) {
|
|
28
28
|
if (entityMeta.abstract || entityMeta.embeddable) continue;
|
|
29
29
|
if (entityMeta.className.startsWith("MikroORM")) continue;
|
|
30
30
|
const properties = [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/entity-graph.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Entity Relationship Graph Extraction\n *\n * Extracts entity relationships from MikroORM metadata and provides\n * them in a format suitable for AI tools to query.\n */\n\nimport { ReferenceKind, type MikroORM } from '@mikro-orm/core'\nimport type { PostgreSqlDriver } from '@mikro-orm/postgresql'\n\n/**\n * Relationship types mapped from MikroORM reference kinds.\n */\nexport type RelationshipType =\n | 'BELONGS_TO' // ManyToOne\n | 'HAS_MANY' // OneToMany\n | 'HAS_ONE' // OneToOne (owner)\n | 'BELONGS_TO_ONE' // OneToOne (inverse)\n | 'HAS_MANY_MANY' // ManyToMany (owner)\n | 'BELONGS_TO_MANY' // ManyToMany (inverse)\n\n/**\n * A relationship triple representing a connection between two entities.\n */\nexport interface EntityTriple {\n source: string\n relationship: RelationshipType\n target: string\n property: string\n nullable?: boolean\n}\n\n/**\n * An entity node with its properties.\n */\nexport interface EntityNode {\n className: string\n tableName: string\n properties: Array<{ name: string; type: string; nullable: boolean }>\n}\n\n/**\n * The complete entity graph with nodes and edges.\n */\nexport interface EntityGraph {\n nodes: EntityNode[]\n edges: EntityTriple[]\n generatedAt: string\n}\n\n/**\n * In-memory cache for the entity graph.\n */\nlet cachedGraph: EntityGraph | null = null\n\n/**\n * Map MikroORM ReferenceKind to our RelationshipType.\n */\nfunction mapReferenceKind(kind: ReferenceKind, mappedBy?: string): RelationshipType {\n switch (kind) {\n case ReferenceKind.MANY_TO_ONE:\n return 'BELONGS_TO'\n case ReferenceKind.ONE_TO_MANY:\n return 'HAS_MANY'\n case ReferenceKind.ONE_TO_ONE:\n // If mappedBy is set, this is the inverse side\n return mappedBy ? 'BELONGS_TO_ONE' : 'HAS_ONE'\n case ReferenceKind.MANY_TO_MANY:\n // If mappedBy is set, this is the inverse side\n return mappedBy ? 'BELONGS_TO_MANY' : 'HAS_MANY_MANY'\n default:\n return 'BELONGS_TO'\n }\n}\n\n/**\n * Get a simple type name from MikroORM property type.\n */\nfunction getSimpleTypeName(type: string | ((...args: unknown[]) => unknown) | undefined): string {\n if (!type) return 'unknown'\n if (typeof type === 'function') return type.name || 'unknown'\n return type\n}\n\n/**\n * Extract the entity graph from MikroORM metadata.\n */\nexport async function extractEntityGraph(orm: MikroORM<PostgreSqlDriver>): Promise<EntityGraph> {\n const metadata = orm.getMetadata()\n const allMetadata = metadata.getAll()\n\n const nodes: EntityNode[] = []\n const edges: EntityTriple[] = []\n\n for (const [, entityMeta] of Object.entries(allMetadata)) {\n // Skip abstract entities and embeddables\n if (entityMeta.abstract || entityMeta.embeddable) continue\n\n // Skip internal MikroORM entities\n if (entityMeta.className.startsWith('MikroORM')) continue\n\n const properties: Array<{ name: string; type: string; nullable: boolean }> = []\n\n for (const prop of entityMeta.props) {\n // Skip internal properties\n if (prop.name.startsWith('_')) continue\n\n // Handle relationships\n if (prop.kind !== undefined && prop.kind !== ReferenceKind.SCALAR) {\n // This is a relationship property\n const targetEntity = prop.type\n if (targetEntity && targetEntity !== entityMeta.className) {\n const relationship = mapReferenceKind(prop.kind, prop.mappedBy)\n\n edges.push({\n source: entityMeta.className,\n relationship,\n target: targetEntity,\n property: prop.name,\n nullable: prop.nullable ?? false,\n })\n }\n } else {\n // Regular scalar property\n properties.push({\n name: prop.name,\n type: getSimpleTypeName(prop.type),\n nullable: prop.nullable ?? false,\n })\n }\n }\n\n nodes.push({\n className: entityMeta.className,\n tableName: entityMeta.tableName,\n properties,\n })\n }\n\n // Sort for consistent output\n nodes.sort((a, b) => a.className.localeCompare(b.className))\n edges.sort((a, b) => {\n const sourceCompare = a.source.localeCompare(b.source)\n if (sourceCompare !== 0) return sourceCompare\n return a.property.localeCompare(b.property)\n })\n\n return {\n nodes,\n edges,\n generatedAt: new Date().toISOString(),\n }\n}\n\n/**\n * Format the entity graph as readable triples.\n *\n * Example output:\n * (CustomerEntity)-[HAS_MANY:deals]->(CustomerDeal)\n * (SalesOrder)-[BELONGS_TO:channel]->(SalesChannel)\n */\nexport function formatGraphAsTriples(graph: EntityGraph): string[] {\n return graph.edges.map((edge) => {\n const nullable = edge.nullable ? '?' : ''\n return `(${edge.source})-[${edge.relationship}${nullable}:${edge.property}]->(${edge.target})`\n })\n}\n\n/**\n * Cache the entity graph in memory.\n */\nexport function cacheEntityGraph(graph: EntityGraph): void {\n cachedGraph = graph\n}\n\n/**\n * Retrieve the cached entity graph.\n */\nexport function getCachedEntityGraph(): EntityGraph | null {\n return cachedGraph\n}\n\n/**\n * Filter graph edges by entity name (source or target).\n */\nexport function filterGraphByEntity(graph: EntityGraph, entityName: string): EntityTriple[] {\n const lowerEntity = entityName.toLowerCase()\n return graph.edges.filter(\n (edge) => edge.source.toLowerCase().includes(lowerEntity) || edge.target.toLowerCase().includes(lowerEntity)\n )\n}\n\n/**\n * Filter graph edges by module (inferred from table name prefix).\n */\nexport function filterGraphByModule(graph: EntityGraph, moduleName: string): EntityTriple[] {\n const lowerModule = moduleName.toLowerCase()\n\n // Find entities that belong to this module (by table name prefix or class name)\n const moduleEntities = new Set<string>()\n for (const node of graph.nodes) {\n if (node.tableName.startsWith(lowerModule) || node.className.toLowerCase().includes(lowerModule)) {\n moduleEntities.add(node.className)\n }\n }\n\n return graph.edges.filter((edge) => moduleEntities.has(edge.source) || moduleEntities.has(edge.target))\n}\n\n/**\n * Filter graph edges by relationship type.\n */\nexport function filterGraphByType(graph: EntityGraph, type: RelationshipType): EntityTriple[] {\n return graph.edges.filter((edge) => edge.relationship === type)\n}\n\n/**\n * Get entity fields for a specific entity.\n */\nexport function getEntityFields(graph: EntityGraph, entityName: string): EntityNode | undefined {\n const lowerEntity = entityName.toLowerCase()\n return graph.nodes.find((node) => node.className.toLowerCase() === lowerEntity)\n}\n\n/**\n * List all entities grouped by inferred module.\n */\nexport function listEntitiesByModule(graph: EntityGraph): Map<string, string[]> {\n const byModule = new Map<string, string[]>()\n\n for (const node of graph.nodes) {\n // Infer module from table name prefix (e.g., 'sales_orders' -> 'sales')\n const module = inferModuleFromEntity(node.className, node.tableName)\n\n const existing = byModule.get(module) ?? []\n existing.push(node.className)\n byModule.set(module, existing)\n }\n\n return byModule\n}\n\n/**\n * Infer module name from entity class name or table name.\n *\n * Patterns:\n * - Table prefix: 'sales_orders' \u2192 'sales'\n * - Class prefix: 'SalesOrder' \u2192 'sales' (PascalCase to module)\n * - Common mappings: CustomerEntity \u2192 'customers', CatalogProduct \u2192 'catalog'\n */\nexport function inferModuleFromEntity(className: string, tableName: string): string {\n // First try table name prefix (most reliable)\n const tableParts = tableName.split('_')\n if (tableParts.length > 1) {\n return tableParts[0]\n }\n\n // Try to extract from class name (e.g., SalesOrder \u2192 sales)\n // Handle common entity suffixes\n const nameWithoutSuffix = className\n .replace(/Entity$/, '')\n .replace(/Model$/, '')\n\n // Extract the first word from PascalCase\n const match = nameWithoutSuffix.match(/^([A-Z][a-z]+)/)\n if (match) {\n const prefix = match[1].toLowerCase()\n // Map common prefixes to module names\n const moduleMap: Record<string, string> = {\n sales: 'sales',\n customer: 'customers',\n catalog: 'catalog',\n product: 'catalog',\n order: 'sales',\n invoice: 'sales',\n quote: 'sales',\n auth: 'auth',\n user: 'auth',\n tenant: 'auth',\n organization: 'auth',\n workflow: 'workflows',\n config: 'configs',\n dictionary: 'dictionaries',\n entity: 'entities',\n search: 'search',\n attachment: 'attachments',\n audit: 'audit_logs',\n api: 'api_keys',\n dashboard: 'dashboards',\n widget: 'widgets',\n feature: 'feature_toggles',\n perspective: 'perspectives',\n currency: 'currencies',\n content: 'content',\n onboarding: 'onboarding',\n }\n if (moduleMap[prefix]) {\n return moduleMap[prefix]\n }\n return prefix\n }\n\n return 'core'\n}\n\n/**\n * Get both outgoing and incoming relationships for an entity.\n */\nexport function getEntityRelationships(\n graph: EntityGraph,\n entityName: string\n): { outgoing: EntityTriple[]; incoming: EntityTriple[] } {\n const outgoing = graph.edges.filter(\n (edge) => edge.source.toLowerCase() === entityName.toLowerCase()\n )\n const incoming = graph.edges.filter(\n (edge) =>\n edge.target.toLowerCase() === entityName.toLowerCase() &&\n edge.source.toLowerCase() !== entityName.toLowerCase()\n )\n return { outgoing, incoming }\n}\n\n/**\n * Format a single relationship triple as a string.\n */\nexport function formatTriple(edge: EntityTriple): string {\n const nullable = edge.nullable ? '?' : ''\n return `(${edge.source})-[${edge.relationship}${nullable}:${edge.property}]->(${edge.target})`\n}\n\n/**\n * Get graph statistics.\n */\nexport function getGraphStats(graph: EntityGraph): {\n totalEntities: number\n totalRelationships: number\n modules: string[]\n} {\n const byModule = listEntitiesByModule(graph)\n return {\n totalEntities: graph.nodes.length,\n totalRelationships: graph.edges.length,\n modules: Array.from(byModule.keys()).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)),\n }\n}\n"],
|
|
5
|
-
"mappings": "AAOA,SAAS,qBAAoC;
|
|
4
|
+
"sourcesContent": ["/**\n * Entity Relationship Graph Extraction\n *\n * Extracts entity relationships from MikroORM metadata and provides\n * them in a format suitable for AI tools to query.\n */\n\nimport { ReferenceKind, type MikroORM } from '@mikro-orm/core'\nimport type { PostgreSqlDriver } from '@mikro-orm/postgresql'\nimport { listEntityMetadata } from '@open-mercato/shared/lib/db/entityMetadata'\n\n/**\n * Relationship types mapped from MikroORM reference kinds.\n */\nexport type RelationshipType =\n | 'BELONGS_TO' // ManyToOne\n | 'HAS_MANY' // OneToMany\n | 'HAS_ONE' // OneToOne (owner)\n | 'BELONGS_TO_ONE' // OneToOne (inverse)\n | 'HAS_MANY_MANY' // ManyToMany (owner)\n | 'BELONGS_TO_MANY' // ManyToMany (inverse)\n\n/**\n * A relationship triple representing a connection between two entities.\n */\nexport interface EntityTriple {\n source: string\n relationship: RelationshipType\n target: string\n property: string\n nullable?: boolean\n}\n\n/**\n * An entity node with its properties.\n */\nexport interface EntityNode {\n className: string\n tableName: string\n properties: Array<{ name: string; type: string; nullable: boolean }>\n}\n\n/**\n * The complete entity graph with nodes and edges.\n */\nexport interface EntityGraph {\n nodes: EntityNode[]\n edges: EntityTriple[]\n generatedAt: string\n}\n\n/**\n * In-memory cache for the entity graph.\n */\nlet cachedGraph: EntityGraph | null = null\n\n/**\n * Map MikroORM ReferenceKind to our RelationshipType.\n */\nfunction mapReferenceKind(kind: ReferenceKind, mappedBy?: string): RelationshipType {\n switch (kind) {\n case ReferenceKind.MANY_TO_ONE:\n return 'BELONGS_TO'\n case ReferenceKind.ONE_TO_MANY:\n return 'HAS_MANY'\n case ReferenceKind.ONE_TO_ONE:\n // If mappedBy is set, this is the inverse side\n return mappedBy ? 'BELONGS_TO_ONE' : 'HAS_ONE'\n case ReferenceKind.MANY_TO_MANY:\n // If mappedBy is set, this is the inverse side\n return mappedBy ? 'BELONGS_TO_MANY' : 'HAS_MANY_MANY'\n default:\n return 'BELONGS_TO'\n }\n}\n\n/**\n * Get a simple type name from MikroORM property type.\n */\nfunction getSimpleTypeName(type: string | ((...args: unknown[]) => unknown) | undefined): string {\n if (!type) return 'unknown'\n if (typeof type === 'function') return type.name || 'unknown'\n return type\n}\n\n/**\n * Extract the entity graph from MikroORM metadata.\n */\nexport async function extractEntityGraph(orm: MikroORM<PostgreSqlDriver>): Promise<EntityGraph> {\n const allMetadata = listEntityMetadata(orm)\n\n const nodes: EntityNode[] = []\n const edges: EntityTriple[] = []\n\n for (const entityMeta of allMetadata) {\n // Skip abstract entities and embeddables\n if (entityMeta.abstract || entityMeta.embeddable) continue\n\n // Skip internal MikroORM entities\n if (entityMeta.className.startsWith('MikroORM')) continue\n\n const properties: Array<{ name: string; type: string; nullable: boolean }> = []\n\n for (const prop of entityMeta.props) {\n // Skip internal properties\n if (prop.name.startsWith('_')) continue\n\n // Handle relationships\n if (prop.kind !== undefined && prop.kind !== ReferenceKind.SCALAR) {\n // This is a relationship property\n const targetEntity = prop.type\n if (targetEntity && targetEntity !== entityMeta.className) {\n const relationship = mapReferenceKind(prop.kind, prop.mappedBy)\n\n edges.push({\n source: entityMeta.className,\n relationship,\n target: targetEntity,\n property: prop.name,\n nullable: prop.nullable ?? false,\n })\n }\n } else {\n // Regular scalar property\n properties.push({\n name: prop.name,\n type: getSimpleTypeName(prop.type),\n nullable: prop.nullable ?? false,\n })\n }\n }\n\n nodes.push({\n className: entityMeta.className,\n tableName: entityMeta.tableName,\n properties,\n })\n }\n\n // Sort for consistent output\n nodes.sort((a, b) => a.className.localeCompare(b.className))\n edges.sort((a, b) => {\n const sourceCompare = a.source.localeCompare(b.source)\n if (sourceCompare !== 0) return sourceCompare\n return a.property.localeCompare(b.property)\n })\n\n return {\n nodes,\n edges,\n generatedAt: new Date().toISOString(),\n }\n}\n\n/**\n * Format the entity graph as readable triples.\n *\n * Example output:\n * (CustomerEntity)-[HAS_MANY:deals]->(CustomerDeal)\n * (SalesOrder)-[BELONGS_TO:channel]->(SalesChannel)\n */\nexport function formatGraphAsTriples(graph: EntityGraph): string[] {\n return graph.edges.map((edge) => {\n const nullable = edge.nullable ? '?' : ''\n return `(${edge.source})-[${edge.relationship}${nullable}:${edge.property}]->(${edge.target})`\n })\n}\n\n/**\n * Cache the entity graph in memory.\n */\nexport function cacheEntityGraph(graph: EntityGraph): void {\n cachedGraph = graph\n}\n\n/**\n * Retrieve the cached entity graph.\n */\nexport function getCachedEntityGraph(): EntityGraph | null {\n return cachedGraph\n}\n\n/**\n * Filter graph edges by entity name (source or target).\n */\nexport function filterGraphByEntity(graph: EntityGraph, entityName: string): EntityTriple[] {\n const lowerEntity = entityName.toLowerCase()\n return graph.edges.filter(\n (edge) => edge.source.toLowerCase().includes(lowerEntity) || edge.target.toLowerCase().includes(lowerEntity)\n )\n}\n\n/**\n * Filter graph edges by module (inferred from table name prefix).\n */\nexport function filterGraphByModule(graph: EntityGraph, moduleName: string): EntityTriple[] {\n const lowerModule = moduleName.toLowerCase()\n\n // Find entities that belong to this module (by table name prefix or class name)\n const moduleEntities = new Set<string>()\n for (const node of graph.nodes) {\n if (node.tableName.startsWith(lowerModule) || node.className.toLowerCase().includes(lowerModule)) {\n moduleEntities.add(node.className)\n }\n }\n\n return graph.edges.filter((edge) => moduleEntities.has(edge.source) || moduleEntities.has(edge.target))\n}\n\n/**\n * Filter graph edges by relationship type.\n */\nexport function filterGraphByType(graph: EntityGraph, type: RelationshipType): EntityTriple[] {\n return graph.edges.filter((edge) => edge.relationship === type)\n}\n\n/**\n * Get entity fields for a specific entity.\n */\nexport function getEntityFields(graph: EntityGraph, entityName: string): EntityNode | undefined {\n const lowerEntity = entityName.toLowerCase()\n return graph.nodes.find((node) => node.className.toLowerCase() === lowerEntity)\n}\n\n/**\n * List all entities grouped by inferred module.\n */\nexport function listEntitiesByModule(graph: EntityGraph): Map<string, string[]> {\n const byModule = new Map<string, string[]>()\n\n for (const node of graph.nodes) {\n // Infer module from table name prefix (e.g., 'sales_orders' -> 'sales')\n const module = inferModuleFromEntity(node.className, node.tableName)\n\n const existing = byModule.get(module) ?? []\n existing.push(node.className)\n byModule.set(module, existing)\n }\n\n return byModule\n}\n\n/**\n * Infer module name from entity class name or table name.\n *\n * Patterns:\n * - Table prefix: 'sales_orders' \u2192 'sales'\n * - Class prefix: 'SalesOrder' \u2192 'sales' (PascalCase to module)\n * - Common mappings: CustomerEntity \u2192 'customers', CatalogProduct \u2192 'catalog'\n */\nexport function inferModuleFromEntity(className: string, tableName: string): string {\n // First try table name prefix (most reliable)\n const tableParts = tableName.split('_')\n if (tableParts.length > 1) {\n return tableParts[0]\n }\n\n // Try to extract from class name (e.g., SalesOrder \u2192 sales)\n // Handle common entity suffixes\n const nameWithoutSuffix = className\n .replace(/Entity$/, '')\n .replace(/Model$/, '')\n\n // Extract the first word from PascalCase\n const match = nameWithoutSuffix.match(/^([A-Z][a-z]+)/)\n if (match) {\n const prefix = match[1].toLowerCase()\n // Map common prefixes to module names\n const moduleMap: Record<string, string> = {\n sales: 'sales',\n customer: 'customers',\n catalog: 'catalog',\n product: 'catalog',\n order: 'sales',\n invoice: 'sales',\n quote: 'sales',\n auth: 'auth',\n user: 'auth',\n tenant: 'auth',\n organization: 'auth',\n workflow: 'workflows',\n config: 'configs',\n dictionary: 'dictionaries',\n entity: 'entities',\n search: 'search',\n attachment: 'attachments',\n audit: 'audit_logs',\n api: 'api_keys',\n dashboard: 'dashboards',\n widget: 'widgets',\n feature: 'feature_toggles',\n perspective: 'perspectives',\n currency: 'currencies',\n content: 'content',\n onboarding: 'onboarding',\n }\n if (moduleMap[prefix]) {\n return moduleMap[prefix]\n }\n return prefix\n }\n\n return 'core'\n}\n\n/**\n * Get both outgoing and incoming relationships for an entity.\n */\nexport function getEntityRelationships(\n graph: EntityGraph,\n entityName: string\n): { outgoing: EntityTriple[]; incoming: EntityTriple[] } {\n const outgoing = graph.edges.filter(\n (edge) => edge.source.toLowerCase() === entityName.toLowerCase()\n )\n const incoming = graph.edges.filter(\n (edge) =>\n edge.target.toLowerCase() === entityName.toLowerCase() &&\n edge.source.toLowerCase() !== entityName.toLowerCase()\n )\n return { outgoing, incoming }\n}\n\n/**\n * Format a single relationship triple as a string.\n */\nexport function formatTriple(edge: EntityTriple): string {\n const nullable = edge.nullable ? '?' : ''\n return `(${edge.source})-[${edge.relationship}${nullable}:${edge.property}]->(${edge.target})`\n}\n\n/**\n * Get graph statistics.\n */\nexport function getGraphStats(graph: EntityGraph): {\n totalEntities: number\n totalRelationships: number\n modules: string[]\n} {\n const byModule = listEntitiesByModule(graph)\n return {\n totalEntities: graph.nodes.length,\n totalRelationships: graph.edges.length,\n modules: Array.from(byModule.keys()).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)),\n }\n}\n"],
|
|
5
|
+
"mappings": "AAOA,SAAS,qBAAoC;AAE7C,SAAS,0BAA0B;AA6CnC,IAAI,cAAkC;AAKtC,SAAS,iBAAiB,MAAqB,UAAqC;AAClF,UAAQ,MAAM;AAAA,IACZ,KAAK,cAAc;AACjB,aAAO;AAAA,IACT,KAAK,cAAc;AACjB,aAAO;AAAA,IACT,KAAK,cAAc;AAEjB,aAAO,WAAW,mBAAmB;AAAA,IACvC,KAAK,cAAc;AAEjB,aAAO,WAAW,oBAAoB;AAAA,IACxC;AACE,aAAO;AAAA,EACX;AACF;AAKA,SAAS,kBAAkB,MAAsE;AAC/F,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,OAAO,SAAS,WAAY,QAAO,KAAK,QAAQ;AACpD,SAAO;AACT;AAKA,eAAsB,mBAAmB,KAAuD;AAC9F,QAAM,cAAc,mBAAmB,GAAG;AAE1C,QAAM,QAAsB,CAAC;AAC7B,QAAM,QAAwB,CAAC;AAE/B,aAAW,cAAc,aAAa;AAEpC,QAAI,WAAW,YAAY,WAAW,WAAY;AAGlD,QAAI,WAAW,UAAU,WAAW,UAAU,EAAG;AAEjD,UAAM,aAAuE,CAAC;AAE9E,eAAW,QAAQ,WAAW,OAAO;AAEnC,UAAI,KAAK,KAAK,WAAW,GAAG,EAAG;AAG/B,UAAI,KAAK,SAAS,UAAa,KAAK,SAAS,cAAc,QAAQ;AAEjE,cAAM,eAAe,KAAK;AAC1B,YAAI,gBAAgB,iBAAiB,WAAW,WAAW;AACzD,gBAAM,eAAe,iBAAiB,KAAK,MAAM,KAAK,QAAQ;AAE9D,gBAAM,KAAK;AAAA,YACT,QAAQ,WAAW;AAAA,YACnB;AAAA,YACA,QAAQ;AAAA,YACR,UAAU,KAAK;AAAA,YACf,UAAU,KAAK,YAAY;AAAA,UAC7B,CAAC;AAAA,QACH;AAAA,MACF,OAAO;AAEL,mBAAW,KAAK;AAAA,UACd,MAAM,KAAK;AAAA,UACX,MAAM,kBAAkB,KAAK,IAAI;AAAA,UACjC,UAAU,KAAK,YAAY;AAAA,QAC7B,CAAC;AAAA,MACH;AAAA,IACF;AAEA,UAAM,KAAK;AAAA,MACT,WAAW,WAAW;AAAA,MACtB,WAAW,WAAW;AAAA,MACtB;AAAA,IACF,CAAC;AAAA,EACH;AAGA,QAAM,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,cAAc,EAAE,SAAS,CAAC;AAC3D,QAAM,KAAK,CAAC,GAAG,MAAM;AACnB,UAAM,gBAAgB,EAAE,OAAO,cAAc,EAAE,MAAM;AACrD,QAAI,kBAAkB,EAAG,QAAO;AAChC,WAAO,EAAE,SAAS,cAAc,EAAE,QAAQ;AAAA,EAC5C,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,EACtC;AACF;AASO,SAAS,qBAAqB,OAA8B;AACjE,SAAO,MAAM,MAAM,IAAI,CAAC,SAAS;AAC/B,UAAM,WAAW,KAAK,WAAW,MAAM;AACvC,WAAO,IAAI,KAAK,MAAM,MAAM,KAAK,YAAY,GAAG,QAAQ,IAAI,KAAK,QAAQ,OAAO,KAAK,MAAM;AAAA,EAC7F,CAAC;AACH;AAKO,SAAS,iBAAiB,OAA0B;AACzD,gBAAc;AAChB;AAKO,SAAS,uBAA2C;AACzD,SAAO;AACT;AAKO,SAAS,oBAAoB,OAAoB,YAAoC;AAC1F,QAAM,cAAc,WAAW,YAAY;AAC3C,SAAO,MAAM,MAAM;AAAA,IACjB,CAAC,SAAS,KAAK,OAAO,YAAY,EAAE,SAAS,WAAW,KAAK,KAAK,OAAO,YAAY,EAAE,SAAS,WAAW;AAAA,EAC7G;AACF;AAKO,SAAS,oBAAoB,OAAoB,YAAoC;AAC1F,QAAM,cAAc,WAAW,YAAY;AAG3C,QAAM,iBAAiB,oBAAI,IAAY;AACvC,aAAW,QAAQ,MAAM,OAAO;AAC9B,QAAI,KAAK,UAAU,WAAW,WAAW,KAAK,KAAK,UAAU,YAAY,EAAE,SAAS,WAAW,GAAG;AAChG,qBAAe,IAAI,KAAK,SAAS;AAAA,IACnC;AAAA,EACF;AAEA,SAAO,MAAM,MAAM,OAAO,CAAC,SAAS,eAAe,IAAI,KAAK,MAAM,KAAK,eAAe,IAAI,KAAK,MAAM,CAAC;AACxG;AAKO,SAAS,kBAAkB,OAAoB,MAAwC;AAC5F,SAAO,MAAM,MAAM,OAAO,CAAC,SAAS,KAAK,iBAAiB,IAAI;AAChE;AAKO,SAAS,gBAAgB,OAAoB,YAA4C;AAC9F,QAAM,cAAc,WAAW,YAAY;AAC3C,SAAO,MAAM,MAAM,KAAK,CAAC,SAAS,KAAK,UAAU,YAAY,MAAM,WAAW;AAChF;AAKO,SAAS,qBAAqB,OAA2C;AAC9E,QAAM,WAAW,oBAAI,IAAsB;AAE3C,aAAW,QAAQ,MAAM,OAAO;AAE9B,UAAM,SAAS,sBAAsB,KAAK,WAAW,KAAK,SAAS;AAEnE,UAAM,WAAW,SAAS,IAAI,MAAM,KAAK,CAAC;AAC1C,aAAS,KAAK,KAAK,SAAS;AAC5B,aAAS,IAAI,QAAQ,QAAQ;AAAA,EAC/B;AAEA,SAAO;AACT;AAUO,SAAS,sBAAsB,WAAmB,WAA2B;AAElF,QAAM,aAAa,UAAU,MAAM,GAAG;AACtC,MAAI,WAAW,SAAS,GAAG;AACzB,WAAO,WAAW,CAAC;AAAA,EACrB;AAIA,QAAM,oBAAoB,UACvB,QAAQ,WAAW,EAAE,EACrB,QAAQ,UAAU,EAAE;AAGvB,QAAM,QAAQ,kBAAkB,MAAM,gBAAgB;AACtD,MAAI,OAAO;AACT,UAAM,SAAS,MAAM,CAAC,EAAE,YAAY;AAEpC,UAAM,YAAoC;AAAA,MACxC,OAAO;AAAA,MACP,UAAU;AAAA,MACV,SAAS;AAAA,MACT,SAAS;AAAA,MACT,OAAO;AAAA,MACP,SAAS;AAAA,MACT,OAAO;AAAA,MACP,MAAM;AAAA,MACN,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,cAAc;AAAA,MACd,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,YAAY;AAAA,MACZ,OAAO;AAAA,MACP,KAAK;AAAA,MACL,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,aAAa;AAAA,MACb,UAAU;AAAA,MACV,SAAS;AAAA,MACT,YAAY;AAAA,IACd;AACA,QAAI,UAAU,MAAM,GAAG;AACrB,aAAO,UAAU,MAAM;AAAA,IACzB;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAKO,SAAS,uBACd,OACA,YACwD;AACxD,QAAM,WAAW,MAAM,MAAM;AAAA,IAC3B,CAAC,SAAS,KAAK,OAAO,YAAY,MAAM,WAAW,YAAY;AAAA,EACjE;AACA,QAAM,WAAW,MAAM,MAAM;AAAA,IAC3B,CAAC,SACC,KAAK,OAAO,YAAY,MAAM,WAAW,YAAY,KACrD,KAAK,OAAO,YAAY,MAAM,WAAW,YAAY;AAAA,EACzD;AACA,SAAO,EAAE,UAAU,SAAS;AAC9B;AAKO,SAAS,aAAa,MAA4B;AACvD,QAAM,WAAW,KAAK,WAAW,MAAM;AACvC,SAAO,IAAI,KAAK,MAAM,MAAM,KAAK,YAAY,GAAG,QAAQ,IAAI,KAAK,QAAQ,OAAO,KAAK,MAAM;AAC7F;AAKO,SAAS,cAAc,OAI5B;AACA,QAAM,WAAW,qBAAqB,KAAK;AAC3C,SAAO;AAAA,IACL,eAAe,MAAM,MAAM;AAAA,IAC3B,oBAAoB,MAAM,MAAM;AAAA,IAChC,SAAS,MAAM,KAAK,SAAS,KAAK,CAAC,EAAE,KAAK,CAAC,GAAG,MAAO,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAE;AAAA,EAClF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -53,6 +53,9 @@ function createAnthropicAdapter() {
|
|
|
53
53
|
}
|
|
54
54
|
return envKeys[0];
|
|
55
55
|
},
|
|
56
|
+
mapEndUserIdentifier(identifier) {
|
|
57
|
+
return { anthropic: { metadata: { userId: identifier } } };
|
|
58
|
+
},
|
|
56
59
|
createModel(options) {
|
|
57
60
|
const anthropic = createAnthropic({
|
|
58
61
|
apiKey: options.apiKey,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/ai_assistant/lib/llm-adapters/anthropic.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * AnthropicAdapter \u2014 implements the LlmProvider port for Anthropic's\n * Messages API (Claude Haiku, Sonnet, Opus).\n *\n * Wraps `createAnthropic({ apiKey })` from `@ai-sdk/anthropic` and exposes\n * a curated model list for the AI Assistant UI dropdown.\n *\n * @see packages/shared/src/lib/ai/llm-provider.ts\n * @see .ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport { createAnthropic } from '@ai-sdk/anthropic'\nimport type {\n EnvLookup,\n LlmCreateModelOptions,\n LlmModelInfo,\n LlmProvider,\n} from '@open-mercato/shared/lib/ai/llm-provider'\n\nconst DEFAULT_MODEL = 'claude-haiku-4-5-20251001'\n\nconst DEFAULT_MODELS: readonly LlmModelInfo[] = [\n {\n id: 'claude-haiku-4-5-20251001',\n name: 'Claude Haiku 4.5',\n contextWindow: 200000,\n tags: ['budget'],\n },\n {\n id: 'claude-sonnet-4-6-20260107',\n name: 'Claude Sonnet 4.6',\n contextWindow: 200000,\n tags: ['flagship'],\n },\n {\n id: 'claude-opus-4-6-20260107',\n name: 'Claude Opus 4.6',\n contextWindow: 1000000,\n tags: ['flagship', 'reasoning'],\n },\n] as const\n\n/**\n * Factory returning a fresh `AnthropicAdapter` instance. The adapter is\n * stateless \u2014 caller is free to reuse the returned object.\n *\n * `createModel` accepts an optional `baseURL` from {@link LlmCreateModelOptions}\n * and forwards it to `createAnthropic({ apiKey, baseURL })`.\n *\n * baseURL only works for Anthropic Messages-protocol relays (Cloudflare AI\n * Gateway in Anthropic mode, Helicone proxy). For OpenAI-format gateways,\n * use the OpenAI / OpenRouter presets.\n */\nexport function createAnthropicAdapter(): LlmProvider {\n const envKeys = ['ANTHROPIC_API_KEY', 'OPENCODE_ANTHROPIC_API_KEY'] as const\n\n function resolveApiKey(env?: EnvLookup): string | null {\n const lookup = env ?? process.env\n for (const key of envKeys) {\n const value = lookup[key]\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed.length > 0) return trimmed\n }\n }\n return null\n }\n\n return {\n id: 'anthropic',\n name: 'Anthropic',\n envKeys,\n defaultModel: DEFAULT_MODEL,\n defaultModels: DEFAULT_MODELS,\n\n isConfigured(env?: EnvLookup): boolean {\n return resolveApiKey(env) !== null\n },\n\n resolveApiKey,\n\n getConfiguredEnvKey(env?: EnvLookup): string {\n const lookup = env ?? process.env\n for (const key of envKeys) {\n const value = lookup[key]\n if (typeof value === 'string' && value.trim().length > 0) {\n return key\n }\n }\n return envKeys[0]\n },\n\n createModel(options: LlmCreateModelOptions): unknown {\n const anthropic = createAnthropic({\n apiKey: options.apiKey,\n ...(options.baseURL ? { baseURL: options.baseURL } : {}),\n })\n return anthropic(options.modelId)\n },\n }\n}\n"],
|
|
5
|
-
"mappings": "AAWA,SAAS,uBAAuB;AAQhC,MAAM,gBAAgB;AAEtB,MAAM,iBAA0C;AAAA,EAC9C;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,eAAe;AAAA,IACf,MAAM,CAAC,QAAQ;AAAA,EACjB;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,eAAe;AAAA,IACf,MAAM,CAAC,UAAU;AAAA,EACnB;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,eAAe;AAAA,IACf,MAAM,CAAC,YAAY,WAAW;AAAA,EAChC;AACF;AAaO,SAAS,yBAAsC;AACpD,QAAM,UAAU,CAAC,qBAAqB,4BAA4B;AAElE,WAAS,cAAc,KAAgC;AACrD,UAAM,SAAS,OAAO,QAAQ;AAC9B,eAAW,OAAO,SAAS;AACzB,YAAM,QAAQ,OAAO,GAAG;AACxB,UAAI,OAAO,UAAU,UAAU;AAC7B,cAAM,UAAU,MAAM,KAAK;AAC3B,YAAI,QAAQ,SAAS,EAAG,QAAO;AAAA,MACjC;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,IACN;AAAA,IACA,cAAc;AAAA,IACd,eAAe;AAAA,IAEf,aAAa,KAA0B;AACrC,aAAO,cAAc,GAAG,MAAM;AAAA,IAChC;AAAA,IAEA;AAAA,IAEA,oBAAoB,KAAyB;AAC3C,YAAM,SAAS,OAAO,QAAQ;AAC9B,iBAAW,OAAO,SAAS;AACzB,cAAM,QAAQ,OAAO,GAAG;AACxB,YAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,GAAG;AACxD,iBAAO;AAAA,QACT;AAAA,MACF;AACA,aAAO,QAAQ,CAAC;AAAA,IAClB;AAAA,IAEA,YAAY,SAAyC;AACnD,YAAM,YAAY,gBAAgB;AAAA,QAChC,QAAQ,QAAQ;AAAA,QAChB,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;AAAA,MACxD,CAAC;AACD,aAAO,UAAU,QAAQ,OAAO;AAAA,IAClC;AAAA,EACF;AACF;",
|
|
4
|
+
"sourcesContent": ["/**\n * AnthropicAdapter \u2014 implements the LlmProvider port for Anthropic's\n * Messages API (Claude Haiku, Sonnet, Opus).\n *\n * Wraps `createAnthropic({ apiKey })` from `@ai-sdk/anthropic` and exposes\n * a curated model list for the AI Assistant UI dropdown.\n *\n * @see packages/shared/src/lib/ai/llm-provider.ts\n * @see .ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport { createAnthropic } from '@ai-sdk/anthropic'\nimport type {\n EnvLookup,\n LlmCreateModelOptions,\n LlmModelInfo,\n LlmProvider,\n} from '@open-mercato/shared/lib/ai/llm-provider'\n\nconst DEFAULT_MODEL = 'claude-haiku-4-5-20251001'\n\nconst DEFAULT_MODELS: readonly LlmModelInfo[] = [\n {\n id: 'claude-haiku-4-5-20251001',\n name: 'Claude Haiku 4.5',\n contextWindow: 200000,\n tags: ['budget'],\n },\n {\n id: 'claude-sonnet-4-6-20260107',\n name: 'Claude Sonnet 4.6',\n contextWindow: 200000,\n tags: ['flagship'],\n },\n {\n id: 'claude-opus-4-6-20260107',\n name: 'Claude Opus 4.6',\n contextWindow: 1000000,\n tags: ['flagship', 'reasoning'],\n },\n] as const\n\n/**\n * Factory returning a fresh `AnthropicAdapter` instance. The adapter is\n * stateless \u2014 caller is free to reuse the returned object.\n *\n * `createModel` accepts an optional `baseURL` from {@link LlmCreateModelOptions}\n * and forwards it to `createAnthropic({ apiKey, baseURL })`.\n *\n * baseURL only works for Anthropic Messages-protocol relays (Cloudflare AI\n * Gateway in Anthropic mode, Helicone proxy). For OpenAI-format gateways,\n * use the OpenAI / OpenRouter presets.\n */\nexport function createAnthropicAdapter(): LlmProvider {\n const envKeys = ['ANTHROPIC_API_KEY', 'OPENCODE_ANTHROPIC_API_KEY'] as const\n\n function resolveApiKey(env?: EnvLookup): string | null {\n const lookup = env ?? process.env\n for (const key of envKeys) {\n const value = lookup[key]\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed.length > 0) return trimmed\n }\n }\n return null\n }\n\n return {\n id: 'anthropic',\n name: 'Anthropic',\n envKeys,\n defaultModel: DEFAULT_MODEL,\n defaultModels: DEFAULT_MODELS,\n\n isConfigured(env?: EnvLookup): boolean {\n return resolveApiKey(env) !== null\n },\n\n resolveApiKey,\n\n getConfiguredEnvKey(env?: EnvLookup): string {\n const lookup = env ?? process.env\n for (const key of envKeys) {\n const value = lookup[key]\n if (typeof value === 'string' && value.trim().length > 0) {\n return key\n }\n }\n return envKeys[0]\n },\n\n mapEndUserIdentifier(identifier: string): Record<string, unknown> {\n // Anthropic Messages API accepts `metadata.user_id` so abuse enforcement\n // can target a single hashed end user instead of the org key. The key MUST\n // be the AI SDK provider-option name `userId` (camelCase) \u2014 the SDK\n // translates it to the `metadata.user_id` request-body field and strips\n // unknown providerOptions keys, so a snake_case key would be dropped and\n // never reach Anthropic.\n return { anthropic: { metadata: { userId: identifier } } }\n },\n\n createModel(options: LlmCreateModelOptions): unknown {\n const anthropic = createAnthropic({\n apiKey: options.apiKey,\n ...(options.baseURL ? { baseURL: options.baseURL } : {}),\n })\n return anthropic(options.modelId)\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "AAWA,SAAS,uBAAuB;AAQhC,MAAM,gBAAgB;AAEtB,MAAM,iBAA0C;AAAA,EAC9C;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,eAAe;AAAA,IACf,MAAM,CAAC,QAAQ;AAAA,EACjB;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,eAAe;AAAA,IACf,MAAM,CAAC,UAAU;AAAA,EACnB;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,eAAe;AAAA,IACf,MAAM,CAAC,YAAY,WAAW;AAAA,EAChC;AACF;AAaO,SAAS,yBAAsC;AACpD,QAAM,UAAU,CAAC,qBAAqB,4BAA4B;AAElE,WAAS,cAAc,KAAgC;AACrD,UAAM,SAAS,OAAO,QAAQ;AAC9B,eAAW,OAAO,SAAS;AACzB,YAAM,QAAQ,OAAO,GAAG;AACxB,UAAI,OAAO,UAAU,UAAU;AAC7B,cAAM,UAAU,MAAM,KAAK;AAC3B,YAAI,QAAQ,SAAS,EAAG,QAAO;AAAA,MACjC;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,IACN;AAAA,IACA,cAAc;AAAA,IACd,eAAe;AAAA,IAEf,aAAa,KAA0B;AACrC,aAAO,cAAc,GAAG,MAAM;AAAA,IAChC;AAAA,IAEA;AAAA,IAEA,oBAAoB,KAAyB;AAC3C,YAAM,SAAS,OAAO,QAAQ;AAC9B,iBAAW,OAAO,SAAS;AACzB,cAAM,QAAQ,OAAO,GAAG;AACxB,YAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,GAAG;AACxD,iBAAO;AAAA,QACT;AAAA,MACF;AACA,aAAO,QAAQ,CAAC;AAAA,IAClB;AAAA,IAEA,qBAAqB,YAA6C;AAOhE,aAAO,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,WAAW,EAAE,EAAE;AAAA,IAC3D;AAAA,IAEA,YAAY,SAAyC;AACnD,YAAM,YAAY,gBAAgB;AAAA,QAChC,QAAQ,QAAQ;AAAA,QAChB,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;AAAA,MACxD,CAAC;AACD,aAAO,UAAU,QAAQ,OAAO;AAAA,IAClC;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -29,12 +29,13 @@ function createOpenAICompatibleProvider(preset) {
|
|
|
29
29
|
}
|
|
30
30
|
return preset.baseURL;
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
const provider = {
|
|
33
33
|
id: preset.id,
|
|
34
34
|
name: preset.name,
|
|
35
35
|
envKeys: preset.envKeys,
|
|
36
36
|
defaultModel: preset.defaultModel,
|
|
37
37
|
defaultModels: preset.defaultModels,
|
|
38
|
+
supportsInputModeration: preset.supportsInputModeration ?? false,
|
|
38
39
|
usesVendorPrefixedModelIds: preset.usesVendorPrefixedModelIds ?? false,
|
|
39
40
|
isConfigured(env) {
|
|
40
41
|
return resolveApiKey(env) !== null;
|
|
@@ -59,6 +60,12 @@ function createOpenAICompatibleProvider(preset) {
|
|
|
59
60
|
return openai(options.modelId);
|
|
60
61
|
}
|
|
61
62
|
};
|
|
63
|
+
if (preset.supportsEndUserSafetyIdentifier) {
|
|
64
|
+
provider.mapEndUserIdentifier = (identifier) => ({
|
|
65
|
+
openai: { safetyIdentifier: identifier }
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return provider;
|
|
62
69
|
}
|
|
63
70
|
export {
|
|
64
71
|
createOpenAICompatibleProvider
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/ai_assistant/lib/llm-adapters/openai.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * OpenAIAdapter \u2014 implements the LlmProvider port for the OpenAI\n * chat-completions protocol.\n *\n * This single adapter serves OpenAI itself and every OpenAI-compatible\n * backend (DeepInfra, Groq, Together, Fireworks, Azure OpenAI, LiteLLM,\n * Ollama, LocalAI, vLLM, \u2026). Vendor-specific configuration \u2014 endpoint\n * URL, available models, env var conventions \u2014 lives in\n * `./openai-compatible-presets.ts` as plain data, not code.\n *\n * The factory {@link createOpenAICompatibleProvider} takes a preset and\n * returns a fully-configured `LlmProvider` that internally calls\n * `createOpenAI({ apiKey, baseURL })` from `@ai-sdk/openai`.\n *\n * @see packages/shared/src/lib/ai/llm-provider.ts\n * @see ./openai-compatible-presets.ts\n * @see .ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport { createOpenAI } from '@ai-sdk/openai'\nimport type {\n EnvLookup,\n LlmCreateModelOptions,\n LlmModelInfo,\n LlmProvider,\n} from '@open-mercato/shared/lib/ai/llm-provider'\n\n/**\n * Configuration for a single OpenAI-compatible provider instance.\n *\n * Built-in presets live in {@link ./openai-compatible-presets.ts}.\n * Downstream applications may construct custom presets at bootstrap time\n * and register them with `llmProviderRegistry.register(\n * createOpenAICompatibleProvider(customPreset),\n * )`.\n */\nexport interface OpenAICompatiblePreset {\n /** Stable id (e.g. `openai`, `deepinfra`, `groq`, `together`). */\n id: string\n /** Human-readable display name. */\n name: string\n /**\n * Upstream base URL. Leave `undefined` to use the AI SDK default\n * (`https://api.openai.com/v1`). Required for DeepInfra, Groq, etc.\n */\n baseURL?: string\n /**\n * Env var names where the adapter looks for the API key, in priority\n * order. Each preset declares its own keys so unrelated presets never\n * accidentally share credentials (e.g. DeepInfra uses\n * `DEEPINFRA_API_KEY`, not `OPENAI_API_KEY`).\n */\n envKeys: readonly string[]\n /** Default model id used when the caller does not specify one. */\n defaultModel: string\n /** Curated model catalog shown in the UI dropdown. */\n defaultModels: readonly LlmModelInfo[]\n /**\n * Optional env var names for overriding the base URL at runtime.\n * Primarily used by presets that rely on a user-supplied URL\n * (Azure deployment, self-hosted LiteLLM, Ollama on a custom port).\n * The first non-empty value wins and overrides {@link baseURL}.\n */\n baseURLEnvKeys?: readonly string[]\n /**\n * True when this gateway's model ids are `vendor/model` strings (OpenRouter,\n * Requesty, LiteLLM). Surfaced on the resolved `LlmProvider` so the model\n * factory suppresses intra-tier slash provider-splitting for a configured\n * selection of this provider. Omit for non-gateway backends (openai,\n * deepinfra, groq, \u2026).\n */\n usesVendorPrefixedModelIds?: boolean\n}\n\nfunction readFirstNonEmpty(\n env: EnvLookup,\n keys: readonly string[],\n): string | null {\n for (const key of keys) {\n const value = env[key]\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed.length > 0) return trimmed\n }\n }\n return null\n}\n\n/**\n * Builds a `LlmProvider` instance bound to a specific OpenAI-compatible\n * preset. The returned object is stateless and can be registered directly\n * with `llmProviderRegistry.register(...)`.\n */\nexport function createOpenAICompatibleProvider(\n preset: OpenAICompatiblePreset,\n): LlmProvider {\n if (!preset.id || preset.id.length === 0) {\n throw new Error('[OpenAIAdapter] Preset must have a non-empty id')\n }\n if (!preset.envKeys || preset.envKeys.length === 0) {\n throw new Error(\n `[OpenAIAdapter] Preset \"${preset.id}\" must declare at least one env key`,\n )\n }\n\n function resolveApiKey(env?: EnvLookup): string | null {\n return readFirstNonEmpty(env ?? process.env, preset.envKeys)\n }\n\n function resolveBaseURL(env?: EnvLookup): string | undefined {\n const lookup = env ?? process.env\n if (preset.baseURLEnvKeys && preset.baseURLEnvKeys.length > 0) {\n const override = readFirstNonEmpty(lookup, preset.baseURLEnvKeys)\n if (override) return override\n }\n return preset.baseURL\n }\n\n
|
|
5
|
-
"mappings": "AAmBA,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["/**\n * OpenAIAdapter \u2014 implements the LlmProvider port for the OpenAI\n * chat-completions protocol.\n *\n * This single adapter serves OpenAI itself and every OpenAI-compatible\n * backend (DeepInfra, Groq, Together, Fireworks, Azure OpenAI, LiteLLM,\n * Ollama, LocalAI, vLLM, \u2026). Vendor-specific configuration \u2014 endpoint\n * URL, available models, env var conventions \u2014 lives in\n * `./openai-compatible-presets.ts` as plain data, not code.\n *\n * The factory {@link createOpenAICompatibleProvider} takes a preset and\n * returns a fully-configured `LlmProvider` that internally calls\n * `createOpenAI({ apiKey, baseURL })` from `@ai-sdk/openai`.\n *\n * @see packages/shared/src/lib/ai/llm-provider.ts\n * @see ./openai-compatible-presets.ts\n * @see .ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport { createOpenAI } from '@ai-sdk/openai'\nimport type {\n EnvLookup,\n LlmCreateModelOptions,\n LlmModelInfo,\n LlmProvider,\n} from '@open-mercato/shared/lib/ai/llm-provider'\n\n/**\n * Configuration for a single OpenAI-compatible provider instance.\n *\n * Built-in presets live in {@link ./openai-compatible-presets.ts}.\n * Downstream applications may construct custom presets at bootstrap time\n * and register them with `llmProviderRegistry.register(\n * createOpenAICompatibleProvider(customPreset),\n * )`.\n */\nexport interface OpenAICompatiblePreset {\n /** Stable id (e.g. `openai`, `deepinfra`, `groq`, `together`). */\n id: string\n /** Human-readable display name. */\n name: string\n /**\n * Upstream base URL. Leave `undefined` to use the AI SDK default\n * (`https://api.openai.com/v1`). Required for DeepInfra, Groq, etc.\n */\n baseURL?: string\n /**\n * Env var names where the adapter looks for the API key, in priority\n * order. Each preset declares its own keys so unrelated presets never\n * accidentally share credentials (e.g. DeepInfra uses\n * `DEEPINFRA_API_KEY`, not `OPENAI_API_KEY`).\n */\n envKeys: readonly string[]\n /** Default model id used when the caller does not specify one. */\n defaultModel: string\n /** Curated model catalog shown in the UI dropdown. */\n defaultModels: readonly LlmModelInfo[]\n /**\n * Optional env var names for overriding the base URL at runtime.\n * Primarily used by presets that rely on a user-supplied URL\n * (Azure deployment, self-hosted LiteLLM, Ollama on a custom port).\n * The first non-empty value wins and overrides {@link baseURL}.\n */\n baseURLEnvKeys?: readonly string[]\n /**\n * When `true`, the provider exposes OpenAI's `/v1/moderations` endpoint and\n * the runtime may run input pre-moderation through it. Only the native\n * `openai` preset sets this \u2014 OpenAI-compatible backends (Groq, DeepInfra,\n * Together, \u2026) do not implement the moderations endpoint, so they leave it\n * unset and rely on their own server-side filtering.\n */\n supportsInputModeration?: boolean\n /**\n * When `true`, the provider maps the runtime end-user safety identifier into\n * OpenAI's per-request `safetyIdentifier` provider option. Only the native\n * `openai` preset sets this \u2014 `safety_identifier` is an OpenAI-specific body\n * field, and OpenAI-compatible/self-hosted backends (Groq, DeepInfra, Azure,\n * Ollama, LM Studio, \u2026) may reject the unknown parameter, so they leave it\n * unset and send no identifier (matching pre-feature behavior).\n */\n supportsEndUserSafetyIdentifier?: boolean\n /**\n * True when this gateway's model ids are `vendor/model` strings (OpenRouter,\n * Requesty, LiteLLM). Surfaced on the resolved `LlmProvider` so the model\n * factory suppresses intra-tier slash provider-splitting for a configured\n * selection of this provider. Omit for non-gateway backends (openai,\n * deepinfra, groq, \u2026).\n */\n usesVendorPrefixedModelIds?: boolean\n}\n\nfunction readFirstNonEmpty(\n env: EnvLookup,\n keys: readonly string[],\n): string | null {\n for (const key of keys) {\n const value = env[key]\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed.length > 0) return trimmed\n }\n }\n return null\n}\n\n/**\n * Builds a `LlmProvider` instance bound to a specific OpenAI-compatible\n * preset. The returned object is stateless and can be registered directly\n * with `llmProviderRegistry.register(...)`.\n */\nexport function createOpenAICompatibleProvider(\n preset: OpenAICompatiblePreset,\n): LlmProvider {\n if (!preset.id || preset.id.length === 0) {\n throw new Error('[OpenAIAdapter] Preset must have a non-empty id')\n }\n if (!preset.envKeys || preset.envKeys.length === 0) {\n throw new Error(\n `[OpenAIAdapter] Preset \"${preset.id}\" must declare at least one env key`,\n )\n }\n\n function resolveApiKey(env?: EnvLookup): string | null {\n return readFirstNonEmpty(env ?? process.env, preset.envKeys)\n }\n\n function resolveBaseURL(env?: EnvLookup): string | undefined {\n const lookup = env ?? process.env\n if (preset.baseURLEnvKeys && preset.baseURLEnvKeys.length > 0) {\n const override = readFirstNonEmpty(lookup, preset.baseURLEnvKeys)\n if (override) return override\n }\n return preset.baseURL\n }\n\n const provider: LlmProvider = {\n id: preset.id,\n name: preset.name,\n envKeys: preset.envKeys,\n defaultModel: preset.defaultModel,\n defaultModels: preset.defaultModels,\n supportsInputModeration: preset.supportsInputModeration ?? false,\n usesVendorPrefixedModelIds: preset.usesVendorPrefixedModelIds ?? false,\n\n isConfigured(env?: EnvLookup): boolean {\n return resolveApiKey(env) !== null\n },\n\n resolveApiKey,\n\n getConfiguredEnvKey(env?: EnvLookup): string {\n const lookup = env ?? process.env\n for (const key of preset.envKeys) {\n const value = lookup[key]\n if (typeof value === 'string' && value.trim().length > 0) {\n return key\n }\n }\n return preset.envKeys[0]\n },\n\n createModel(options: LlmCreateModelOptions): unknown {\n // Per-request baseURL override wins over preset/env defaults.\n const baseURL = options.baseURL ?? resolveBaseURL()\n const openai = createOpenAI({\n apiKey: options.apiKey,\n ...(baseURL ? { baseURL } : {}),\n })\n return openai(options.modelId)\n },\n }\n\n // Only the native OpenAI preset maps the end-user safety identifier. The key\n // MUST be the AI SDK provider-option name `safetyIdentifier` (camelCase) \u2014 the\n // SDK translates it to the `safety_identifier` request-body field and strips\n // any unknown providerOptions keys, so a snake_case key would be silently\n // dropped and never reach OpenAI.\n if (preset.supportsEndUserSafetyIdentifier) {\n provider.mapEndUserIdentifier = (identifier: string): Record<string, unknown> => ({\n openai: { safetyIdentifier: identifier },\n })\n }\n\n return provider\n}\n"],
|
|
5
|
+
"mappings": "AAmBA,SAAS,oBAAoB;AAwE7B,SAAS,kBACP,KACA,MACe;AACf,aAAW,OAAO,MAAM;AACtB,UAAM,QAAQ,IAAI,GAAG;AACrB,QAAI,OAAO,UAAU,UAAU;AAC7B,YAAM,UAAU,MAAM,KAAK;AAC3B,UAAI,QAAQ,SAAS,EAAG,QAAO;AAAA,IACjC;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,+BACd,QACa;AACb,MAAI,CAAC,OAAO,MAAM,OAAO,GAAG,WAAW,GAAG;AACxC,UAAM,IAAI,MAAM,iDAAiD;AAAA,EACnE;AACA,MAAI,CAAC,OAAO,WAAW,OAAO,QAAQ,WAAW,GAAG;AAClD,UAAM,IAAI;AAAA,MACR,2BAA2B,OAAO,EAAE;AAAA,IACtC;AAAA,EACF;AAEA,WAAS,cAAc,KAAgC;AACrD,WAAO,kBAAkB,OAAO,QAAQ,KAAK,OAAO,OAAO;AAAA,EAC7D;AAEA,WAAS,eAAe,KAAqC;AAC3D,UAAM,SAAS,OAAO,QAAQ;AAC9B,QAAI,OAAO,kBAAkB,OAAO,eAAe,SAAS,GAAG;AAC7D,YAAM,WAAW,kBAAkB,QAAQ,OAAO,cAAc;AAChE,UAAI,SAAU,QAAO;AAAA,IACvB;AACA,WAAO,OAAO;AAAA,EAChB;AAEA,QAAM,WAAwB;AAAA,IAC5B,IAAI,OAAO;AAAA,IACX,MAAM,OAAO;AAAA,IACb,SAAS,OAAO;AAAA,IAChB,cAAc,OAAO;AAAA,IACrB,eAAe,OAAO;AAAA,IACtB,yBAAyB,OAAO,2BAA2B;AAAA,IAC3D,4BAA4B,OAAO,8BAA8B;AAAA,IAEjE,aAAa,KAA0B;AACrC,aAAO,cAAc,GAAG,MAAM;AAAA,IAChC;AAAA,IAEA;AAAA,IAEA,oBAAoB,KAAyB;AAC3C,YAAM,SAAS,OAAO,QAAQ;AAC9B,iBAAW,OAAO,OAAO,SAAS;AAChC,cAAM,QAAQ,OAAO,GAAG;AACxB,YAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,GAAG;AACxD,iBAAO;AAAA,QACT;AAAA,MACF;AACA,aAAO,OAAO,QAAQ,CAAC;AAAA,IACzB;AAAA,IAEA,YAAY,SAAyC;AAEnD,YAAM,UAAU,QAAQ,WAAW,eAAe;AAClD,YAAM,SAAS,aAAa;AAAA,QAC1B,QAAQ,QAAQ;AAAA,QAChB,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,MAC/B,CAAC;AACD,aAAO,OAAO,QAAQ,OAAO;AAAA,IAC/B;AAAA,EACF;AAOA,MAAI,OAAO,iCAAiC;AAC1C,aAAS,uBAAuB,CAAC,gBAAiD;AAAA,MAChF,QAAQ,EAAE,kBAAkB,WAAW;AAAA,IACzC;AAAA,EACF;AAEA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -242,17 +242,22 @@ function createModelFactory(_container, deps = {}) {
|
|
|
242
242
|
`LLM provider "${finalProvider.id}" is advertised as configured but resolveApiKey() returned empty.`
|
|
243
243
|
);
|
|
244
244
|
}
|
|
245
|
+
const endUserIdentifier = normalizeOverride(input.endUserIdentifier);
|
|
246
|
+
const providerOptions = endUserIdentifier && finalProvider.mapEndUserIdentifier ? finalProvider.mapEndUserIdentifier(endUserIdentifier) : void 0;
|
|
245
247
|
const model = finalProvider.createModel({
|
|
246
248
|
modelId: finalModelId,
|
|
247
249
|
apiKey: finalApiKey,
|
|
248
|
-
baseURL: resolvedBaseURL
|
|
250
|
+
baseURL: resolvedBaseURL,
|
|
251
|
+
...endUserIdentifier ? { endUserIdentifier } : {}
|
|
249
252
|
});
|
|
250
253
|
return {
|
|
251
254
|
model,
|
|
252
255
|
modelId: finalModelId,
|
|
253
256
|
providerId: finalProvider.id,
|
|
254
257
|
source: finalSource,
|
|
258
|
+
supportsInputModeration: finalProvider.supportsInputModeration === true,
|
|
255
259
|
...resolvedBaseURL !== void 0 ? { baseURL: resolvedBaseURL } : {},
|
|
260
|
+
...providerOptions !== void 0 ? { providerOptions } : {},
|
|
256
261
|
...allowlistResult.fallback ? {
|
|
257
262
|
allowlistFallback: {
|
|
258
263
|
originalProviderId: provider.id,
|