@open-mercato/ai-assistant 0.8.1-develop.7296.1.2111d779db → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/README.md +5 -0
  3. package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js.map +2 -2
  4. package/dist/modules/ai_assistant/data/repositories/AiPendingActionRepository.js.map +1 -1
  5. package/dist/modules/ai_assistant/encryption.js +31 -0
  6. package/dist/modules/ai_assistant/encryption.js.map +7 -0
  7. package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js +20 -9
  8. package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js.map +2 -2
  9. package/dist/modules/ai_assistant/lib/generated-registry-loader.js +30 -2
  10. package/dist/modules/ai_assistant/lib/generated-registry-loader.js.map +2 -2
  11. package/dist/modules/ai_assistant/lib/llm-adapters/openai.js +2 -1
  12. package/dist/modules/ai_assistant/lib/llm-adapters/openai.js.map +2 -2
  13. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +1 -0
  14. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +2 -2
  15. package/dist/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.js +2 -2
  16. package/dist/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.js.map +2 -2
  17. package/package.json +7 -8
  18. package/src/modules/ai_assistant/__tests__/encryption-map.test.ts +175 -0
  19. package/src/modules/ai_assistant/data/repositories/AiChatConversationRepository.ts +5 -4
  20. package/src/modules/ai_assistant/data/repositories/AiPendingActionRepository.ts +3 -3
  21. package/src/modules/ai_assistant/encryption.ts +29 -0
  22. package/src/modules/ai_assistant/lib/__tests__/ai-api-operation-runner.test.ts +154 -0
  23. package/src/modules/ai_assistant/lib/__tests__/generated-registry-loader.test.ts +107 -0
  24. package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +73 -0
  25. package/src/modules/ai_assistant/lib/ai-api-operation-runner.ts +22 -11
  26. package/src/modules/ai_assistant/lib/generated-registry-loader.ts +48 -4
  27. package/src/modules/ai_assistant/lib/llm-adapters/openai.ts +20 -2
  28. package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +1 -0
  29. package/src/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.tsx +2 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/generated-registry-loader.ts"],
4
- "sourcesContent": ["/**\n * Runtime loader for `.mercato/generated/*.generated.ts` registry files.\n *\n * The generated registries import their entries through the `@/` path alias\n * (e.g. `@/.mercato/generated/ai-tools.generated`). That alias is only\n * understood by the Next.js bundler \u2014 in a standalone Node process (the\n * `mcp:dev` / `mcp:serve` MCP servers, the CLI tool-test runner) a raw\n * `import('@/.mercato/...')` throws `ERR_MODULE_NOT_FOUND: Cannot find\n * package '@/.mercato'` because Node treats `@/` as a package specifier.\n *\n * These helpers locate the generated `.ts` file on disk and compile-and-import\n * it with esbuild (transpile-only), rewriting `@/` aliases to absolute paths.\n * This mirrors `loadBootstrapData` in\n * `@open-mercato/shared/lib/bootstrap/dynamicLoader` and works in both the\n * monorepo and standalone apps.\n */\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport crypto from 'node:crypto'\nimport { createRequire } from 'node:module'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\n\nconst logger = createLogger('ai_assistant')\n\nconst requireFromHere = createRequire(import.meta.url)\n\n/**\n * Locate a generated registry file (e.g. `ai-tools.generated.ts`) without\n * hardcoding the workspace layout. Searches upward from this module's compiled\n * location for a `apps/mercato/.mercato/generated/<fileName>` (monorepo), then\n * falls back to cwd-relative lookups (standalone apps run from the app dir).\n */\nexport function findGeneratedFile(fileName: string): string | null {\n const here = (() => {\n try {\n return fileURLToPath(import.meta.url)\n } catch {\n return null\n }\n })()\n\n if (here) {\n let cursor = path.dirname(here)\n for (let i = 0; i < 12; i++) {\n const candidate = path.join(cursor, 'apps', 'mercato', '.mercato', 'generated', fileName)\n if (fs.existsSync(candidate)) return candidate\n const next = path.dirname(cursor)\n if (next === cursor) break\n cursor = next\n }\n }\n // Fallbacks: cwd-based lookup (CLI invoked from apps/mercato, or a standalone\n // app whose root holds `.mercato/generated`).\n const fromCwd = path.resolve(process.cwd(), 'apps', 'mercato', '.mercato', 'generated', fileName)\n if (fs.existsSync(fromCwd)) return fromCwd\n const fromCwdDirect = path.resolve(process.cwd(), '.mercato', 'generated', fileName)\n if (fs.existsSync(fromCwdDirect)) return fromCwdDirect\n return null\n}\n\n/**\n * Compile-and-import a generated registry file on the fly. Rewrites the entry\n * specifiers Node can't resolve standalone (`@/...` aliases and the\n * `../../src/...` relative imports the generator emits for `@app` local\n * modules) to absolute file URLs, transpiles TS \u2192 ESM, and emits a sibling\n * `.mjs` we can `import()` from Node. Cached on mtime so repeat calls in the\n * same process don't recompile.\n *\n * Transpile-only (no bundling): the generated registries declare an array\n * literal whose entries are static `import(\"\u2026\")` arrow functions \u2014 we want\n * those `import()` strings to stay as runtime imports so Node resolves them\n * lazily through the workspace's normal module resolution. Eagerly bundling\n * pulls Next.js / route-handler internals into the `.mjs` and breaks at runtime\n * (e.g. `next/server` package-exports map).\n *\n * The `@app` local module entries are the one exception: those targets are raw\n * app TypeScript with no compiled sibling, so they are compiled separately\n * (see `compileAppLocalModuleEntries`) and the registry points at the artifact.\n */\nexport type CompileGeneratedOptions = {\n /**\n * Compile-and-inline LOCAL (relative / `@/`) module sources so app-source TS\n * contributions \u2014 `apps/<app>/src/modules/<m>/ai-tools.ts` / `ai-agents.ts` \u2014\n * load in the standalone node MCP server. Bare package specifiers\n * (`@open-mercato/*`, `next`, `zod`, `@mikro-orm/*`, \u2026) stay EXTERNAL and are\n * resolved at runtime against the workspace's compiled `dist`, so this neither\n * pulls Next.js internals into the bundle nor duplicates package singletons.\n *\n * Without this, a generated registry that statically imports an app-source\n * `.ts` file throws `ERR_MODULE_NOT_FOUND` under plain node (it cannot load a\n * `.ts`), which aborts the WHOLE registry \u2014 not just the one module.\n * Default `false` keeps transpile-only behaviour for registries whose imports\n * are all packages (e.g. `api-routes.generated.ts`).\n * Ignored under Jest, which requires the CJS artifact path.\n */\n bundleLocalModules?: boolean\n}\n\nexport async function compileAndImportGenerated(\n tsPath: string,\n options: CompileGeneratedOptions = {},\n): Promise<Record<string, unknown>> {\n const useJestCjsArtifact = isJestRuntime()\n const bundle = options.bundleLocalModules === true && !useJestCjsArtifact\n // Separate output filename per mode so switching strategies never reuses a\n // stale artifact from the other path via the mtime cache.\n const jsPath = tsPath.replace(/\\.ts$/, bundle ? '.bundled.mjs' : useJestCjsArtifact ? '.jest.cjs' : '.mjs')\n // appRoot is two directories up from `.mercato/generated/<file>.ts`.\n const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))\n\n if (!fs.existsSync(tsPath)) {\n throw new Error(`Generated file not found: ${tsPath}`)\n }\n\n const runtime = useJestCjsArtifact ? 'cjs' : 'esm'\n const tsSource = fs.readFileSync(tsPath, 'utf-8')\n // Runs on every call, not only when the registry itself is stale: the\n // artifact path is stable, so a registry cache hit would otherwise pin an\n // app module's compiled output to whatever it was when the registry was\n // last regenerated.\n const appLocalArtifacts = await compileAppLocalModuleEntries(tsSource, appRoot, runtime)\n\n const jsExists = fs.existsSync(jsPath)\n const needsCompile =\n !jsExists || fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs\n\n if (needsCompile) {\n const esbuild = await import('esbuild')\n if (bundle) {\n const result = await esbuild.build({\n entryPoints: [tsPath],\n bundle: true,\n // Keep every bare package specifier external (runtime resolution);\n // only relative / aliased app-source files get compiled and inlined.\n packages: 'external',\n format: 'esm',\n platform: 'node',\n target: 'node18',\n sourcemap: false,\n write: false,\n logLevel: 'silent',\n alias: { '@': appRoot },\n })\n fs.writeFileSync(jsPath, result.outputFiles[0].text)\n } else {\n const aliasRewritten = rewriteGeneratedAliasImportsForRuntime(\n tsSource,\n appRoot,\n runtime,\n appLocalArtifacts,\n )\n const result = await esbuild.transform(aliasRewritten, {\n loader: 'ts',\n format: useJestCjsArtifact ? 'cjs' : 'esm',\n target: 'node18',\n sourcemap: false,\n sourcefile: tsPath,\n })\n fs.writeFileSync(jsPath, result.code)\n }\n }\n\n if (useJestCjsArtifact) {\n return requireFromHere(jsPath) as Record<string, unknown>\n }\n return (await import(\n /* webpackIgnore: true */\n /* turbopackIgnore: true */\n pathToFileURL(jsPath).href\n )) as Record<string, unknown>\n}\n\nfunction isJestRuntime(): boolean {\n return typeof process.env.JEST_WORKER_ID === 'string'\n}\n\n/** Every `../../src/...` specifier the generator emitted for `@app` local modules. */\nexport function collectAppLocalSpecifiers(source: string): string[] {\n const specifiers = new Set<string>()\n for (const [, specifier] of source.matchAll(APP_LOCAL_STATIC_IMPORT)) specifiers.add(specifier)\n for (const [, specifier] of source.matchAll(APP_LOCAL_DYNAMIC_IMPORT)) specifiers.add(specifier)\n return [...specifiers].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))\n}\n\n/** Stable, collision-free artifact name for one app-local specifier. */\nfunction appLocalArtifactName(specifier: string, runtime: 'esm' | 'cjs'): string {\n const slug = specifier.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '')\n const digest = crypto.createHash('sha256').update(specifier).digest('hex').slice(0, 8)\n return `${slug}-${digest}.${runtime === 'cjs' ? 'cjs' : 'mjs'}`\n}\n\n/**\n * Compile every `@app` local module entry the generated registry references,\n * and map each specifier to its artifact.\n *\n * Package-backed modules (`@open-mercato/*`) never reach here \u2014 their bare\n * specifiers resolve through `node_modules` to compiled `.js`. App-local\n * modules have no compiled sibling, and Node cannot load their `.ts` source\n * directly: relative specifiers need explicit extensions under type stripping,\n * and the module's own graph (`./di`, `./data/entities`) carries decorator and\n * enum syntax that type stripping rejects outright. Bundling the entry with\n * every package import left external is what makes the source loadable.\n *\n * A module that fails to compile is logged and left pointing at its raw source,\n * which reproduces the pre-existing resolution error rather than silently\n * dropping the module's tools from the registry.\n */\nasync function compileAppLocalModuleEntries(\n source: string,\n appRoot: string,\n runtime: 'esm' | 'cjs',\n): Promise<Map<string, string>> {\n const specifiers = collectAppLocalSpecifiers(source)\n const artifacts = new Map<string, string>()\n if (specifiers.length === 0) return artifacts\n\n const generatedDir = path.join(appRoot, '.mercato', 'generated')\n const { compileAppSourceFile } = await import(\n /* webpackIgnore: true */\n /* turbopackIgnore: true */\n '@open-mercato/shared/lib/bootstrap/dynamicLoader'\n )\n\n for (const specifier of specifiers) {\n const target = path.resolve(generatedDir, specifier)\n const tsPath = fs.existsSync(`${target}.ts`)\n ? `${target}.ts`\n : fs.existsSync(target) && target.endsWith('.ts')\n ? target\n : null\n if (tsPath === null) continue\n\n const outFile = path.join(generatedDir, 'app-modules', appLocalArtifactName(specifier, runtime))\n try {\n await compileAppSourceFile(tsPath, { appRoot, outFile, format: runtime })\n artifacts.set(specifier, outFile)\n } catch (error) {\n logger.warn('Could not compile an app-local module entry for the generated registry', {\n specifier,\n err: error,\n })\n }\n }\n\n return artifacts\n}\n\nconst UNSAFE_JS_STRING_CHAR_ESCAPES: Record<number, string> = {\n 0x3c: '\\\\u003C', // < \u2014 HTML/script-tag breakout\n 0x3e: '\\\\u003E', // > \u2014 HTML/script-tag breakout\n 0x2028: '\\\\u2028', // line separator \u2014 string content but a statement terminator pre-ES2019\n 0x2029: '\\\\u2029', // paragraph separator \u2014 same\n}\n\n/**\n * Escape characters that `JSON.stringify` leaves intact but which can still\n * break out of (or alter the meaning of) the JavaScript string literal that the\n * stringified value is embedded into \u2014 notably `<`/`>` (HTML/script-tag\n * breakout) and the U+2028 / U+2029 line separators (valid string content but\n * statement terminators in pre-ES2019 parsers). Apply this on top of\n * `JSON.stringify` so the emitted import source stays well-formed regardless of\n * the resolved path. Exported for unit testing.\n */\nexport function escapeUnsafeJsStringChars(value: string): string {\n return value.replace(\n /[<>\\u2028\\u2029]/g,\n (char) => UNSAFE_JS_STRING_CHAR_ESCAPES[char.charCodeAt(0)],\n )\n}\n\n/**\n * Stringify a resolved path into a JavaScript string literal that is safe to\n * embed in generated source: `JSON.stringify` handles quoting/standard escapes,\n * and `escapeUnsafeJsStringChars` neutralizes the characters it leaves intact.\n */\nfunction toSafeJsStringLiteral(value: string): string {\n return escapeUnsafeJsStringChars(JSON.stringify(value))\n}\n\n/**\n * Rewrite the two specifier shapes the generator emits for module entries in a\n * generated registry to absolute `file://` URLs Node can resolve in a\n * standalone process:\n *\n * 1. `@/...` path-alias imports (both `from \"@/x\"` and dynamic `import(\"@/x\")`).\n * The `@/` alias is a Next.js bundler convention; outside the bundler Node\n * treats `@/...` as a bare package specifier and throws\n * `ERR_MODULE_NOT_FOUND`. Resolved against `appRoot`.\n * 2. `../../src/...` relative imports the generator emits for `@app` local\n * modules (e.g. `from \"../../src/modules/<id>/ai-tools\"`). esbuild's\n * transform (transpile-only) leaves these untouched, so the compiled\n * `.mjs` keeps an extensionless relative specifier that resolves to a\n * `.ts` file with no compiled `.js`/`.mjs` sibling \u2014 Node ESM then throws\n * `ERR_MODULE_NOT_FOUND`. Resolved against the generated file's directory\n * (`<appRoot>/.mercato/generated`), the location the generator wrote them\n * relative to. Package-backed modules (`@open-mercato/*`) are unaffected \u2014\n * their bare specifiers resolve through `node_modules` to compiled `.js`.\n *\n * When `appLocalArtifacts` maps a shape-2 specifier to a compiled artifact, that\n * artifact wins: pointing Node at raw app TypeScript only works while the file\n * and its whole graph stay within what type stripping accepts, which app\n * modules do not (see `compileAppLocalModuleEntries`). Without a mapping both\n * shapes fall back to the same `.ts`-suffix probe. Other specifiers (bare\n * packages, sibling `./` imports) are left untouched. Exported for unit testing.\n */\nexport function rewriteGeneratedAliasImports(\n source: string,\n appRoot: string,\n appLocalArtifacts?: Map<string, string>,\n): string {\n return rewriteGeneratedAliasImportsForRuntime(source, appRoot, 'esm', appLocalArtifacts)\n}\n\nconst APP_LOCAL_STATIC_IMPORT = /from\\s+[\"']((?:\\.\\.\\/)+src\\/[^\"']+)[\"']/g\nconst APP_LOCAL_DYNAMIC_IMPORT = /import\\s*\\(\\s*[\"']((?:\\.\\.\\/)+src\\/[^\"']+)[\"']\\s*\\)/g\n\nfunction rewriteGeneratedAliasImportsForRuntime(\n source: string,\n appRoot: string,\n runtime: 'esm' | 'cjs',\n appLocalArtifacts: Map<string, string> = new Map(),\n): string {\n const generatedDir = path.join(appRoot, '.mercato', 'generated')\n const toRuntimeLiteral = (target: string): string =>\n toSafeJsStringLiteral(runtime === 'esm' ? pathToFileURL(target).href : target)\n const toResolvedLiteral = (target: string): string => {\n const candidate = fs.existsSync(target)\n ? target\n : fs.existsSync(target + '.ts')\n ? target + '.ts'\n : target\n return toRuntimeLiteral(candidate)\n }\n const resolveAlias = (relativePath: string): string =>\n toResolvedLiteral(path.join(appRoot, relativePath))\n const resolveRelative = (specifier: string): string => {\n const artifact = appLocalArtifacts.get(specifier)\n if (artifact !== undefined) return toRuntimeLiteral(artifact)\n return toResolvedLiteral(path.resolve(generatedDir, specifier))\n }\n return source\n .replace(/from\\s+[\"']@\\/([^\"']+)[\"']/g, (_match, relativePath: string) => {\n return `from ${resolveAlias(relativePath)}`\n })\n .replace(/import\\s*\\(\\s*[\"']@\\/([^\"']+)[\"']\\s*\\)/g, (_match, relativePath: string) => {\n return `import(${resolveAlias(relativePath)})`\n })\n .replace(APP_LOCAL_STATIC_IMPORT, (_match, specifier: string) => {\n return `from ${resolveRelative(specifier)}`\n })\n .replace(APP_LOCAL_DYNAMIC_IMPORT, (_match, specifier: string) => {\n return `import(${resolveRelative(specifier)})`\n })\n}\n\n/**\n * Compile-and-import `api-routes.generated.ts` and register its manifest with\n * the shared registry. Many module tools are \"API-backed\" \u2014 their handlers\n * delegate to `createAiApiOperationRunner`, which fails closed with\n * \"No API route manifest registered\" unless the manifest is present. In the\n * Next.js app this is wired at bootstrap, but the standalone MCP servers\n * (`mcp:dev` / `mcp:serve`) bootstrap DI without it, so we register it here.\n *\n * Idempotent: `registerApiRouteManifests` replaces the stored manifest, so\n * calling this repeatedly (e.g. per-request HTTP handlers) is safe. Returns the\n * number of registered routes (0 when the generated file is absent).\n */\nexport async function ensureApiRouteManifestsRegistered(): Promise<number> {\n const registry = await import('@open-mercato/shared/modules/registry')\n // Already wired (e.g. the Next.js app bootstrap, or a prior call). Leave the\n // existing manifest untouched so we never interfere with the in-app agents\n // framework, which registers it at bootstrap with its own override pipeline.\n const existing = registry.getApiRouteManifests()\n if (existing.length > 0) return existing.length\n\n const tsPath = findGeneratedFile('api-routes.generated.ts')\n if (!tsPath) return 0\n try {\n const mod = await compileAndImportGenerated(tsPath)\n const apiRoutes = (mod as { apiRoutes?: unknown }).apiRoutes\n if (!Array.isArray(apiRoutes)) return 0\n registry.registerApiRouteManifests(\n apiRoutes as Parameters<typeof registry.registerApiRouteManifests>[0],\n )\n return apiRoutes.length\n } catch (error) {\n logger.warn('Could not register api-routes manifest', { err: error })\n return 0\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Runtime loader for `.mercato/generated/*.generated.ts` registry files.\n *\n * The generated registries import their entries through the `@/` path alias\n * (e.g. `@/.mercato/generated/ai-tools.generated`). That alias is only\n * understood by the Next.js bundler \u2014 in a standalone Node process (the\n * `mcp:dev` / `mcp:serve` MCP servers, the CLI tool-test runner) a raw\n * `import('@/.mercato/...')` throws `ERR_MODULE_NOT_FOUND: Cannot find\n * package '@/.mercato'` because Node treats `@/` as a package specifier.\n *\n * These helpers locate the generated `.ts` file on disk and compile-and-import\n * it with esbuild (transpile-only), rewriting `@/` aliases to absolute paths.\n * This mirrors `loadBootstrapData` in\n * `@open-mercato/shared/lib/bootstrap/dynamicLoader` and works in both the\n * monorepo and standalone apps.\n */\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport crypto from 'node:crypto'\nimport { createRequire } from 'node:module'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\n\nconst logger = createLogger('ai_assistant')\n\nconst requireFromHere = createRequire(import.meta.url)\n\n/**\n * Locate a generated registry file (e.g. `ai-tools.generated.ts`) without\n * hardcoding the workspace layout. Searches upward from this module's compiled\n * location for a `apps/mercato/.mercato/generated/<fileName>` (monorepo), then\n * falls back to cwd-relative lookups (standalone apps run from the app dir).\n */\nexport function findGeneratedFile(fileName: string): string | null {\n const here = (() => {\n try {\n return fileURLToPath(import.meta.url)\n } catch {\n return null\n }\n })()\n\n if (here) {\n let cursor = path.dirname(here)\n for (let i = 0; i < 12; i++) {\n const candidate = path.join(cursor, 'apps', 'mercato', '.mercato', 'generated', fileName)\n if (fs.existsSync(candidate)) return candidate\n const next = path.dirname(cursor)\n if (next === cursor) break\n cursor = next\n }\n }\n // Fallbacks: cwd-based lookup (CLI invoked from apps/mercato, or a standalone\n // app whose root holds `.mercato/generated`).\n const fromCwd = path.resolve(process.cwd(), 'apps', 'mercato', '.mercato', 'generated', fileName)\n if (fs.existsSync(fromCwd)) return fromCwd\n const fromCwdDirect = path.resolve(process.cwd(), '.mercato', 'generated', fileName)\n if (fs.existsSync(fromCwdDirect)) return fromCwdDirect\n return null\n}\n\n/**\n * Compile-and-import a generated registry file on the fly. Rewrites the entry\n * specifiers Node can't resolve standalone (`@/...` aliases and the\n * `../../src/...` relative imports the generator emits for `@app` local\n * modules) to absolute file URLs, transpiles TS \u2192 ESM, and emits a sibling\n * `.mjs` we can `import()` from Node. Cached on mtime so repeat calls in the\n * same process don't recompile.\n *\n * Transpile-only (no bundling): the generated registries declare an array\n * literal whose entries are static `import(\"\u2026\")` arrow functions \u2014 we want\n * those `import()` strings to stay as runtime imports so Node resolves them\n * lazily through the workspace's normal module resolution. Eagerly bundling\n * pulls Next.js / route-handler internals into the `.mjs` and breaks at runtime\n * (e.g. `next/server` package-exports map).\n *\n * The `@app` local module entries are the one exception: those targets are raw\n * app TypeScript with no compiled sibling, so they are compiled separately\n * (see `compileAppLocalModuleEntries`) and the registry points at the artifact.\n */\nexport type CompileGeneratedOptions = {\n /**\n * Compile-and-inline LOCAL (relative / `@/`) module sources so app-source TS\n * contributions \u2014 `apps/<app>/src/modules/<m>/ai-tools.ts` / `ai-agents.ts` \u2014\n * load in the standalone node MCP server. Bare package specifiers\n * (`@open-mercato/*`, `next`, `zod`, `@mikro-orm/*`, \u2026) stay EXTERNAL and are\n * resolved at runtime against the workspace's compiled `dist`, so this neither\n * pulls Next.js internals into the bundle nor duplicates package singletons.\n *\n * Without this, a generated registry that statically imports an app-source\n * `.ts` file throws `ERR_MODULE_NOT_FOUND` under plain node (it cannot load a\n * `.ts`), which aborts the WHOLE registry \u2014 not just the one module.\n * Default `false` keeps transpile-only behaviour for registries whose imports\n * are all packages (e.g. `api-routes.generated.ts`).\n * Ignored under Jest, which requires the CJS artifact path.\n */\n bundleLocalModules?: boolean\n}\n\nexport async function compileAndImportGenerated(\n tsPath: string,\n options: CompileGeneratedOptions = {},\n): Promise<Record<string, unknown>> {\n const useJestCjsArtifact = isJestRuntime()\n const bundle = options.bundleLocalModules === true && !useJestCjsArtifact\n // Separate output filename per mode so switching strategies never reuses a\n // stale artifact from the other path via the mtime cache.\n const jsPath = tsPath.replace(/\\.ts$/, bundle ? '.bundled.mjs' : useJestCjsArtifact ? '.jest.cjs' : '.mjs')\n // appRoot is two directories up from `.mercato/generated/<file>.ts`.\n const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))\n\n if (!fs.existsSync(tsPath)) {\n throw new Error(`Generated file not found: ${tsPath}`)\n }\n\n const runtime = useJestCjsArtifact ? 'cjs' : 'esm'\n const tsSource = fs.readFileSync(tsPath, 'utf-8')\n // Runs on every call, not only when the registry itself is stale: the\n // artifact path is stable, so a registry cache hit would otherwise pin an\n // app module's compiled output to whatever it was when the registry was\n // last regenerated.\n const appLocalArtifacts = await compileAppLocalModuleEntries(tsSource, appRoot, runtime)\n\n const jsExists = fs.existsSync(jsPath)\n const needsCompile =\n !jsExists || fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs\n\n if (needsCompile) {\n const esbuild = await import('esbuild')\n if (bundle) {\n const result = await esbuild.build({\n entryPoints: [tsPath],\n bundle: true,\n // Keep every bare package specifier external (runtime resolution);\n // only relative / aliased app-source files get compiled and inlined.\n packages: 'external',\n format: 'esm',\n platform: 'node',\n target: 'node18',\n sourcemap: false,\n write: false,\n logLevel: 'silent',\n plugins: [createAppAliasPlugin(appRoot)],\n })\n fs.writeFileSync(jsPath, result.outputFiles[0].text)\n } else {\n const aliasRewritten = rewriteGeneratedAliasImportsForRuntime(\n tsSource,\n appRoot,\n runtime,\n appLocalArtifacts,\n )\n const result = await esbuild.transform(aliasRewritten, {\n loader: 'ts',\n format: useJestCjsArtifact ? 'cjs' : 'esm',\n target: 'node18',\n sourcemap: false,\n sourcefile: tsPath,\n })\n fs.writeFileSync(jsPath, result.code)\n }\n }\n\n if (useJestCjsArtifact) {\n return requireFromHere(jsPath) as Record<string, unknown>\n }\n return (await import(\n /* webpackIgnore: true */\n /* turbopackIgnore: true */\n pathToFileURL(jsPath).href\n )) as Record<string, unknown>\n}\n\nconst APP_ALIAS_SUFFIXES = ['.ts', '.tsx', '/index.ts', '/index.tsx']\n\n/**\n * Resolve an `@/<rest>` specifier the way the app's tsconfig maps it:\n * `@/.mercato/*` to the app root, every other `@/*` to `src/` first with the\n * app root as a fallback (apps that keep their sources at the root). Mirrors\n * the alias resolver in `createCliBundlePlugins`\n * (`@open-mercato/shared/lib/bootstrap/dynamicLoader`), which this module can\n * only load lazily. Returns `null` when no file matches. Exported for unit testing.\n */\nexport function resolveAppAliasPath(appRoot: string, rest: string): string | null {\n const bases = rest.startsWith('.mercato/')\n ? [path.join(appRoot, rest)]\n : [path.join(appRoot, 'src', rest), path.join(appRoot, rest)]\n for (const base of bases) {\n if (fs.existsSync(base) && fs.statSync(base).isFile()) return base\n for (const suffix of APP_ALIAS_SUFFIXES) {\n if (fs.existsSync(base + suffix)) return base + suffix\n }\n }\n return null\n}\n\n/**\n * esbuild plugin resolving `@/` through `resolveAppAliasPath`. An unmatched\n * specifier falls back to the literal app-root mapping so esbuild reports the\n * missing file against the path the app author wrote. Exported for unit testing.\n */\nexport function createAppAliasPlugin(appRoot: string): import('esbuild').Plugin {\n return {\n name: 'app-alias',\n setup(build) {\n build.onResolve({ filter: /^@\\// }, (args) => {\n const rest = args.path.slice('@/'.length)\n return { path: resolveAppAliasPath(appRoot, rest) ?? path.join(appRoot, rest) }\n })\n },\n }\n}\n\nfunction isJestRuntime(): boolean {\n return typeof process.env.JEST_WORKER_ID === 'string'\n}\n\n/** Every `../../src/...` specifier the generator emitted for `@app` local modules. */\nexport function collectAppLocalSpecifiers(source: string): string[] {\n const specifiers = new Set<string>()\n for (const [, specifier] of source.matchAll(APP_LOCAL_STATIC_IMPORT)) specifiers.add(specifier)\n for (const [, specifier] of source.matchAll(APP_LOCAL_DYNAMIC_IMPORT)) specifiers.add(specifier)\n return [...specifiers].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))\n}\n\n/** Stable, collision-free artifact name for one app-local specifier. */\nfunction appLocalArtifactName(specifier: string, runtime: 'esm' | 'cjs'): string {\n const slug = specifier.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '')\n const digest = crypto.createHash('sha256').update(specifier).digest('hex').slice(0, 8)\n return `${slug}-${digest}.${runtime === 'cjs' ? 'cjs' : 'mjs'}`\n}\n\n/**\n * Compile every `@app` local module entry the generated registry references,\n * and map each specifier to its artifact.\n *\n * Package-backed modules (`@open-mercato/*`) never reach here \u2014 their bare\n * specifiers resolve through `node_modules` to compiled `.js`. App-local\n * modules have no compiled sibling, and Node cannot load their `.ts` source\n * directly: relative specifiers need explicit extensions under type stripping,\n * and the module's own graph (`./di`, `./data/entities`) carries decorator and\n * enum syntax that type stripping rejects outright. Bundling the entry with\n * every package import left external is what makes the source loadable.\n *\n * A module that fails to compile is logged and left pointing at its raw source,\n * which reproduces the pre-existing resolution error rather than silently\n * dropping the module's tools from the registry.\n */\nasync function compileAppLocalModuleEntries(\n source: string,\n appRoot: string,\n runtime: 'esm' | 'cjs',\n): Promise<Map<string, string>> {\n const specifiers = collectAppLocalSpecifiers(source)\n const artifacts = new Map<string, string>()\n if (specifiers.length === 0) return artifacts\n\n const generatedDir = path.join(appRoot, '.mercato', 'generated')\n const { compileAppSourceFile } = await import(\n /* webpackIgnore: true */\n /* turbopackIgnore: true */\n '@open-mercato/shared/lib/bootstrap/dynamicLoader'\n )\n\n for (const specifier of specifiers) {\n const target = path.resolve(generatedDir, specifier)\n const tsPath = fs.existsSync(`${target}.ts`)\n ? `${target}.ts`\n : fs.existsSync(target) && target.endsWith('.ts')\n ? target\n : null\n if (tsPath === null) continue\n\n const outFile = path.join(generatedDir, 'app-modules', appLocalArtifactName(specifier, runtime))\n try {\n await compileAppSourceFile(tsPath, { appRoot, outFile, format: runtime })\n artifacts.set(specifier, outFile)\n } catch (error) {\n logger.warn('Could not compile an app-local module entry for the generated registry', {\n specifier,\n err: error,\n })\n }\n }\n\n return artifacts\n}\n\nconst UNSAFE_JS_STRING_CHAR_ESCAPES: Record<number, string> = {\n 0x3c: '\\\\u003C', // < \u2014 HTML/script-tag breakout\n 0x3e: '\\\\u003E', // > \u2014 HTML/script-tag breakout\n 0x2028: '\\\\u2028', // line separator \u2014 string content but a statement terminator pre-ES2019\n 0x2029: '\\\\u2029', // paragraph separator \u2014 same\n}\n\n/**\n * Escape characters that `JSON.stringify` leaves intact but which can still\n * break out of (or alter the meaning of) the JavaScript string literal that the\n * stringified value is embedded into \u2014 notably `<`/`>` (HTML/script-tag\n * breakout) and the U+2028 / U+2029 line separators (valid string content but\n * statement terminators in pre-ES2019 parsers). Apply this on top of\n * `JSON.stringify` so the emitted import source stays well-formed regardless of\n * the resolved path. Exported for unit testing.\n */\nexport function escapeUnsafeJsStringChars(value: string): string {\n return value.replace(\n /[<>\\u2028\\u2029]/g,\n (char) => UNSAFE_JS_STRING_CHAR_ESCAPES[char.charCodeAt(0)],\n )\n}\n\n/**\n * Stringify a resolved path into a JavaScript string literal that is safe to\n * embed in generated source: `JSON.stringify` handles quoting/standard escapes,\n * and `escapeUnsafeJsStringChars` neutralizes the characters it leaves intact.\n */\nfunction toSafeJsStringLiteral(value: string): string {\n return escapeUnsafeJsStringChars(JSON.stringify(value))\n}\n\n/**\n * Rewrite the two specifier shapes the generator emits for module entries in a\n * generated registry to absolute `file://` URLs Node can resolve in a\n * standalone process:\n *\n * 1. `@/...` path-alias imports (both `from \"@/x\"` and dynamic `import(\"@/x\")`).\n * The `@/` alias is a Next.js bundler convention; outside the bundler Node\n * treats `@/...` as a bare package specifier and throws\n * `ERR_MODULE_NOT_FOUND`. Resolved like the app tsconfig maps the alias\n * (see `resolveAppAliasPath`), falling back to `appRoot`.\n * 2. `../../src/...` relative imports the generator emits for `@app` local\n * modules (e.g. `from \"../../src/modules/<id>/ai-tools\"`). esbuild's\n * transform (transpile-only) leaves these untouched, so the compiled\n * `.mjs` keeps an extensionless relative specifier that resolves to a\n * `.ts` file with no compiled `.js`/`.mjs` sibling \u2014 Node ESM then throws\n * `ERR_MODULE_NOT_FOUND`. Resolved against the generated file's directory\n * (`<appRoot>/.mercato/generated`), the location the generator wrote them\n * relative to. Package-backed modules (`@open-mercato/*`) are unaffected \u2014\n * their bare specifiers resolve through `node_modules` to compiled `.js`.\n *\n * When `appLocalArtifacts` maps a shape-2 specifier to a compiled artifact, that\n * artifact wins: pointing Node at raw app TypeScript only works while the file\n * and its whole graph stay within what type stripping accepts, which app\n * modules do not (see `compileAppLocalModuleEntries`). Without a mapping both\n * shapes fall back to the same `.ts`-suffix probe. Other specifiers (bare\n * packages, sibling `./` imports) are left untouched. Exported for unit testing.\n */\nexport function rewriteGeneratedAliasImports(\n source: string,\n appRoot: string,\n appLocalArtifacts?: Map<string, string>,\n): string {\n return rewriteGeneratedAliasImportsForRuntime(source, appRoot, 'esm', appLocalArtifacts)\n}\n\nconst APP_LOCAL_STATIC_IMPORT = /from\\s+[\"']((?:\\.\\.\\/)+src\\/[^\"']+)[\"']/g\nconst APP_LOCAL_DYNAMIC_IMPORT = /import\\s*\\(\\s*[\"']((?:\\.\\.\\/)+src\\/[^\"']+)[\"']\\s*\\)/g\n\nfunction rewriteGeneratedAliasImportsForRuntime(\n source: string,\n appRoot: string,\n runtime: 'esm' | 'cjs',\n appLocalArtifacts: Map<string, string> = new Map(),\n): string {\n const generatedDir = path.join(appRoot, '.mercato', 'generated')\n const toRuntimeLiteral = (target: string): string =>\n toSafeJsStringLiteral(runtime === 'esm' ? pathToFileURL(target).href : target)\n const toResolvedLiteral = (target: string): string => {\n const candidate = fs.existsSync(target)\n ? target\n : fs.existsSync(target + '.ts')\n ? target + '.ts'\n : target\n return toRuntimeLiteral(candidate)\n }\n const resolveAlias = (relativePath: string): string => {\n const resolved = resolveAppAliasPath(appRoot, relativePath)\n if (resolved !== null) return toRuntimeLiteral(resolved)\n return toResolvedLiteral(path.join(appRoot, relativePath))\n }\n const resolveRelative = (specifier: string): string => {\n const artifact = appLocalArtifacts.get(specifier)\n if (artifact !== undefined) return toRuntimeLiteral(artifact)\n return toResolvedLiteral(path.resolve(generatedDir, specifier))\n }\n return source\n .replace(/from\\s+[\"']@\\/([^\"']+)[\"']/g, (_match, relativePath: string) => {\n return `from ${resolveAlias(relativePath)}`\n })\n .replace(/import\\s*\\(\\s*[\"']@\\/([^\"']+)[\"']\\s*\\)/g, (_match, relativePath: string) => {\n return `import(${resolveAlias(relativePath)})`\n })\n .replace(APP_LOCAL_STATIC_IMPORT, (_match, specifier: string) => {\n return `from ${resolveRelative(specifier)}`\n })\n .replace(APP_LOCAL_DYNAMIC_IMPORT, (_match, specifier: string) => {\n return `import(${resolveRelative(specifier)})`\n })\n}\n\n/**\n * Compile-and-import `api-routes.generated.ts` and register its manifest with\n * the shared registry. Many module tools are \"API-backed\" \u2014 their handlers\n * delegate to `createAiApiOperationRunner`, which fails closed with\n * \"No API route manifest registered\" unless the manifest is present. In the\n * Next.js app this is wired at bootstrap, but the standalone MCP servers\n * (`mcp:dev` / `mcp:serve`) bootstrap DI without it, so we register it here.\n *\n * Idempotent: `registerApiRouteManifests` replaces the stored manifest, so\n * calling this repeatedly (e.g. per-request HTTP handlers) is safe. Returns the\n * number of registered routes (0 when the generated file is absent).\n */\nexport async function ensureApiRouteManifestsRegistered(): Promise<number> {\n const registry = await import('@open-mercato/shared/modules/registry')\n // Already wired (e.g. the Next.js app bootstrap, or a prior call). Leave the\n // existing manifest untouched so we never interfere with the in-app agents\n // framework, which registers it at bootstrap with its own override pipeline.\n const existing = registry.getApiRouteManifests()\n if (existing.length > 0) return existing.length\n\n const tsPath = findGeneratedFile('api-routes.generated.ts')\n if (!tsPath) return 0\n try {\n const mod = await compileAndImportGenerated(tsPath)\n const apiRoutes = (mod as { apiRoutes?: unknown }).apiRoutes\n if (!Array.isArray(apiRoutes)) return 0\n registry.registerApiRouteManifests(\n apiRoutes as Parameters<typeof registry.registerApiRouteManifests>[0],\n )\n return apiRoutes.length\n } catch (error) {\n logger.warn('Could not register api-routes manifest', { err: error })\n return 0\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -18,6 +18,7 @@ function createOpenAICompatibleProvider(preset) {
18
18
  `[OpenAIAdapter] Preset "${preset.id}" must declare at least one env key`
19
19
  );
20
20
  }
21
+ const apiMode = preset.apiMode ?? "chat";
21
22
  function resolveApiKey(env) {
22
23
  return readFirstNonEmpty(env ?? process.env, preset.envKeys);
23
24
  }
@@ -57,7 +58,7 @@ function createOpenAICompatibleProvider(preset) {
57
58
  apiKey: options.apiKey,
58
59
  ...baseURL ? { baseURL } : {}
59
60
  });
60
- return openai(options.modelId);
61
+ return apiMode === "responses" ? openai.responses(options.modelId) : openai.chat(options.modelId);
61
62
  }
62
63
  };
63
64
  if (preset.supportsEndUserSafetyIdentifier) {
@@ -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 * 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;",
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` and builds a\n * Chat Completions model, or a Responses API model for presets that set\n * `apiMode: 'responses'` (only the native `openai` preset).\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 * Wire protocol used for model calls. `'chat'` (the default) targets\n * `POST {baseURL}/chat/completions`, which every OpenAI-compatible backend\n * (DeepInfra, Groq, Together, Fireworks, LiteLLM, Ollama, \u2026) implements.\n * `'responses'` targets OpenAI's Responses API (`POST {baseURL}/responses`),\n * which only the native `openai` preset opts into \u2014 compatible backends do\n * not implement it and answer `404 Not Found`.\n */\n apiMode?: OpenAICompatibleApiMode\n}\n\n/** Wire protocol an {@link OpenAICompatiblePreset} speaks. */\nexport type OpenAICompatibleApiMode = 'chat' | 'responses'\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 const apiMode: OpenAICompatibleApiMode = preset.apiMode ?? 'chat'\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 apiMode === 'responses'\n ? openai.responses(options.modelId)\n : openai.chat(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": "AAqBA,SAAS,oBAAoB;AAoF7B,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,QAAM,UAAmC,OAAO,WAAW;AAE3D,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,YAAY,cACf,OAAO,UAAU,QAAQ,OAAO,IAChC,OAAO,KAAK,QAAQ,OAAO;AAAA,IACjC;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
  }
@@ -4,6 +4,7 @@ const OPENAI_PRESET = {
4
4
  baseURL: void 0,
5
5
  baseURLEnvKeys: ["OPENAI_BASE_URL"],
6
6
  envKeys: ["OPENAI_API_KEY", "OPENCODE_OPENAI_API_KEY"],
7
+ apiMode: "responses",
7
8
  supportsInputModeration: true,
8
9
  supportsEndUserSafetyIdentifier: true,
9
10
  defaultModel: "gpt-5-mini",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/openai-compatible-presets.ts"],
4
- "sourcesContent": ["/**\n * Curated registry of OpenAI-compatible LLM backends.\n *\n * Each preset is plain data \u2014 adding a new backend takes one entry in\n * this array, zero new adapter files, and zero changes to route handlers.\n * The {@link createOpenAICompatibleProvider} factory in `./llm-adapters/openai.ts`\n * turns each preset into a concrete `LlmProvider` at bootstrap time.\n *\n * Preset model catalogs are curated snapshots as of 2026-04-14 and should\n * be updated as upstream catalogs evolve. Users can always override the\n * selected model via the `OPENCODE_MODEL` env var without editing this\n * file.\n *\n * @see ./llm-adapters/openai.ts\n * @see .ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport type { OpenAICompatiblePreset } from './llm-adapters/openai'\n\n/**\n * Standard OpenAI \u2014 default OpenAI API at api.openai.com.\n */\nconst OPENAI_PRESET: OpenAICompatiblePreset = {\n id: 'openai',\n name: 'OpenAI',\n baseURL: undefined,\n baseURLEnvKeys: ['OPENAI_BASE_URL'],\n envKeys: ['OPENAI_API_KEY', 'OPENCODE_OPENAI_API_KEY'],\n supportsInputModeration: true,\n supportsEndUserSafetyIdentifier: true,\n defaultModel: 'gpt-5-mini',\n defaultModels: [\n {\n id: 'gpt-5-mini',\n name: 'GPT-5 Mini',\n contextWindow: 128000,\n tags: ['budget'],\n },\n {\n id: 'gpt-5',\n name: 'GPT-5',\n contextWindow: 128000,\n tags: ['flagship'],\n },\n {\n id: 'gpt-4o-mini',\n name: 'GPT-4o Mini',\n contextWindow: 128000,\n tags: ['budget'],\n },\n {\n id: 'gpt-4o',\n name: 'GPT-4o',\n contextWindow: 128000,\n },\n ],\n}\n\n/**\n * DeepInfra \u2014 hosts open-weight flagship models at 3-12\u00D7 lower cost than\n * the native APIs. The curated catalog targets the AI Assistant use case\n * (routing + tool use + conversational chat).\n */\nconst DEEPINFRA_PRESET: OpenAICompatiblePreset = {\n id: 'deepinfra',\n name: 'DeepInfra',\n baseURL: 'https://api.deepinfra.com/v1/openai',\n baseURLEnvKeys: ['DEEPINFRA_BASE_URL'],\n envKeys: ['DEEPINFRA_API_KEY'],\n defaultModel: 'zai-org/GLM-5.1',\n defaultModels: [\n {\n id: 'zai-org/GLM-5.1',\n name: 'GLM-5.1 (Zhipu)',\n contextWindow: 202752,\n tags: ['flagship'],\n },\n {\n id: 'zai-org/GLM-4.7-Flash',\n name: 'GLM-4.7 Flash',\n contextWindow: 202752,\n tags: ['budget'],\n },\n {\n id: 'Qwen/Qwen3-235B-A22B-Instruct-2507',\n name: 'Qwen3 235B (MoE)',\n contextWindow: 262144,\n tags: ['flagship'],\n },\n {\n id: 'meta-llama/Llama-4-Scout-17B-16E-Instruct',\n name: 'Llama 4 Scout',\n contextWindow: 327680,\n },\n {\n id: 'deepseek-ai/DeepSeek-V3.2-Exp',\n name: 'DeepSeek V3.2',\n contextWindow: 163840,\n tags: ['reasoning'],\n },\n {\n id: 'Qwen/Qwen3-Coder-30B-A3B-Instruct',\n name: 'Qwen3 Coder 30B',\n contextWindow: 262144,\n tags: ['coding'],\n },\n ],\n}\n\n/**\n * Groq \u2014 specializes in low-latency inference on LPU hardware.\n * Best suited for snappy tool-use and routing, less so for long reasoning.\n */\nconst GROQ_PRESET: OpenAICompatiblePreset = {\n id: 'groq',\n name: 'Groq',\n baseURL: 'https://api.groq.com/openai/v1',\n baseURLEnvKeys: ['GROQ_BASE_URL'],\n envKeys: ['GROQ_API_KEY'],\n defaultModel: 'llama-3.3-70b-versatile',\n defaultModels: [\n {\n id: 'llama-3.3-70b-versatile',\n name: 'Llama 3.3 70B Versatile',\n contextWindow: 131072,\n },\n {\n id: 'llama-4-scout-17b',\n name: 'Llama 4 Scout 17B',\n contextWindow: 131072,\n },\n {\n id: 'mixtral-8x22b-32768',\n name: 'Mixtral 8x22B',\n contextWindow: 32768,\n },\n ],\n}\n\n/**\n * Together AI \u2014 broad catalog of open-weight models with per-model pricing.\n */\nconst TOGETHER_PRESET: OpenAICompatiblePreset = {\n id: 'together',\n name: 'Together AI',\n baseURL: 'https://api.together.xyz/v1',\n baseURLEnvKeys: ['TOGETHER_BASE_URL'],\n envKeys: ['TOGETHER_API_KEY'],\n defaultModel: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',\n defaultModels: [\n {\n id: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',\n name: 'Llama 3.3 70B Turbo',\n contextWindow: 131072,\n },\n {\n id: 'Qwen/Qwen2.5-72B-Instruct-Turbo',\n name: 'Qwen 2.5 72B Turbo',\n contextWindow: 32768,\n },\n ],\n}\n\n/**\n * Fireworks AI \u2014 fast inference with a curated catalog.\n */\nconst FIREWORKS_PRESET: OpenAICompatiblePreset = {\n id: 'fireworks',\n name: 'Fireworks AI',\n baseURL: 'https://api.fireworks.ai/inference/v1',\n baseURLEnvKeys: ['FIREWORKS_BASE_URL'],\n envKeys: ['FIREWORKS_API_KEY'],\n defaultModel: 'accounts/fireworks/models/llama-v3p3-70b-instruct',\n defaultModels: [\n {\n id: 'accounts/fireworks/models/llama-v3p3-70b-instruct',\n name: 'Llama 3.3 70B',\n contextWindow: 131072,\n },\n ],\n}\n\n\n/**\n * LiteLLM proxy \u2014 self-hosted router for arbitrary upstream providers.\n * Base URL must be supplied via `LITELLM_BASE_URL`.\n */\nconst LITELLM_PRESET: OpenAICompatiblePreset = {\n id: 'litellm',\n name: 'LiteLLM',\n baseURL: 'http://localhost:4000/v1',\n baseURLEnvKeys: ['LITELLM_BASE_URL'],\n envKeys: ['LITELLM_API_KEY'],\n defaultModel: 'gpt-4o-mini',\n // LiteLLM proxies arbitrary upstreams and commonly relays `vendor/model` ids\n // (e.g. `anthropic/claude-\u2026`), so a configured selection must not slash-split.\n usesVendorPrefixedModelIds: true,\n defaultModels: [\n {\n id: 'gpt-4o-mini',\n name: 'GPT-4o Mini (via LiteLLM)',\n contextWindow: 128000,\n },\n ],\n}\n\n/**\n * Ollama \u2014 local model runner for development and offline use.\n * Default port 11434 can be overridden via `OLLAMA_BASE_URL`.\n */\nconst OLLAMA_PRESET: OpenAICompatiblePreset = {\n id: 'ollama',\n name: 'Ollama (local)',\n baseURL: 'http://localhost:11434/v1',\n baseURLEnvKeys: ['OLLAMA_BASE_URL'],\n envKeys: ['OLLAMA_API_KEY'],\n defaultModel: 'llama3.3',\n defaultModels: [\n {\n id: 'llama3.3',\n name: 'Llama 3.3 (local)',\n contextWindow: 131072,\n },\n {\n id: 'qwen2.5-coder',\n name: 'Qwen 2.5 Coder (local)',\n contextWindow: 131072,\n tags: ['coding'],\n },\n ],\n}\n\n/**\n * OpenRouter \u2014 unified API gateway providing access to hundreds of models\n * from Anthropic, OpenAI, Google, Meta, and others via a single OpenAI-\n * compatible endpoint.\n */\nconst OPENROUTER_PRESET: OpenAICompatiblePreset = {\n id: 'openrouter',\n name: 'OpenRouter',\n baseURL: 'https://openrouter.ai/api/v1',\n baseURLEnvKeys: ['OPENROUTER_BASE_URL'],\n envKeys: ['OPENROUTER_API_KEY'],\n defaultModel: 'meta-llama/llama-3.3-70b-instruct',\n // OpenRouter model ids are `vendor/model` (e.g. `anthropic/claude-sonnet-4.5`);\n // the leading `anthropic/` is part of the model id, not a native-provider pin.\n usesVendorPrefixedModelIds: true,\n defaultModels: [\n {\n id: 'meta-llama/llama-3.3-70b-instruct',\n name: 'Llama 3.3 70B Instruct',\n contextWindow: 131072,\n },\n ],\n}\n\n/**\n * Requesty \u2014 unified OpenAI-compatible LLM gateway providing access to\n * models from many providers via a single endpoint, using `provider/model`\n * naming (e.g. `openai/gpt-4o-mini`).\n */\nconst REQUESTY_PRESET: OpenAICompatiblePreset = {\n id: 'requesty',\n name: 'Requesty',\n baseURL: 'https://router.requesty.ai/v1',\n baseURLEnvKeys: ['REQUESTY_BASE_URL'],\n envKeys: ['REQUESTY_API_KEY'],\n defaultModel: 'openai/gpt-4o-mini',\n // Requesty uses `provider/model` naming (e.g. `openai/gpt-4o-mini`) natively.\n usesVendorPrefixedModelIds: true,\n defaultModels: [\n {\n id: 'openai/gpt-4o-mini',\n name: 'GPT-4o mini',\n contextWindow: 128000,\n },\n ],\n}\n\n/**\n * LM Studio \u2014 local model server for development and offline use.\n * Default port 1234 can be overridden via `LM_STUDIO_BASE_URL`.\n * `defaultModel` is intentionally empty \u2014 LM Studio auto-detects\n * the loaded model when the request body's `model` field is empty.\n */\nconst LM_STUDIO_PRESET: OpenAICompatiblePreset = {\n id: 'lm-studio',\n name: 'LM Studio (local)',\n baseURL: 'http://localhost:1234/v1',\n baseURLEnvKeys: ['LM_STUDIO_BASE_URL'],\n envKeys: ['LM_STUDIO_API_KEY'],\n defaultModel: '',\n defaultModels: [],\n}\n\n/**\n * Built-in presets registered at bootstrap time. Order matters \u2014 it\n * determines the default iteration order of\n * `llmProviderRegistry.resolveFirstConfigured()` when no explicit order\n * is supplied.\n */\nexport const OPENAI_COMPATIBLE_PRESETS: readonly OpenAICompatiblePreset[] = [\n OPENAI_PRESET,\n DEEPINFRA_PRESET,\n GROQ_PRESET,\n TOGETHER_PRESET,\n FIREWORKS_PRESET,\n LITELLM_PRESET,\n OLLAMA_PRESET,\n OPENROUTER_PRESET,\n REQUESTY_PRESET,\n LM_STUDIO_PRESET,\n]\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Curated registry of OpenAI-compatible LLM backends.\n *\n * Each preset is plain data \u2014 adding a new backend takes one entry in\n * this array, zero new adapter files, and zero changes to route handlers.\n * The {@link createOpenAICompatibleProvider} factory in `./llm-adapters/openai.ts`\n * turns each preset into a concrete `LlmProvider` at bootstrap time.\n *\n * Preset model catalogs are curated snapshots as of 2026-04-14 and should\n * be updated as upstream catalogs evolve. Users can always override the\n * selected model via the `OPENCODE_MODEL` env var without editing this\n * file.\n *\n * @see ./llm-adapters/openai.ts\n * @see .ai/specs/implemented/2026-04-14-llm-provider-ports-and-adapters.md\n */\n\nimport type { OpenAICompatiblePreset } from './llm-adapters/openai'\n\n/**\n * Standard OpenAI \u2014 default OpenAI API at api.openai.com.\n */\nconst OPENAI_PRESET: OpenAICompatiblePreset = {\n id: 'openai',\n name: 'OpenAI',\n baseURL: undefined,\n baseURLEnvKeys: ['OPENAI_BASE_URL'],\n envKeys: ['OPENAI_API_KEY', 'OPENCODE_OPENAI_API_KEY'],\n apiMode: 'responses',\n supportsInputModeration: true,\n supportsEndUserSafetyIdentifier: true,\n defaultModel: 'gpt-5-mini',\n defaultModels: [\n {\n id: 'gpt-5-mini',\n name: 'GPT-5 Mini',\n contextWindow: 128000,\n tags: ['budget'],\n },\n {\n id: 'gpt-5',\n name: 'GPT-5',\n contextWindow: 128000,\n tags: ['flagship'],\n },\n {\n id: 'gpt-4o-mini',\n name: 'GPT-4o Mini',\n contextWindow: 128000,\n tags: ['budget'],\n },\n {\n id: 'gpt-4o',\n name: 'GPT-4o',\n contextWindow: 128000,\n },\n ],\n}\n\n/**\n * DeepInfra \u2014 hosts open-weight flagship models at 3-12\u00D7 lower cost than\n * the native APIs. The curated catalog targets the AI Assistant use case\n * (routing + tool use + conversational chat).\n */\nconst DEEPINFRA_PRESET: OpenAICompatiblePreset = {\n id: 'deepinfra',\n name: 'DeepInfra',\n baseURL: 'https://api.deepinfra.com/v1/openai',\n baseURLEnvKeys: ['DEEPINFRA_BASE_URL'],\n envKeys: ['DEEPINFRA_API_KEY'],\n defaultModel: 'zai-org/GLM-5.1',\n defaultModels: [\n {\n id: 'zai-org/GLM-5.1',\n name: 'GLM-5.1 (Zhipu)',\n contextWindow: 202752,\n tags: ['flagship'],\n },\n {\n id: 'zai-org/GLM-4.7-Flash',\n name: 'GLM-4.7 Flash',\n contextWindow: 202752,\n tags: ['budget'],\n },\n {\n id: 'Qwen/Qwen3-235B-A22B-Instruct-2507',\n name: 'Qwen3 235B (MoE)',\n contextWindow: 262144,\n tags: ['flagship'],\n },\n {\n id: 'meta-llama/Llama-4-Scout-17B-16E-Instruct',\n name: 'Llama 4 Scout',\n contextWindow: 327680,\n },\n {\n id: 'deepseek-ai/DeepSeek-V3.2-Exp',\n name: 'DeepSeek V3.2',\n contextWindow: 163840,\n tags: ['reasoning'],\n },\n {\n id: 'Qwen/Qwen3-Coder-30B-A3B-Instruct',\n name: 'Qwen3 Coder 30B',\n contextWindow: 262144,\n tags: ['coding'],\n },\n ],\n}\n\n/**\n * Groq \u2014 specializes in low-latency inference on LPU hardware.\n * Best suited for snappy tool-use and routing, less so for long reasoning.\n */\nconst GROQ_PRESET: OpenAICompatiblePreset = {\n id: 'groq',\n name: 'Groq',\n baseURL: 'https://api.groq.com/openai/v1',\n baseURLEnvKeys: ['GROQ_BASE_URL'],\n envKeys: ['GROQ_API_KEY'],\n defaultModel: 'llama-3.3-70b-versatile',\n defaultModels: [\n {\n id: 'llama-3.3-70b-versatile',\n name: 'Llama 3.3 70B Versatile',\n contextWindow: 131072,\n },\n {\n id: 'llama-4-scout-17b',\n name: 'Llama 4 Scout 17B',\n contextWindow: 131072,\n },\n {\n id: 'mixtral-8x22b-32768',\n name: 'Mixtral 8x22B',\n contextWindow: 32768,\n },\n ],\n}\n\n/**\n * Together AI \u2014 broad catalog of open-weight models with per-model pricing.\n */\nconst TOGETHER_PRESET: OpenAICompatiblePreset = {\n id: 'together',\n name: 'Together AI',\n baseURL: 'https://api.together.xyz/v1',\n baseURLEnvKeys: ['TOGETHER_BASE_URL'],\n envKeys: ['TOGETHER_API_KEY'],\n defaultModel: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',\n defaultModels: [\n {\n id: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',\n name: 'Llama 3.3 70B Turbo',\n contextWindow: 131072,\n },\n {\n id: 'Qwen/Qwen2.5-72B-Instruct-Turbo',\n name: 'Qwen 2.5 72B Turbo',\n contextWindow: 32768,\n },\n ],\n}\n\n/**\n * Fireworks AI \u2014 fast inference with a curated catalog.\n */\nconst FIREWORKS_PRESET: OpenAICompatiblePreset = {\n id: 'fireworks',\n name: 'Fireworks AI',\n baseURL: 'https://api.fireworks.ai/inference/v1',\n baseURLEnvKeys: ['FIREWORKS_BASE_URL'],\n envKeys: ['FIREWORKS_API_KEY'],\n defaultModel: 'accounts/fireworks/models/llama-v3p3-70b-instruct',\n defaultModels: [\n {\n id: 'accounts/fireworks/models/llama-v3p3-70b-instruct',\n name: 'Llama 3.3 70B',\n contextWindow: 131072,\n },\n ],\n}\n\n\n/**\n * LiteLLM proxy \u2014 self-hosted router for arbitrary upstream providers.\n * Base URL must be supplied via `LITELLM_BASE_URL`.\n */\nconst LITELLM_PRESET: OpenAICompatiblePreset = {\n id: 'litellm',\n name: 'LiteLLM',\n baseURL: 'http://localhost:4000/v1',\n baseURLEnvKeys: ['LITELLM_BASE_URL'],\n envKeys: ['LITELLM_API_KEY'],\n defaultModel: 'gpt-4o-mini',\n // LiteLLM proxies arbitrary upstreams and commonly relays `vendor/model` ids\n // (e.g. `anthropic/claude-\u2026`), so a configured selection must not slash-split.\n usesVendorPrefixedModelIds: true,\n defaultModels: [\n {\n id: 'gpt-4o-mini',\n name: 'GPT-4o Mini (via LiteLLM)',\n contextWindow: 128000,\n },\n ],\n}\n\n/**\n * Ollama \u2014 local model runner for development and offline use.\n * Default port 11434 can be overridden via `OLLAMA_BASE_URL`.\n */\nconst OLLAMA_PRESET: OpenAICompatiblePreset = {\n id: 'ollama',\n name: 'Ollama (local)',\n baseURL: 'http://localhost:11434/v1',\n baseURLEnvKeys: ['OLLAMA_BASE_URL'],\n envKeys: ['OLLAMA_API_KEY'],\n defaultModel: 'llama3.3',\n defaultModels: [\n {\n id: 'llama3.3',\n name: 'Llama 3.3 (local)',\n contextWindow: 131072,\n },\n {\n id: 'qwen2.5-coder',\n name: 'Qwen 2.5 Coder (local)',\n contextWindow: 131072,\n tags: ['coding'],\n },\n ],\n}\n\n/**\n * OpenRouter \u2014 unified API gateway providing access to hundreds of models\n * from Anthropic, OpenAI, Google, Meta, and others via a single OpenAI-\n * compatible endpoint.\n */\nconst OPENROUTER_PRESET: OpenAICompatiblePreset = {\n id: 'openrouter',\n name: 'OpenRouter',\n baseURL: 'https://openrouter.ai/api/v1',\n baseURLEnvKeys: ['OPENROUTER_BASE_URL'],\n envKeys: ['OPENROUTER_API_KEY'],\n defaultModel: 'meta-llama/llama-3.3-70b-instruct',\n // OpenRouter model ids are `vendor/model` (e.g. `anthropic/claude-sonnet-4.5`);\n // the leading `anthropic/` is part of the model id, not a native-provider pin.\n usesVendorPrefixedModelIds: true,\n defaultModels: [\n {\n id: 'meta-llama/llama-3.3-70b-instruct',\n name: 'Llama 3.3 70B Instruct',\n contextWindow: 131072,\n },\n ],\n}\n\n/**\n * Requesty \u2014 unified OpenAI-compatible LLM gateway providing access to\n * models from many providers via a single endpoint, using `provider/model`\n * naming (e.g. `openai/gpt-4o-mini`).\n */\nconst REQUESTY_PRESET: OpenAICompatiblePreset = {\n id: 'requesty',\n name: 'Requesty',\n baseURL: 'https://router.requesty.ai/v1',\n baseURLEnvKeys: ['REQUESTY_BASE_URL'],\n envKeys: ['REQUESTY_API_KEY'],\n defaultModel: 'openai/gpt-4o-mini',\n // Requesty uses `provider/model` naming (e.g. `openai/gpt-4o-mini`) natively.\n usesVendorPrefixedModelIds: true,\n defaultModels: [\n {\n id: 'openai/gpt-4o-mini',\n name: 'GPT-4o mini',\n contextWindow: 128000,\n },\n ],\n}\n\n/**\n * LM Studio \u2014 local model server for development and offline use.\n * Default port 1234 can be overridden via `LM_STUDIO_BASE_URL`.\n * `defaultModel` is intentionally empty \u2014 LM Studio auto-detects\n * the loaded model when the request body's `model` field is empty.\n */\nconst LM_STUDIO_PRESET: OpenAICompatiblePreset = {\n id: 'lm-studio',\n name: 'LM Studio (local)',\n baseURL: 'http://localhost:1234/v1',\n baseURLEnvKeys: ['LM_STUDIO_BASE_URL'],\n envKeys: ['LM_STUDIO_API_KEY'],\n defaultModel: '',\n defaultModels: [],\n}\n\n/**\n * Built-in presets registered at bootstrap time. Order matters \u2014 it\n * determines the default iteration order of\n * `llmProviderRegistry.resolveFirstConfigured()` when no explicit order\n * is supplied.\n */\nexport const OPENAI_COMPATIBLE_PRESETS: readonly OpenAICompatiblePreset[] = [\n OPENAI_PRESET,\n DEEPINFRA_PRESET,\n GROQ_PRESET,\n TOGETHER_PRESET,\n FIREWORKS_PRESET,\n LITELLM_PRESET,\n OLLAMA_PRESET,\n OPENROUTER_PRESET,\n REQUESTY_PRESET,\n LM_STUDIO_PRESET,\n]\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -23,7 +23,7 @@ function ConversationSharedRenderer({
23
23
  const handleView = async () => {
24
24
  setExecuting(true);
25
25
  try {
26
- if (viewAction) {
26
+ if (viewAction && notification.status !== "actioned") {
27
27
  await onAction(viewAction.id);
28
28
  }
29
29
  if (notification.linkHref) router.push(notification.linkHref);
@@ -74,7 +74,7 @@ function ConversationSharedRenderer({
74
74
  e.stopPropagation();
75
75
  handleView();
76
76
  },
77
- disabled: executing || !viewAction && !notification.linkHref,
77
+ disabled: executing || !viewAction && !notification.linkHref || notification.status === "actioned" && !notification.linkHref,
78
78
  children: [
79
79
  /* @__PURE__ */ jsx(ExternalLink, { className: "size-3.5", "aria-hidden": "true" }),
80
80
  t("ai_assistant.notifications.conversation_shared.view_button", "View Conversation")
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Share2, ExternalLink, X } from 'lucide-react'\nimport { useRouter } from 'next/navigation'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { formatRelativeTime } from '@open-mercato/shared/lib/time'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { NotificationRendererProps } from '@open-mercato/shared/modules/notifications/types'\n\nexport function ConversationSharedRenderer({\n notification,\n onAction,\n onDismiss,\n actions = [],\n}: NotificationRendererProps) {\n const t = useT()\n const locale = useLocale()\n const router = useRouter()\n const [executing, setExecuting] = React.useState(false)\n const isUnread = notification.status === 'unread'\n const viewAction = actions.find((a) => a.id === 'view') ?? actions[0] ?? null\n\n const handleView = async () => {\n setExecuting(true)\n try {\n if (viewAction) {\n await onAction(viewAction.id)\n }\n if (notification.linkHref) router.push(notification.linkHref)\n } finally {\n setExecuting(false)\n }\n }\n\n const timeAgo = formatRelativeTime(notification.createdAt, { locale, translate: t }) ?? ''\n\n return (\n <div\n className={cn(\n 'group relative flex gap-4 items-start rounded-xl p-3 transition-colors hover:bg-muted/40 cursor-pointer',\n isUnread && 'bg-muted/20',\n )}\n onClick={executing ? undefined : handleView}\n onKeyDown={(e) => {\n if (executing) return\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n handleView()\n }\n }}\n role=\"button\"\n tabIndex={0}\n >\n <div className=\"relative shrink-0 flex size-10 items-center justify-center rounded-full bg-status-info-bg\">\n <Share2 className=\"size-5 text-status-info-icon\" aria-hidden=\"true\" />\n {isUnread ? (\n <span\n className=\"absolute -right-1 -top-1 size-3 rounded-full bg-accent-indigo ring-2 ring-background\"\n aria-hidden=\"true\"\n />\n ) : null}\n </div>\n\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <p className=\"truncate text-sm font-medium leading-5 tracking-tight text-foreground\">\n {notification.title}\n </p>\n\n <div className=\"text-xs leading-4 text-muted-foreground\">\n {timeAgo ? <span className=\"whitespace-nowrap\">{timeAgo}</span> : null}\n </div>\n\n <div className=\"mt-2 flex items-center gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-8 rounded-md px-2.5 bg-accent-indigo text-accent-indigo-foreground hover:bg-accent-indigo/90\"\n onClick={(e) => {\n e.stopPropagation()\n handleView()\n }}\n disabled={executing || (!viewAction && !notification.linkHref)}\n >\n <ExternalLink className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ai_assistant.notifications.conversation_shared.view_button', 'View Conversation')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-8 rounded-md px-2.5\"\n onClick={(e) => {\n e.stopPropagation()\n onDismiss()\n }}\n >\n {t('notifications.actions.dismiss', 'Dismiss')}\n </Button>\n </div>\n </div>\n\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n className=\"absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100\"\n onClick={(e) => {\n e.stopPropagation()\n onDismiss()\n }}\n aria-label={t('notifications.actions.dismiss', 'Dismiss')}\n >\n <X className=\"size-3.5\" />\n </IconButton>\n </div>\n )\n}\n\nexport default ConversationSharedRenderer\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Share2, ExternalLink, X } from 'lucide-react'\nimport { useRouter } from 'next/navigation'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { formatRelativeTime } from '@open-mercato/shared/lib/time'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { NotificationRendererProps } from '@open-mercato/shared/modules/notifications/types'\n\nexport function ConversationSharedRenderer({\n notification,\n onAction,\n onDismiss,\n actions = [],\n}: NotificationRendererProps) {\n const t = useT()\n const locale = useLocale()\n const router = useRouter()\n const [executing, setExecuting] = React.useState(false)\n const isUnread = notification.status === 'unread'\n const viewAction = actions.find((a) => a.id === 'view') ?? actions[0] ?? null\n\n const handleView = async () => {\n setExecuting(true)\n try {\n if (viewAction && notification.status !== 'actioned') {\n await onAction(viewAction.id)\n }\n if (notification.linkHref) router.push(notification.linkHref)\n } finally {\n setExecuting(false)\n }\n }\n\n const timeAgo = formatRelativeTime(notification.createdAt, { locale, translate: t }) ?? ''\n\n return (\n <div\n className={cn(\n 'group relative flex gap-4 items-start rounded-xl p-3 transition-colors hover:bg-muted/40 cursor-pointer',\n isUnread && 'bg-muted/20',\n )}\n onClick={executing ? undefined : handleView}\n onKeyDown={(e) => {\n if (executing) return\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n handleView()\n }\n }}\n role=\"button\"\n tabIndex={0}\n >\n <div className=\"relative shrink-0 flex size-10 items-center justify-center rounded-full bg-status-info-bg\">\n <Share2 className=\"size-5 text-status-info-icon\" aria-hidden=\"true\" />\n {isUnread ? (\n <span\n className=\"absolute -right-1 -top-1 size-3 rounded-full bg-accent-indigo ring-2 ring-background\"\n aria-hidden=\"true\"\n />\n ) : null}\n </div>\n\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <p className=\"truncate text-sm font-medium leading-5 tracking-tight text-foreground\">\n {notification.title}\n </p>\n\n <div className=\"text-xs leading-4 text-muted-foreground\">\n {timeAgo ? <span className=\"whitespace-nowrap\">{timeAgo}</span> : null}\n </div>\n\n <div className=\"mt-2 flex items-center gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n className=\"h-8 rounded-md px-2.5 bg-accent-indigo text-accent-indigo-foreground hover:bg-accent-indigo/90\"\n onClick={(e) => {\n e.stopPropagation()\n handleView()\n }}\n disabled={executing || (!viewAction && !notification.linkHref) || (notification.status === 'actioned' && !notification.linkHref)}\n >\n <ExternalLink className=\"size-3.5\" aria-hidden=\"true\" />\n {t('ai_assistant.notifications.conversation_shared.view_button', 'View Conversation')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-8 rounded-md px-2.5\"\n onClick={(e) => {\n e.stopPropagation()\n onDismiss()\n }}\n >\n {t('notifications.actions.dismiss', 'Dismiss')}\n </Button>\n </div>\n </div>\n\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n size=\"xs\"\n className=\"absolute right-2 top-2 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100\"\n onClick={(e) => {\n e.stopPropagation()\n onDismiss()\n }}\n aria-label={t('notifications.actions.dismiss', 'Dismiss')}\n >\n <X className=\"size-3.5\" />\n </IconButton>\n </div>\n )\n}\n\nexport default ConversationSharedRenderer\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ai-assistant",
3
- "version": "0.8.1-develop.7296.1.2111d779db",
3
+ "version": "0.9.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -100,16 +100,16 @@
100
100
  "zod-to-json-schema": "^3.25.2"
101
101
  },
102
102
  "peerDependencies": {
103
- "@open-mercato/shared": "0.8.1-develop.7296.1.2111d779db",
104
- "@open-mercato/ui": "0.8.1-develop.7296.1.2111d779db",
103
+ "@open-mercato/shared": "0.9.0",
104
+ "@open-mercato/ui": "0.9.0",
105
105
  "react": "^19.0.0",
106
106
  "react-dom": "^19.0.0",
107
107
  "zod": ">=3.23.0"
108
108
  },
109
109
  "devDependencies": {
110
- "@open-mercato/cli": "0.8.1-develop.7296.1.2111d779db",
111
- "@open-mercato/shared": "0.8.1-develop.7296.1.2111d779db",
112
- "@open-mercato/ui": "0.8.1-develop.7296.1.2111d779db",
110
+ "@open-mercato/cli": "0.9.0",
111
+ "@open-mercato/shared": "0.9.0",
112
+ "@open-mercato/ui": "0.9.0",
113
113
  "@types/react": "^19.2.18",
114
114
  "@types/react-dom": "^19.2.5",
115
115
  "react": "19.2.8",
@@ -124,6 +124,5 @@
124
124
  "type": "git",
125
125
  "url": "https://github.com/open-mercato/open-mercato",
126
126
  "directory": "packages/ai-assistant"
127
- },
128
- "stableVersion": "0.8.0"
127
+ }
129
128
  }
@@ -0,0 +1,175 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { MetadataStorage } from '@mikro-orm/core'
3
+ import { isEncryptedPayloadShape } from '@open-mercato/shared/lib/encryption/aes'
4
+ import type { KmsService } from '@open-mercato/shared/lib/encryption/kms'
5
+ import { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'
6
+ import { defaultEncryptionMaps } from '../encryption'
7
+ import * as aiAssistantEntities from '../data/entities'
8
+
9
+ const MODULE_ID = 'ai_assistant'
10
+
11
+ function toSnake(value: string): string {
12
+ return value
13
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
14
+ .replace(/\W+/g, '_')
15
+ .replace(/_{2,}/g, '_')
16
+ .replace(/(?:^_+|_+$)/g, '')
17
+ .toLowerCase()
18
+ }
19
+
20
+ type EntityMetadataLike = {
21
+ className?: string
22
+ properties?: Record<string, { name?: string; fieldName?: string }>
23
+ }
24
+
25
+ function collectRegisteredEntities(): Map<string, Set<string>> {
26
+ void aiAssistantEntities
27
+ const registered = new Map<string, Set<string>>()
28
+ for (const meta of Object.values(MetadataStorage.getMetadata()) as EntityMetadataLike[]) {
29
+ if (!meta.className) continue
30
+ const columns = new Set<string>()
31
+ for (const [propertyName, property] of Object.entries(meta.properties ?? {})) {
32
+ columns.add(property.fieldName ?? toSnake(property.name ?? propertyName))
33
+ }
34
+ registered.set(`${MODULE_ID}:${toSnake(meta.className)}`, columns)
35
+ }
36
+ return registered
37
+ }
38
+
39
+ function fieldsOf(entityId: string): string[] {
40
+ const map = defaultEncryptionMaps.find((entry) => entry.entityId === entityId)
41
+ return (map?.fields ?? []).map((field) => (typeof field === 'string' ? field : field.field))
42
+ }
43
+
44
+ describe('ai_assistant encryption maps', () => {
45
+ const registered = collectRegisteredEntities()
46
+
47
+ it('encrypts chat message content and its structured payloads', () => {
48
+ expect(fieldsOf('ai_assistant:ai_chat_message')).toEqual([
49
+ 'content',
50
+ 'ui_parts',
51
+ 'files_metadata',
52
+ 'metadata',
53
+ ])
54
+ })
55
+
56
+ it('encrypts the conversation title', () => {
57
+ expect(fieldsOf('ai_assistant:ai_chat_conversation')).toEqual(['title'])
58
+ })
59
+
60
+ it('encrypts the pending-action mutation payloads', () => {
61
+ expect(fieldsOf('ai_assistant:ai_pending_action')).toEqual([
62
+ 'normalized_input',
63
+ 'field_diff',
64
+ 'records',
65
+ ])
66
+ })
67
+
68
+ it('keeps every map tenant-scoped so rows seal under the tenant key', () => {
69
+ for (const map of defaultEncryptionMaps) {
70
+ expect(map.keyScope).toBeUndefined()
71
+ }
72
+ })
73
+
74
+ it.each(defaultEncryptionMaps.map((map) => [map.entityId, map] as const))(
75
+ 'resolves %s to a registered entity and its columns',
76
+ (entityId, map) => {
77
+ const columns = registered.get(entityId)
78
+ expect(columns).toBeDefined()
79
+ const unknownFields = map.fields
80
+ .map((field) => (typeof field === 'string' ? field : field.field))
81
+ .filter((field) => !columns?.has(field))
82
+ expect(unknownFields).toEqual([])
83
+ },
84
+ )
85
+ })
86
+
87
+ describe('ai_assistant encryption maps applied by the tenant encryption service', () => {
88
+ const dekKey = Buffer.alloc(32, 7).toString('base64')
89
+ const originalToggle = process.env.TENANT_DATA_ENCRYPTION
90
+
91
+ beforeAll(() => {
92
+ process.env.TENANT_DATA_ENCRYPTION = 'yes'
93
+ })
94
+
95
+ afterAll(() => {
96
+ if (originalToggle === undefined) delete process.env.TENANT_DATA_ENCRYPTION
97
+ else process.env.TENANT_DATA_ENCRYPTION = originalToggle
98
+ })
99
+
100
+ function createService() {
101
+ const em = {
102
+ execute: async (_sql: string, params: unknown[]) => {
103
+ const map = defaultEncryptionMaps.find((entry) => entry.entityId === params[0])
104
+ return map ? [{ entity_id: map.entityId, fields_json: map.fields }] : []
105
+ },
106
+ }
107
+ const kms: KmsService = {
108
+ isHealthy: () => true,
109
+ getTenantDek: async (tenantId) => ({ tenantId, key: dekKey, fetchedAt: Date.now() }),
110
+ createTenantDek: async (tenantId) => ({ tenantId, key: dekKey, fetchedAt: Date.now() }),
111
+ }
112
+ return new TenantDataEncryptionService(em as never, { kms })
113
+ }
114
+
115
+ it('stores chat message content and payloads as ciphertext and restores them on read', async () => {
116
+ const service = createService()
117
+ const tenantId = randomUUID()
118
+ const organizationId = randomUUID()
119
+ const message = {
120
+ conversation_id: 'conv-1',
121
+ role: 'user',
122
+ content: 'Raise the credit limit for ACME to 50k',
123
+ ui_parts: [{ componentId: 'field-diff-card', props: { field: 'creditLimit' } }],
124
+ files_metadata: [{ name: 'contract.pdf' }],
125
+ metadata: { note: 'customer asked by phone' },
126
+ }
127
+
128
+ const encrypted = await service.encryptEntityPayload('ai_assistant:ai_chat_message', message, tenantId, organizationId)
129
+
130
+ for (const field of ['content', 'ui_parts', 'files_metadata', 'metadata'] as const) {
131
+ expect(isEncryptedPayloadShape(encrypted[field])).toBe(true)
132
+ expect(String(encrypted[field])).not.toContain('ACME')
133
+ }
134
+ expect(encrypted.conversation_id).toBe('conv-1')
135
+ expect(encrypted.role).toBe('user')
136
+
137
+ const decrypted = await service.decryptEntityPayload('ai_assistant:ai_chat_message', encrypted, tenantId, organizationId)
138
+ expect(decrypted.content).toBe(message.content)
139
+ expect(JSON.parse(String(decrypted.ui_parts))).toEqual(message.ui_parts)
140
+ expect(JSON.parse(String(decrypted.metadata))).toEqual(message.metadata)
141
+ })
142
+
143
+ it('stores pending-action mutation payloads as ciphertext', async () => {
144
+ const service = createService()
145
+ const encrypted = await service.encryptEntityPayload(
146
+ 'ai_assistant:ai_pending_action',
147
+ {
148
+ tool_name: 'customers.update_company',
149
+ normalized_input: { name: 'ACME Holdings' },
150
+ field_diff: [{ field: 'name', before: 'ACME', after: 'ACME Holdings' }],
151
+ records: [{ recordId: 'rec-1', label: 'ACME' }],
152
+ },
153
+ randomUUID(),
154
+ randomUUID(),
155
+ )
156
+
157
+ for (const field of ['normalized_input', 'field_diff', 'records'] as const) {
158
+ expect(isEncryptedPayloadShape(encrypted[field])).toBe(true)
159
+ }
160
+ expect(encrypted.tool_name).toBe('customers.update_company')
161
+ })
162
+
163
+ it('stores the conversation title as ciphertext', async () => {
164
+ const service = createService()
165
+ const encrypted = await service.encryptEntityPayload(
166
+ 'ai_assistant:ai_chat_conversation',
167
+ { title: 'ACME credit review', agent_id: 'customers.account_assistant' },
168
+ randomUUID(),
169
+ randomUUID(),
170
+ )
171
+
172
+ expect(isEncryptedPayloadShape(encrypted.title)).toBe(true)
173
+ expect(encrypted.agent_id).toBe('customers.account_assistant')
174
+ })
175
+ })
@@ -19,10 +19,11 @@ import type {
19
19
  *
20
20
  * Owner-first MVP per spec
21
21
  * `2026-05-05-ai-chat-server-side-conversation-storage`. Every read/write
22
- * goes through `findOneWithDecryption` / `findWithDecryption` so the repo
23
- * stays consistent with the rest of the module and is GDPR-encryption-ready
24
- * without a second refactor when `content` / `ui_parts` columns are
25
- * eventually flagged.
22
+ * goes through `findOneWithDecryption` / `findWithDecryption` because the
23
+ * conversation `title` and the message `content` / `ui_parts` /
24
+ * `files_metadata` / `metadata` columns are declared in the module's
25
+ * `encryption.ts` and are encrypted at rest when tenant data encryption is
26
+ * enabled.
26
27
  *
27
28
  * Tenant + organization scope is required on every method. View-only callers
28
29
  * are owner-scoped. Callers with `ai_assistant.conversations.manage` may
@@ -66,9 +66,9 @@ export interface AiPendingActionSetStatusExtra {
66
66
  * - `listExpired` for the cleanup worker (Step 5.12).
67
67
  *
68
68
  * Every read goes through `findOneWithDecryption` / `findWithDecryption`
69
- * even though today no column is GDPR-flagged. This keeps the repo
70
- * consistent with the rest of the module and preps for a future encrypted
71
- * `normalizedInput` without a second refactor.
69
+ * because `normalized_input`, `field_diff` and `records` are declared in the
70
+ * module's `encryption.ts` and are encrypted at rest when tenant data
71
+ * encryption is enabled.
72
72
  */
73
73
  export class AiPendingActionRepository {
74
74
  constructor(private readonly em: EntityManager) {}
@@ -0,0 +1,29 @@
1
+ import type { ModuleEncryptionMap } from '@open-mercato/shared/modules/encryption'
2
+
3
+ export const defaultEncryptionMaps: ModuleEncryptionMap[] = [
4
+ {
5
+ entityId: 'ai_assistant:ai_chat_message',
6
+ fields: [
7
+ { field: 'content' },
8
+ { field: 'ui_parts' },
9
+ { field: 'files_metadata' },
10
+ { field: 'metadata' },
11
+ ],
12
+ },
13
+ {
14
+ entityId: 'ai_assistant:ai_chat_conversation',
15
+ fields: [
16
+ { field: 'title' },
17
+ ],
18
+ },
19
+ {
20
+ entityId: 'ai_assistant:ai_pending_action',
21
+ fields: [
22
+ { field: 'normalized_input' },
23
+ { field: 'field_diff' },
24
+ { field: 'records' },
25
+ ],
26
+ },
27
+ ]
28
+
29
+ export default defaultEncryptionMaps