@aglyn/plugins-ai 1.0.0-beta.250 → 1.0.0-beta.253
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/ai-brief-dialog.component.js +15 -1
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-links.js +1 -1
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.js +20 -2
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-jobs-store.d.ts +11 -0
- package/src/lib/components/ai-jobs-store.js +22 -0
- package/src/lib/components/ai-jobs-store.js.map +1 -1
- package/src/lib/jobs/ai-job-crm-step.js +7 -4
- package/src/lib/jobs/ai-job-crm-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +24 -0
- package/src/lib/jobs/ai-job-drafts.js +46 -0
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-experiment-step.js +6 -1
- package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
- package/src/lib/jobs/ai-job-form-dataset.d.ts +6 -1
- package/src/lib/jobs/ai-job-form-dataset.js +7 -2
- package/src/lib/jobs/ai-job-form-dataset.js.map +1 -1
- package/src/lib/jobs/ai-job-form-reuse.d.ts +78 -0
- package/src/lib/jobs/ai-job-form-reuse.js +136 -0
- package/src/lib/jobs/ai-job-form-reuse.js.map +1 -0
- package/src/lib/jobs/ai-job-form-step.d.ts +17 -0
- package/src/lib/jobs/ai-job-form-step.js +63 -11
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.js +6 -1
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +10 -5
- package/src/lib/jobs/ai-job-page-language.js +10 -5
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +32 -1
- package/src/lib/jobs/ai-job-plan-step.js +53 -2
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-seo-step.js +5 -3
- package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +7 -5
- package/src/lib/jobs/ai-job-site-content.js +7 -5
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +1 -1
- package/src/lib/jobs/ai-job-site-datasets.js +2 -2
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +2 -1
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.js +6 -3
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +11 -7
- package/src/lib/jobs/ai-layout-stock-photos.js +25 -10
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-stock-business-nouns.d.ts +62 -0
- package/src/lib/jobs/ai-stock-business-nouns.js +264 -0
- package/src/lib/jobs/ai-stock-business-nouns.js.map +1 -0
- package/src/lib/layout-language/ai-layout-language.js +5 -1
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/layout-language/ai-layout-links.js +5 -2
- package/src/lib/layout-language/ai-layout-links.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +7 -3
- package/src/lib/model/ai-build-plan.js +11 -1
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +39 -19
- package/src/lib/model/ai-credit-estimate.js +43 -19
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +30 -0
- package/src/lib/model/ai-job-activity.js +35 -0
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-media-credits.d.ts +5 -3
- package/src/lib/model/ai-media-credits.js +7 -5
- package/src/lib/model/ai-media-credits.js.map +1 -1
- package/src/lib/model/ai-site-job.js +6 -3
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-start-seo.d.ts +11 -2
- package/src/lib/model/ai-site-start-seo.js +29 -15
- package/src/lib/model/ai-site-start-seo.js.map +1 -1
- package/src/lib/model/ai-site-store-pages.d.ts +5 -0
- package/src/lib/model/ai-site-store-pages.js +2 -0
- package/src/lib/model/ai-site-store-pages.js.map +1 -1
- package/src/lib/plugin-config.js +2 -2
- package/src/lib/plugin-config.js.map +1 -1
- package/src/lib/providers/anthropic-crm-wording-superseded.d.ts +29 -0
- package/src/lib/providers/anthropic-crm-wording-superseded.js +31 -0
- package/src/lib/providers/anthropic-crm-wording-superseded.js.map +1 -0
- package/src/lib/providers/anthropic.js +29 -7
- package/src/lib/providers/anthropic.js.map +1 -1
- package/src/lib/providers/catalog.d.ts +24 -17
- package/src/lib/providers/catalog.js +63 -30
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/providers/contract.d.ts +19 -1
- package/src/lib/providers/contract.js +10 -0
- package/src/lib/providers/contract.js.map +1 -1
- package/src/lib/providers/model-choice.d.ts +1 -1
- package/src/lib/providers/model-choice.js +39 -30
- package/src/lib/providers/model-choice.js.map +1 -1
- package/src/lib/providers/routing.d.ts +20 -1
- package/src/lib/providers/routing.js +71 -12
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-dev-env.d.ts +58 -1
- package/src/lib/runtime/ai-dev-env.js +86 -6
- package/src/lib/runtime/ai-dev-env.js.map +1 -1
- package/src/lib/runtime/ai-dev-replay.d.ts +4 -0
- package/src/lib/runtime/ai-dev-replay.js +4 -0
- package/src/lib/runtime/ai-dev-replay.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.js +13 -3
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +8 -4
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.d.ts +43 -1
- package/src/lib/runtime/ai-eval-live.js +79 -9
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-live-batch.d.ts +20 -4
- package/src/lib/runtime/ai-live-batch.js +57 -10
- package/src/lib/runtime/ai-live-batch.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +4 -0
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-runtime.js +21 -4
- package/src/lib/runtime/ai-runtime.js.map +1 -1
- package/src/lib/runtime/seo-fields.js +4 -3
- package/src/lib/runtime/seo-fields.js.map +1 -1
- package/src/lib/server/ai-assist.js +3 -2
- package/src/lib/server/ai-assist.js.map +1 -1
- package/src/lib/server/ai-media-image.d.ts +5 -1
- package/src/lib/server/ai-media-image.js +6 -2
- package/src/lib/server/ai-media-image.js.map +1 -1
- package/src/lib/server/ai-media-svg.js +4 -0
- package/src/lib/server/ai-media-svg.js.map +1 -1
- package/src/lib/usage/ai-allotment-alerts.js +2 -5
- package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-media-image.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// lockdown-423: via libs/plugins/ai/src/lib/runtime/ai-gate.ts\n// The POST climbs `aiGateLadder`, whose lockdown rung is the verdict, and\n// every stored picture also passes the upload route's.\n\nimport { mediaFilterKeys } from '@aglyn/aglyn/app-utils/media-metadata'\nimport { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { MEDIA_ALT_MAX_LENGTH } from '@aglyn/aglyn/app-utils/media-alt'\nimport { aiMediaCreditsPerPicture } from '../model/ai-media-credits'\nimport { aiSvgThemePalette, isAiSvgColor } from '../model/ai-svg'\nimport { estimateAiBilledUsd } from '../providers/catalog'\nimport { AI_UPSTREAM_FAILURE_COPY, AiUpstreamError, type AiUsage } from '../providers/contract'\nimport {\n AI_IMAGE_ASPECT_RATIOS,\n AI_IMAGE_MAX_COUNT,\n AI_IMAGE_PROMPT_MAX_CHARS,\n AI_SVG_MAX_COLORS,\n AI_SVG_STYLES,\n AiImageSafetyRefusal,\n aiImageSizeForPlan,\n isAiImageAspectRatio,\n isAiRasterStyle,\n type AiImageAspectRatio,\n type AiImageMode,\n type AiImageProvider,\n type AiRasterStyle,\n type AiSvgStyle,\n} from '../providers/image-contract'\nimport { vertexImageProvider } from '../providers/vertex-image'\nimport { aiGateLadder, type AiGateContext } from '../runtime/ai-gate'\nimport { aiInventoryTheme } from '../runtime/site-inventory'\nimport { assistBandRefuses, assistCreditsFromUsd } from '../usage/assist-credits'\nimport {\n publicAssistQuota,\n recordAssistCost,\n releaseAssistMessage,\n} from '../usage/assist-usage'\nimport { aiMediaRasterPrompt } from './ai-media-raster-prompt'\nimport { aiMediaSvgModel, generateAiMediaSvg, type AiMediaSvgOutcome } from './ai-media-svg'\n\n/**\n * \"Create with AI\" in Media (AGL-3602): pictures from a description, stored\n * in the library that asked.\n *\n * `POST /api/ai/media/images { orgId, library, hostId?, folderId?, mode,\n * prompt, aspectRatio, count, style?, palette? }` answers\n * `{ mediaIds, filtered, failed, credits, warning? }`.\n *\n * Two modes:\n *\n * - `photo` — Google's image models on Vertex AI (`vertex-image.ts`), for\n * every kind that is not SVG: a photo, a photographic genre, an art style\n * or a design asset, each the description followed by its kind's fixed\n * style wording (`ai-media-raster-prompt.ts`). Off unless the deployment\n * configured them; the door answers 404 with a sentence the dialog shows.\n * - `illustration` — an SVG drawn by the text provider every other AI door\n * uses (`ai-media-svg.ts`): an illustration, an icon, a tileable pattern or\n * a simple logo mark, in the site's theme colors or the person's own. No\n * new vendor, so it runs wherever text AI does.\n *\n * There is no GET: the button draws from the shell's own gates, and this door\n * decides when someone asks for a picture.\n *\n * ## A request, not a job\n *\n * Both modes answer inside the request, the way \"Save as a component with\n * AI\" does, and are metered on the same meter every door uses.\n *\n * ## The money, in order\n *\n * 1. `aiGateLadder` RESERVES before anything is spent: the `aiGenerative`\n * entitlement, `release_ai_generative`, the `ai-generate` kill switch,\n * `ai.generate` on the site, the per-account and per-address windows, the\n * Free taste's own rungs and the band.\n * 2. A workspace whose band is a wall (Free, or a hard cap) is refused before\n * the provider is called when the pictures' estimate is more than it has\n * left.\n * 3. The provider is called. A fault of ours or the provider's hands the\n * reservation back and charges nothing. A declined description charges\n * only what the decline spent, recorded as a declined request (which a\n * Free workspace is never charged for, and which its daily refusal pause\n * counts).\n * 4. Each picture is stored, and only what was stored is SETTLED, at billed\n * rates above the provider's: a photo per picture plus its prompt and\n * thinking, an illustration as the tokens that drew it. A picture held\n * back, one the library refused, and an illustration that failed its\n * safety check twice are never charged — that last one is ours.\n *\n * ## Stored the way any upload is stored\n *\n * Each picture is posted to the console's own `/api/media/upload` with the\n * caller's credential, so the scope, the uploads lockdown, the structural\n * inspection, SVG sanitizing, the quarantine list, the storage band, the CDN\n * variants and the storage counter all apply exactly as they do to a file\n * the person dropped in. This door writes no media document of its own; it\n * only adds, to the documents that route made, the alt text and the record\n * of how the picture was made.\n */\n\n/** The per-account window: a person makes a few sets a minute, not dozens. */\nconst RATE_LIMIT = { key: 'ai-media-image', limit: 6, windowMs: 60_000 }\n\n/** What a person reads when a picture failed on our side. */\nexport const AI_MEDIA_OURS_COPY =\n \"This one's on us — you weren't charged. The picture didn't come out right; try again, or describe it a little differently.\"\n\n/** What the door answers on a deployment that makes no photos. */\nexport const AI_MEDIA_NO_PHOTOS_COPY =\n \"Photos and painted styles aren't available here yet. Choose a vector kind, such as Illustration, to draw one instead.\"\n\n/** The provider photos come from; a spec stands one in through jest. */\nfunction imageProvider(): AiImageProvider {\n return vertexImageProvider\n}\n\n/** Every control character as a space, squeezed, trimmed and cut. */\nexport function cleanAiImagePrompt(value: unknown): string {\n let out = ''\n for (const char of String(value ?? '')) {\n const code = char.charCodeAt(0)\n out += code < 32 || code === 127 || code === 0x2028 || code === 0x2029 ? ' ' : char\n }\n return out.replace(/\\s+/g, ' ').trim().slice(0, AI_IMAGE_PROMPT_MAX_CHARS)\n}\n\n/**\n * The alt text a picture is stored with: the sentence given, as a sentence,\n * cut at a word inside the library's alt limit. A person edits it like any\n * other.\n */\nexport function aiImageAltText(text: string): string {\n const clean = text.replace(/\\s+/g, ' ').trim()\n if (!clean) return ''\n const sentence = clean.charAt(0).toUpperCase() + clean.slice(1)\n if (sentence.length <= MEDIA_ALT_MAX_LENGTH) return sentence\n const cut = sentence.slice(0, MEDIA_ALT_MAX_LENGTH - 1)\n const space = cut.lastIndexOf(' ')\n return `${(space > 40 ? cut.slice(0, space) : cut).trim()}…`\n}\n\nconst EXTENSIONS: Readonly<Record<string, string>> = {\n 'image/png': 'png',\n 'image/jpeg': 'jpg',\n 'image/webp': 'webp',\n 'image/svg+xml': 'svg',\n}\n\n/** A file name from the description's first words: `ai-a-red-barn-2.png`. */\nexport function aiImageFileName(prompt: string, index: number, mimeType: string): string {\n const words = prompt\n .toLowerCase()\n .normalize('NFKD')\n .replace(/[^a-z0-9\\s-]/g, ' ')\n .split(/\\s+/)\n .filter(Boolean)\n .slice(0, 6)\n .join('-')\n .slice(0, 60)\n .replace(/-+$/, '')\n return `ai-${words || 'image'}-${index + 1}.${EXTENSIONS[mimeType] ?? 'png'}`\n}\n\n/** What the library asked for, read and bounded. */\nexport interface AiMediaImageInput {\n orgId: string\n library: 'host' | 'org'\n /** The site whose library this is, or the site on screen for the org's. */\n hostId: string | null\n folderId: string | null\n mode: AiImageMode\n prompt: string\n aspectRatio: string\n count: number\n /** The kind: an SVG style for an illustration, an image-provider style for a photo. */\n style: string\n /** An illustration's colors: the site theme's, or these. */\n palette: { source: 'theme' } | { source: 'custom'; colors: string[] }\n}\n\nexport function parseAiMediaImageInput(body: Record<string, unknown> | null): AiMediaImageInput {\n const text = (key: string, limit = 128) => {\n const value = body?.[key]\n return typeof value === 'string' ? value.trim().slice(0, limit) : ''\n }\n const count = Math.floor(Number(body?.['count'] ?? 1))\n const palette = body?.['palette'] as Record<string, unknown> | undefined\n const colors = Array.isArray(palette?.['colors']) ? (palette?.['colors'] as unknown[]) : []\n return {\n orgId: text('orgId'),\n library: body?.['library'] === 'org' ? 'org' : 'host',\n hostId: text('hostId') || null,\n folderId: text('folderId', 64) || null,\n mode: body?.['mode'] === 'illustration' ? 'illustration' : 'photo',\n prompt: cleanAiImagePrompt(body?.['prompt']),\n aspectRatio: text('aspectRatio', 8),\n count: Number.isFinite(count) ? count : 0,\n style: text('style', 24) || (body?.['mode'] === 'illustration' ? 'illustration' : 'photo'),\n palette:\n palette?.['source'] === 'custom'\n ? {\n source: 'custom',\n colors: colors\n .map((color) => String(color ?? '').trim().toLowerCase())\n .slice(0, AI_SVG_MAX_COLORS + 1),\n }\n : { source: 'theme' },\n }\n}\n\n/** One stored picture, or the upload route's refusal. */\ntype StoreOutcome = { mediaId: string } | { error: string; status: number }\n\n/**\n * Posts one picture to the console's own upload route, as the caller. The\n * same body the library's Upload button sends, so the route cannot tell the\n * two apart and applies every check it applies to that one.\n */\nasync function storeThroughUploadRoute(\n request: Request,\n input: AiMediaImageInput,\n image: { base64: string; mimeType: string },\n fileName: string,\n): Promise<StoreOutcome> {\n const scope =\n input.library === 'org'\n ? { orgId: input.orgId, ...(input.hostId ? { forHostId: input.hostId } : {}) }\n : { hostId: input.hostId }\n let response: Response\n try {\n response = await fetch(new URL('/api/media/upload', request.url), {\n method: 'POST',\n headers: {\n Authorization: request.headers.get('authorization') ?? '',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n ...scope,\n fileName,\n contentType: image.mimeType,\n folderId: input.folderId,\n data: image.base64,\n }),\n })\n } catch {\n return { error: 'The picture could not be saved to the media library.', status: 502 }\n }\n const payload = (await response.json().catch(() => ({}))) as Record<string, unknown>\n const mediaId = typeof payload['mediaId'] === 'string' ? payload['mediaId'] : ''\n if (response.ok && mediaId) return { mediaId }\n return {\n error:\n typeof payload['error'] === 'string'\n ? payload['error']\n : 'The picture could not be saved to the media library.',\n status: response.ok ? 502 : response.status,\n }\n}\n\n/** The record a generated picture keeps of how it was made. */\nexport interface AiMediaProvenance {\n model: string\n prompt: string\n mode: AiImageMode\n /** The kind the picture was made as. */\n style?: AiSvgStyle | AiRasterStyle\n aspectRatio: string\n /** The meter's signal id for the request that made it. */\n signalId: string | null\n generatedBy: string\n}\n\n/**\n * Adds the alt text and the provenance to a document the upload route made.\n * An `update`, which cannot mint a document: the route minted it.\n */\nasync function annotateStoredImage(\n gate: AiGateContext,\n input: AiMediaImageInput,\n mediaId: string,\n alt: string,\n provenance: AiMediaProvenance,\n): Promise<void> {\n const scopeRef =\n input.library === 'org'\n ? gate.firestore.collection('orgs').doc(input.orgId)\n : gate.firestore.collection('hosts').doc(String(input.hostId))\n const mediaRef = scopeRef.collection('media').doc(mediaId)\n const stored = ((await mediaRef.get()).data() ?? {}) as Parameters<typeof mediaFilterKeys>[0]\n await mediaRef.update({\n alt,\n description: 'Created with AI from a description.',\n aiGenerated: { ...provenance, createdAt: new Date() },\n // What the library filters by, restamped with the alt now set: the upload\n // route wrote \"Alt text is missing\" before the alt existed.\n ...mediaFilterKeys({ ...stored, alt }),\n })\n}\n\n/** The colors an illustration is drawn in. */\nasync function illustrationPalette(\n gate: AiGateContext,\n input: AiMediaImageInput,\n): Promise<string[]> {\n if (input.palette.source === 'custom') return input.palette.colors\n if (!input.hostId) return []\n const snapshot = await gate.firestore.collection('hosts').doc(input.hostId).get()\n return aiSvgThemePalette(aiInventoryTheme(snapshot.exists ? snapshot.data() ?? null : null)?.colors)\n}\n\nconst ZERO_USAGE: AiUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }\n\nconst sumUsage = (usages: readonly AiUsage[]): AiUsage =>\n usages.reduce<AiUsage>(\n (sum, usage) => ({\n inputTokens: sum.inputTokens + usage.inputTokens,\n outputTokens: sum.outputTokens + usage.outputTokens,\n cacheReadTokens: sum.cacheReadTokens + usage.cacheReadTokens,\n cacheWriteTokens: sum.cacheWriteTokens + usage.cacheWriteTokens,\n }),\n ZERO_USAGE,\n )\n\n/** A picture made, before it is stored. */\ninterface MadePicture {\n base64: string\n mimeType: string\n alt: string\n /** What making it spent, charged only once it is stored. */\n usage: AiUsage\n}\n\nexport async function POST(request: Request): Promise<Response> {\n let body: Record<string, unknown> | null\n try {\n body = (await request.json()) as Record<string, unknown>\n } catch {\n body = null\n }\n const input = parseAiMediaImageInput(body)\n const provider = imageProvider()\n const svgModel = input.mode === 'illustration' ? aiMediaSvgModel() : undefined\n // A deployment that makes no pictures of this kind has no such door, so\n // nothing about the caller or the workspace is read first.\n if (input.mode === 'photo' && !provider.configured()) {\n return Response.json({ error: AI_MEDIA_NO_PHOTOS_COPY, reason: 'unavailable' }, { status: 404 })\n }\n if (input.mode === 'illustration' && !svgModel) {\n return Response.json({ error: 'Not found' }, { status: 404 })\n }\n\n const gate = await aiGateLadder(\n { request, orgId: input.orgId, hostId: input.hostId },\n {\n feature: 'aiGenerative',\n releaseFlag: 'release_ai_generative',\n lockdownFeature: 'ai-generate',\n permission: 'ai.generate',\n rateLimit: RATE_LIMIT,\n },\n )\n if (gate instanceof Response) return gate\n // Nothing is spent until a provider answers, so every exit before it\n // hands the reservation back.\n const release = () =>\n releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(() => undefined)\n const refuse = async (error: string, status: number, extra: Record<string, unknown> = {}) => {\n await release()\n return Response.json({ error, ...extra }, { status })\n }\n\n if (input.library === 'host' && !input.hostId) {\n return refuse('Open the site whose media library the pictures go in.', 400)\n }\n if (!input.prompt) return refuse('Describe the picture you want first.', 400)\n if (!isAiImageAspectRatio(input.aspectRatio)) {\n return refuse(`Choose a shape: ${AI_IMAGE_ASPECT_RATIOS.join(', ')}.`, 400)\n }\n const aspectRatio: AiImageAspectRatio = input.aspectRatio\n if (input.count < 1 || input.count > AI_IMAGE_MAX_COUNT) {\n return refuse(`Make between 1 and ${AI_IMAGE_MAX_COUNT} pictures at a time.`, 400)\n }\n if (input.mode === 'photo' && !isAiRasterStyle(input.style)) {\n return refuse('Choose a kind of picture from the list.', 400)\n }\n if (input.mode === 'illustration') {\n if (!(AI_SVG_STYLES as readonly string[]).includes(input.style)) {\n return refuse('Choose an illustration, an icon, a pattern or a logo mark.', 400)\n }\n if (\n input.palette.source === 'custom' &&\n (!input.palette.colors.length ||\n input.palette.colors.length > AI_SVG_MAX_COLORS ||\n !input.palette.colors.every(isAiSvgColor))\n ) {\n return refuse(`Give between 1 and ${AI_SVG_MAX_COLORS} colors as hex values, such as #1a73e8.`, 400)\n }\n }\n\n const model = input.mode === 'photo' ? provider.defaultModel() : (svgModel as string)\n // The size is the plan's, never the request's: 512 px on Free, 1K on a\n // paid plan, each estimated and charged at its own per-picture rate.\n const imageSize = aiImageSizeForPlan(resolveEffectivePlan(gate.org) === 'free')\n const perPicture = aiMediaCreditsPerPicture(input.mode, model, imageSize)\n const estimate = perPicture * input.count\n const credits = publicAssistQuota(gate.reservation).credits\n if (credits && assistBandRefuses(gate.org) && credits.remaining < estimate) {\n return refuse(\n credits.remaining < perPicture\n ? 'This workspace has used its AI credits for the month.'\n : `That needs about ${estimate} credits and this workspace has ${credits.remaining} left this month. Make fewer pictures.`,\n 429,\n { reason: 'quota', quota: publicAssistQuota(gate.reservation) },\n )\n }\n\n /** Records what a decline spent: a declined request, never a picture. */\n const recordDecline = (usage: AiUsage, declinedModel: string) =>\n recordAssistCost(gate.firestore, gate.orgId, {\n route: '/api/ai/media/images',\n hostId: input.hostId,\n model: declinedModel,\n tier: 'entitled',\n usage: { ...usage, images: 0 },\n docsPaths: [],\n stopReason: 'refusal',\n free: gate.reservation.free ?? null,\n uid: gate.uid,\n kind: 'image',\n }).catch((meterError) =>\n console.error('ai media refusal not recorded', { orgId: gate.orgId, meterError }),\n )\n\n let made: MadePicture[]\n /** Pictures asked for and not made: held back, declined or failed on our side. */\n let unmade: number\n /** Tokens a photo request spent beside its pictures, charged once any is stored. */\n let photoUsage: AiUsage = ZERO_USAGE\n let declineReason: string | null = null\n let style: AiSvgStyle | AiRasterStyle | undefined\n\n if (input.mode === 'photo') {\n style = input.style as AiRasterStyle\n try {\n const result = await provider.generate({\n model,\n prompt: aiMediaRasterPrompt(input.prompt, style),\n aspectRatio,\n size: imageSize,\n count: input.count,\n })\n photoUsage = result.usage\n unmade = result.filtered\n made = result.images.map((image) => ({\n ...image,\n alt: aiImageAltText(input.prompt),\n usage: ZERO_USAGE,\n }))\n } catch (error) {\n if (error instanceof AiImageSafetyRefusal) {\n // Declined, not failed: no picture is charged, and the request is\n // recorded as a declined one so the Free taste's refusal pause\n // counts it.\n await recordDecline(ZERO_USAGE, model)\n return Response.json({ error: error.message, reason: 'safety' }, { status: 422 })\n }\n await release()\n console.error('ai photo generation failed', { orgId: gate.orgId, error })\n const status = error instanceof AiUpstreamError && error.accountProblem ? 503 : 502\n return Response.json({ error: AI_UPSTREAM_FAILURE_COPY }, { status })\n }\n } else {\n const svgStyle = input.style as AiSvgStyle\n style = svgStyle\n const palette = await illustrationPalette(gate, input)\n const settled = await Promise.allSettled(\n Array.from({ length: input.count }, (_unused, variant) =>\n generateAiMediaSvg({\n model,\n prompt: input.prompt,\n style: svgStyle,\n aspectRatio,\n palette,\n variant,\n count: input.count,\n }),\n ),\n )\n const outcomes = settled.flatMap((entry) =>\n entry.status === 'fulfilled' ? [entry.value] : [],\n )\n if (!outcomes.length) {\n await release()\n console.error('ai illustration generation failed', {\n orgId: gate.orgId,\n error: (settled[0] as PromiseRejectedResult | undefined)?.reason,\n })\n return Response.json({ error: AI_UPSTREAM_FAILURE_COPY }, { status: 502 })\n }\n const drawn = outcomes.filter(\n (outcome): outcome is Extract<AiMediaSvgOutcome, { kind: 'drawn' }> => outcome.kind === 'drawn',\n )\n const declined = outcomes.filter((outcome) => outcome.kind === 'declined')\n unmade = input.count - drawn.length\n declineReason =\n (declined[0] as Extract<AiMediaSvgOutcome, { kind: 'declined' }> | undefined)?.reason ?? null\n made = drawn.map((outcome) => ({\n base64: Buffer.from(outcome.svg, 'utf8').toString('base64'),\n mimeType: 'image/svg+xml',\n alt: aiImageAltText(outcome.alt || input.prompt),\n usage: outcome.usage,\n }))\n if (!made.length) {\n if (declined.length) {\n // The model declined: what it spent is a declined request.\n await recordDecline(sumUsage(declined.map((outcome) => outcome.usage)), model)\n return Response.json({ error: declineReason, reason: 'safety' }, { status: 422 })\n }\n // Every picture failed its check twice. Ours, so nothing is charged.\n await release()\n return Response.json({ error: AI_MEDIA_OURS_COPY, reason: 'ours' }, { status: 502 })\n }\n }\n\n const stored: Array<{ mediaId: string; picture: MadePicture }> = []\n let refusal: { error: string; status: number } | null = null\n for (const [index, picture] of made.entries()) {\n const outcome = await storeThroughUploadRoute(\n request,\n input,\n picture,\n aiImageFileName(input.prompt, index, picture.mimeType),\n )\n if ('mediaId' in outcome) stored.push({ mediaId: outcome.mediaId, picture })\n else refusal ??= outcome\n }\n\n if (!stored.length) {\n // Made and not kept: the library refused every picture (its storage\n // band, a lockdown, a permission), so nothing is charged.\n await release()\n return Response.json(\n { error: refusal?.error ?? 'The pictures could not be saved.' },\n { status: refusal?.status ?? 502 },\n )\n }\n\n // Only what was stored is settled: a photo's pictures at the per-picture\n // rate with the request's prompt and thinking, an illustration's tokens.\n const usage =\n input.mode === 'photo'\n ? { ...photoUsage, images: stored.length, imageSize }\n : sumUsage(stored.map((entry) => entry.picture.usage))\n let signalId: string | null = null\n try {\n signalId = await recordAssistCost(gate.firestore, gate.orgId, {\n route: '/api/ai/media/images',\n hostId: input.hostId,\n model,\n tier: 'entitled',\n usage,\n docsPaths: [],\n stopReason: 'end_turn',\n free: gate.reservation.free ?? null,\n uid: gate.uid,\n kind: 'image',\n })\n } catch (error) {\n // The pictures are in the library and were paid for by us; a meter\n // that could not be written is not a reason to hide them.\n console.error('ai media usage not recorded', { orgId: gate.orgId, error })\n }\n\n await Promise.all(\n stored.map(({ mediaId, picture }) =>\n annotateStoredImage(gate, input, mediaId, picture.alt, {\n model,\n prompt: input.prompt,\n mode: input.mode,\n ...(style ? { style } : {}),\n aspectRatio,\n signalId,\n generatedBy: gate.uid,\n }).catch((error) => console.error('ai media alt text not written', { mediaId, error })),\n ),\n )\n\n const warning =\n refusal?.error ??\n (declineReason && unmade > 0 ? declineReason : unmade > 0 && input.mode === 'illustration'\n ? AI_MEDIA_OURS_COPY\n : null)\n return Response.json({\n mediaIds: stored.map((entry) => entry.mediaId),\n filtered: unmade,\n failed: made.length - stored.length,\n // What was charged, as the meter priced it.\n credits: assistCreditsFromUsd(estimateAiBilledUsd(usage, model)),\n ...(warning ? { warning } : {}),\n })\n}\n"],"names":["mediaFilterKeys","resolveEffectivePlan","MEDIA_ALT_MAX_LENGTH","aiMediaCreditsPerPicture","aiSvgThemePalette","isAiSvgColor","estimateAiBilledUsd","AI_UPSTREAM_FAILURE_COPY","AiUpstreamError","AI_IMAGE_ASPECT_RATIOS","AI_IMAGE_MAX_COUNT","AI_IMAGE_PROMPT_MAX_CHARS","AI_SVG_MAX_COLORS","AI_SVG_STYLES","AiImageSafetyRefusal","aiImageSizeForPlan","isAiImageAspectRatio","isAiRasterStyle","vertexImageProvider","aiGateLadder","aiInventoryTheme","assistBandRefuses","assistCreditsFromUsd","publicAssistQuota","recordAssistCost","releaseAssistMessage","aiMediaRasterPrompt","aiMediaSvgModel","generateAiMediaSvg","RATE_LIMIT","key","limit","windowMs","AI_MEDIA_OURS_COPY","AI_MEDIA_NO_PHOTOS_COPY","imageProvider","cleanAiImagePrompt","value","out","char","String","code","charCodeAt","replace","trim","slice","aiImageAltText","text","clean","sentence","charAt","toUpperCase","length","cut","space","lastIndexOf","EXTENSIONS","aiImageFileName","prompt","index","mimeType","words","toLowerCase","normalize","split","filter","Boolean","join","parseAiMediaImageInput","body","count","Math","floor","Number","palette","colors","Array","isArray","orgId","library","hostId","folderId","mode","aspectRatio","isFinite","style","source","map","color","storeThroughUploadRoute","request","input","image","fileName","scope","forHostId","response","fetch","URL","url","method","headers","Authorization","get","JSON","stringify","contentType","data","base64","error","status","payload","json","catch","mediaId","ok","annotateStoredImage","gate","alt","provenance","scopeRef","firestore","collection","doc","mediaRef","stored","update","description","aiGenerated","createdAt","Date","illustrationPalette","snapshot","exists","ZERO_USAGE","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","sumUsage","usages","reduce","sum","usage","POST","provider","svgModel","undefined","configured","Response","reason","feature","releaseFlag","lockdownFeature","permission","rateLimit","release","reservation","refuse","extra","includes","every","model","defaultModel","imageSize","org","perPicture","estimate","credits","remaining","quota","recordDecline","declinedModel","route","tier","images","docsPaths","stopReason","free","uid","kind","meterError","console","made","unmade","photoUsage","declineReason","result","generate","size","filtered","message","accountProblem","declined","svgStyle","settled","Promise","allSettled","from","_unused","variant","outcomes","flatMap","entry","drawn","outcome","Buffer","svg","toString","refusal","picture","entries","push","signalId","all","generatedBy","warning","mediaIds","failed"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-media-image.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// lockdown-423: via libs/plugins/ai/src/lib/runtime/ai-gate.ts\n// The POST climbs `aiGateLadder`, whose lockdown rung is the verdict, and\n// every stored picture also passes the upload route's.\n\nimport { mediaFilterKeys } from '@aglyn/aglyn/app-utils/media-metadata'\nimport { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { MEDIA_ALT_MAX_LENGTH } from '@aglyn/aglyn/app-utils/media-alt'\nimport { aiMediaCreditsPerPicture } from '../model/ai-media-credits'\nimport { aiSvgThemePalette, isAiSvgColor } from '../model/ai-svg'\nimport { estimateAiBilledUsd } from '../providers/catalog'\nimport { AI_UPSTREAM_FAILURE_COPY, AiUpstreamError, type AiUsage } from '../providers/contract'\nimport {\n AI_IMAGE_ASPECT_RATIOS,\n AI_IMAGE_MAX_COUNT,\n AI_IMAGE_PROMPT_MAX_CHARS,\n AI_SVG_MAX_COLORS,\n AI_SVG_STYLES,\n AiImageSafetyRefusal,\n aiImageSizeForPlan,\n isAiImageAspectRatio,\n isAiRasterStyle,\n type AiImageAspectRatio,\n type AiImageMode,\n type AiImageProvider,\n type AiRasterStyle,\n type AiSvgStyle,\n} from '../providers/image-contract'\nimport { vertexImageProvider } from '../providers/vertex-image'\nimport { aiGateLadder, type AiGateContext } from '../runtime/ai-gate'\nimport { aiInventoryTheme } from '../runtime/site-inventory'\nimport { assistBandRefuses, assistCreditsFromUsd } from '../usage/assist-credits'\nimport {\n publicAssistQuota,\n recordAssistCost,\n releaseAssistMessage,\n} from '../usage/assist-usage'\nimport { aiMediaRasterPrompt } from './ai-media-raster-prompt'\nimport { aiMediaSvgModel, generateAiMediaSvg, type AiMediaSvgOutcome } from './ai-media-svg'\n\n/**\n * \"Create with AI\" in Media (AGL-3602): pictures from a description, stored\n * in the library that asked.\n *\n * `POST /api/ai/media/images { orgId, library, hostId?, folderId?, mode,\n * prompt, aspectRatio, count, style?, palette? }` answers\n * `{ mediaIds, filtered, failed, credits, warning? }`.\n *\n * Two modes:\n *\n * - `photo` — Google's image models on Vertex AI (`vertex-image.ts`), for\n * every kind that is not SVG: a photo, a photographic genre, an art style\n * or a design asset, each the description followed by its kind's fixed\n * style wording (`ai-media-raster-prompt.ts`). Off unless the deployment\n * configured them; the door answers 404 with a sentence the dialog shows.\n * - `illustration` — an SVG drawn by the text provider every other AI door\n * uses (`ai-media-svg.ts`): an illustration, an icon, a tileable pattern or\n * a simple logo mark, in the site's theme colors or the person's own. No\n * new vendor, so it runs wherever text AI does.\n *\n * There is no GET: the button draws from the shell's own gates, and this door\n * decides when someone asks for a picture.\n *\n * ## A request, not a job\n *\n * Both modes answer inside the request, the way \"Save as a component with\n * AI\" does, and are metered on the same meter every door uses.\n *\n * ## The money, in order\n *\n * 1. `aiGateLadder` RESERVES before anything is spent: the `aiGenerative`\n * entitlement, `release_ai_generative`, the `ai-generate` kill switch,\n * `ai.generate` on the site, the per-account and per-address windows, the\n * Free taste's own rungs and the band.\n * 2. A workspace whose band is a wall (Free, or a hard cap) is refused before\n * the provider is called when the pictures' estimate is more than it has\n * left.\n * 3. The provider is called. A fault of ours or the provider's hands the\n * reservation back and charges nothing. A declined description charges\n * only what the decline spent, recorded as a declined request (which a\n * Free workspace is never charged for, and which its daily refusal pause\n * counts).\n * 4. Each picture is stored, and only what was stored is SETTLED, at billed\n * rates above the provider's: a photo per picture plus its prompt and\n * thinking, an illustration as the tokens that drew it. A picture held\n * back, one the library refused, and an illustration that failed its\n * safety check twice are never charged — that last one is ours.\n *\n * ## Stored the way any upload is stored\n *\n * Each picture is posted to the console's own `/api/media/upload` with the\n * caller's credential, so the scope, the uploads lockdown, the structural\n * inspection, SVG sanitizing, the quarantine list, the storage band, the CDN\n * variants and the storage counter all apply exactly as they do to a file\n * the person dropped in. This door writes no media document of its own; it\n * only adds, to the documents that route made, the alt text and the record\n * of how the picture was made.\n */\n\n/** The per-account window: a person makes a few sets a minute, not dozens. */\nconst RATE_LIMIT = { key: 'ai-media-image', limit: 6, windowMs: 60_000 }\n\n/** What a person reads when a picture failed on our side. */\nexport const AI_MEDIA_OURS_COPY =\n \"This one's on us — you weren't charged. The picture didn't come out right; try again, or describe it a little differently.\"\n\n/** What the door answers on a deployment that makes no photos. */\nexport const AI_MEDIA_NO_PHOTOS_COPY =\n \"Photos and painted styles aren't available here yet. Choose a vector kind, such as Illustration, to draw one instead.\"\n\n/** The provider photos come from; a spec stands one in through jest. */\nfunction imageProvider(): AiImageProvider {\n return vertexImageProvider\n}\n\n/** Every control character as a space, squeezed, trimmed and cut. */\nexport function cleanAiImagePrompt(value: unknown): string {\n let out = ''\n for (const char of String(value ?? '')) {\n const code = char.charCodeAt(0)\n out += code < 32 || code === 127 || code === 0x2028 || code === 0x2029 ? ' ' : char\n }\n return out.replace(/\\s+/g, ' ').trim().slice(0, AI_IMAGE_PROMPT_MAX_CHARS)\n}\n\n/**\n * The alt text a picture is stored with: the sentence given, as a sentence,\n * cut at a word inside the library's alt limit. A person edits it like any\n * other.\n */\nexport function aiImageAltText(text: string): string {\n const clean = text.replace(/\\s+/g, ' ').trim()\n if (!clean) return ''\n const sentence = clean.charAt(0).toUpperCase() + clean.slice(1)\n if (sentence.length <= MEDIA_ALT_MAX_LENGTH) return sentence\n const cut = sentence.slice(0, MEDIA_ALT_MAX_LENGTH - 1)\n const space = cut.lastIndexOf(' ')\n return `${(space > 40 ? cut.slice(0, space) : cut).trim()}…`\n}\n\nconst EXTENSIONS: Readonly<Record<string, string>> = {\n 'image/png': 'png',\n 'image/jpeg': 'jpg',\n 'image/webp': 'webp',\n 'image/svg+xml': 'svg',\n}\n\n/**\n * A file name from the description's first words: `ai-a-red-barn-2.png`. An\n * accent is taken off its letter rather than left to split the word, so\n * \"panadería\" is `panaderia`, not `panaderi-a` (AGL-3791).\n */\nexport function aiImageFileName(prompt: string, index: number, mimeType: string): string {\n const words = prompt\n .toLowerCase()\n .normalize('NFKD')\n .replace(/\\p{M}+/gu, '')\n .replace(/[^a-z0-9\\s-]/g, ' ')\n .split(/\\s+/)\n .filter(Boolean)\n .slice(0, 6)\n .join('-')\n .slice(0, 60)\n .replace(/-+$/, '')\n return `ai-${words || 'image'}-${index + 1}.${EXTENSIONS[mimeType] ?? 'png'}`\n}\n\n/** What the library asked for, read and bounded. */\nexport interface AiMediaImageInput {\n orgId: string\n library: 'host' | 'org'\n /** The site whose library this is, or the site on screen for the org's. */\n hostId: string | null\n folderId: string | null\n mode: AiImageMode\n prompt: string\n aspectRatio: string\n count: number\n /** The kind: an SVG style for an illustration, an image-provider style for a photo. */\n style: string\n /** An illustration's colors: the site theme's, or these. */\n palette: { source: 'theme' } | { source: 'custom'; colors: string[] }\n}\n\nexport function parseAiMediaImageInput(body: Record<string, unknown> | null): AiMediaImageInput {\n const text = (key: string, limit = 128) => {\n const value = body?.[key]\n return typeof value === 'string' ? value.trim().slice(0, limit) : ''\n }\n const count = Math.floor(Number(body?.['count'] ?? 1))\n const palette = body?.['palette'] as Record<string, unknown> | undefined\n const colors = Array.isArray(palette?.['colors']) ? (palette?.['colors'] as unknown[]) : []\n return {\n orgId: text('orgId'),\n library: body?.['library'] === 'org' ? 'org' : 'host',\n hostId: text('hostId') || null,\n folderId: text('folderId', 64) || null,\n mode: body?.['mode'] === 'illustration' ? 'illustration' : 'photo',\n prompt: cleanAiImagePrompt(body?.['prompt']),\n aspectRatio: text('aspectRatio', 8),\n count: Number.isFinite(count) ? count : 0,\n style: text('style', 24) || (body?.['mode'] === 'illustration' ? 'illustration' : 'photo'),\n palette:\n palette?.['source'] === 'custom'\n ? {\n source: 'custom',\n colors: colors\n .map((color) => String(color ?? '').trim().toLowerCase())\n .slice(0, AI_SVG_MAX_COLORS + 1),\n }\n : { source: 'theme' },\n }\n}\n\n/** One stored picture, or the upload route's refusal. */\ntype StoreOutcome = { mediaId: string } | { error: string; status: number }\n\n/**\n * Posts one picture to the console's own upload route, as the caller. The\n * same body the library's Upload button sends, so the route cannot tell the\n * two apart and applies every check it applies to that one.\n */\nasync function storeThroughUploadRoute(\n request: Request,\n input: AiMediaImageInput,\n image: { base64: string; mimeType: string },\n fileName: string,\n): Promise<StoreOutcome> {\n const scope =\n input.library === 'org'\n ? { orgId: input.orgId, ...(input.hostId ? { forHostId: input.hostId } : {}) }\n : { hostId: input.hostId }\n let response: Response\n try {\n response = await fetch(new URL('/api/media/upload', request.url), {\n method: 'POST',\n headers: {\n Authorization: request.headers.get('authorization') ?? '',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n ...scope,\n fileName,\n contentType: image.mimeType,\n folderId: input.folderId,\n data: image.base64,\n }),\n })\n } catch {\n return { error: 'The picture could not be saved to the media library.', status: 502 }\n }\n const payload = (await response.json().catch(() => ({}))) as Record<string, unknown>\n const mediaId = typeof payload['mediaId'] === 'string' ? payload['mediaId'] : ''\n if (response.ok && mediaId) return { mediaId }\n return {\n error:\n typeof payload['error'] === 'string'\n ? payload['error']\n : 'The picture could not be saved to the media library.',\n status: response.ok ? 502 : response.status,\n }\n}\n\n/** The record a generated picture keeps of how it was made. */\nexport interface AiMediaProvenance {\n model: string\n prompt: string\n mode: AiImageMode\n /** The kind the picture was made as. */\n style?: AiSvgStyle | AiRasterStyle\n aspectRatio: string\n /** The meter's signal id for the request that made it. */\n signalId: string | null\n generatedBy: string\n}\n\n/**\n * Adds the alt text and the provenance to a document the upload route made.\n * An `update`, which cannot mint a document: the route minted it.\n */\nasync function annotateStoredImage(\n gate: AiGateContext,\n input: AiMediaImageInput,\n mediaId: string,\n alt: string,\n provenance: AiMediaProvenance,\n): Promise<void> {\n const scopeRef =\n input.library === 'org'\n ? gate.firestore.collection('orgs').doc(input.orgId)\n : gate.firestore.collection('hosts').doc(String(input.hostId))\n const mediaRef = scopeRef.collection('media').doc(mediaId)\n const stored = ((await mediaRef.get()).data() ?? {}) as Parameters<typeof mediaFilterKeys>[0]\n await mediaRef.update({\n alt,\n description: 'Created with AI from a description.',\n aiGenerated: { ...provenance, createdAt: new Date() },\n // What the library filters by, restamped with the alt now set: the upload\n // route wrote \"Alt text is missing\" before the alt existed.\n ...mediaFilterKeys({ ...stored, alt }),\n })\n}\n\n/** The colors an illustration is drawn in. */\nasync function illustrationPalette(\n gate: AiGateContext,\n input: AiMediaImageInput,\n): Promise<string[]> {\n if (input.palette.source === 'custom') return input.palette.colors\n if (!input.hostId) return []\n const snapshot = await gate.firestore.collection('hosts').doc(input.hostId).get()\n return aiSvgThemePalette(aiInventoryTheme(snapshot.exists ? snapshot.data() ?? null : null)?.colors)\n}\n\nconst ZERO_USAGE: AiUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }\n\nconst sumUsage = (usages: readonly AiUsage[]): AiUsage =>\n usages.reduce<AiUsage>(\n (sum, usage) => ({\n inputTokens: sum.inputTokens + usage.inputTokens,\n outputTokens: sum.outputTokens + usage.outputTokens,\n cacheReadTokens: sum.cacheReadTokens + usage.cacheReadTokens,\n cacheWriteTokens: sum.cacheWriteTokens + usage.cacheWriteTokens,\n }),\n ZERO_USAGE,\n )\n\n/** A picture made, before it is stored. */\ninterface MadePicture {\n base64: string\n mimeType: string\n alt: string\n /** What making it spent, charged only once it is stored. */\n usage: AiUsage\n}\n\nexport async function POST(request: Request): Promise<Response> {\n let body: Record<string, unknown> | null\n try {\n body = (await request.json()) as Record<string, unknown>\n } catch {\n body = null\n }\n const input = parseAiMediaImageInput(body)\n const provider = imageProvider()\n const svgModel = input.mode === 'illustration' ? aiMediaSvgModel() : undefined\n // A deployment that makes no pictures of this kind has no such door, so\n // nothing about the caller or the workspace is read first.\n if (input.mode === 'photo' && !provider.configured()) {\n return Response.json({ error: AI_MEDIA_NO_PHOTOS_COPY, reason: 'unavailable' }, { status: 404 })\n }\n if (input.mode === 'illustration' && !svgModel) {\n return Response.json({ error: 'Not found' }, { status: 404 })\n }\n\n const gate = await aiGateLadder(\n { request, orgId: input.orgId, hostId: input.hostId },\n {\n feature: 'aiGenerative',\n releaseFlag: 'release_ai_generative',\n lockdownFeature: 'ai-generate',\n permission: 'ai.generate',\n rateLimit: RATE_LIMIT,\n },\n )\n if (gate instanceof Response) return gate\n // Nothing is spent until a provider answers, so every exit before it\n // hands the reservation back.\n const release = () =>\n releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(() => undefined)\n const refuse = async (error: string, status: number, extra: Record<string, unknown> = {}) => {\n await release()\n return Response.json({ error, ...extra }, { status })\n }\n\n if (input.library === 'host' && !input.hostId) {\n return refuse('Open the site whose media library the pictures go in.', 400)\n }\n if (!input.prompt) return refuse('Describe the picture you want first.', 400)\n if (!isAiImageAspectRatio(input.aspectRatio)) {\n return refuse(`Choose a shape: ${AI_IMAGE_ASPECT_RATIOS.join(', ')}.`, 400)\n }\n const aspectRatio: AiImageAspectRatio = input.aspectRatio\n if (input.count < 1 || input.count > AI_IMAGE_MAX_COUNT) {\n return refuse(`Make between 1 and ${AI_IMAGE_MAX_COUNT} pictures at a time.`, 400)\n }\n if (input.mode === 'photo' && !isAiRasterStyle(input.style)) {\n return refuse('Choose a kind of picture from the list.', 400)\n }\n if (input.mode === 'illustration') {\n if (!(AI_SVG_STYLES as readonly string[]).includes(input.style)) {\n return refuse('Choose an illustration, an icon, a pattern or a logo mark.', 400)\n }\n if (\n input.palette.source === 'custom' &&\n (!input.palette.colors.length ||\n input.palette.colors.length > AI_SVG_MAX_COLORS ||\n !input.palette.colors.every(isAiSvgColor))\n ) {\n return refuse(`Give between 1 and ${AI_SVG_MAX_COLORS} colors as hex values, such as #1a73e8.`, 400)\n }\n }\n\n const model = input.mode === 'photo' ? provider.defaultModel() : (svgModel as string)\n // The size is the plan's, never the request's: 512 px on Free, 1K on a\n // paid plan, each estimated and charged at its own per-picture rate.\n const imageSize = aiImageSizeForPlan(resolveEffectivePlan(gate.org) === 'free')\n const perPicture = aiMediaCreditsPerPicture(input.mode, model, imageSize)\n const estimate = perPicture * input.count\n const credits = publicAssistQuota(gate.reservation).credits\n if (credits && assistBandRefuses(gate.org) && credits.remaining < estimate) {\n return refuse(\n credits.remaining < perPicture\n ? 'This workspace has used its AI credits for the month.'\n : `That needs about ${estimate} credits and this workspace has ${credits.remaining} left this month. Make fewer pictures.`,\n 429,\n { reason: 'quota', quota: publicAssistQuota(gate.reservation) },\n )\n }\n\n /** Records what a decline spent: a declined request, never a picture. */\n const recordDecline = (usage: AiUsage, declinedModel: string) =>\n recordAssistCost(gate.firestore, gate.orgId, {\n route: '/api/ai/media/images',\n hostId: input.hostId,\n model: declinedModel,\n tier: 'entitled',\n usage: { ...usage, images: 0 },\n docsPaths: [],\n stopReason: 'refusal',\n free: gate.reservation.free ?? null,\n uid: gate.uid,\n kind: 'image',\n }).catch((meterError) =>\n console.error('ai media refusal not recorded', { orgId: gate.orgId, meterError }),\n )\n\n let made: MadePicture[]\n /** Pictures asked for and not made: held back, declined or failed on our side. */\n let unmade: number\n /** Tokens a photo request spent beside its pictures, charged once any is stored. */\n let photoUsage: AiUsage = ZERO_USAGE\n let declineReason: string | null = null\n let style: AiSvgStyle | AiRasterStyle | undefined\n\n if (input.mode === 'photo') {\n style = input.style as AiRasterStyle\n try {\n const result = await provider.generate({\n model,\n prompt: aiMediaRasterPrompt(input.prompt, style),\n aspectRatio,\n size: imageSize,\n count: input.count,\n })\n photoUsage = result.usage\n unmade = result.filtered\n made = result.images.map((image) => ({\n ...image,\n alt: aiImageAltText(input.prompt),\n usage: ZERO_USAGE,\n }))\n } catch (error) {\n if (error instanceof AiImageSafetyRefusal) {\n // Declined, not failed: no picture is charged, and the request is\n // recorded as a declined one so the Free taste's refusal pause\n // counts it.\n await recordDecline(ZERO_USAGE, model)\n return Response.json({ error: error.message, reason: 'safety' }, { status: 422 })\n }\n await release()\n console.error('ai photo generation failed', { orgId: gate.orgId, error })\n const status = error instanceof AiUpstreamError && error.accountProblem ? 503 : 502\n return Response.json({ error: AI_UPSTREAM_FAILURE_COPY }, { status })\n }\n } else {\n const svgStyle = input.style as AiSvgStyle\n style = svgStyle\n const palette = await illustrationPalette(gate, input)\n const settled = await Promise.allSettled(\n Array.from({ length: input.count }, (_unused, variant) =>\n generateAiMediaSvg({\n model,\n prompt: input.prompt,\n style: svgStyle,\n aspectRatio,\n palette,\n variant,\n count: input.count,\n }),\n ),\n )\n const outcomes = settled.flatMap((entry) =>\n entry.status === 'fulfilled' ? [entry.value] : [],\n )\n if (!outcomes.length) {\n await release()\n console.error('ai illustration generation failed', {\n orgId: gate.orgId,\n error: (settled[0] as PromiseRejectedResult | undefined)?.reason,\n })\n return Response.json({ error: AI_UPSTREAM_FAILURE_COPY }, { status: 502 })\n }\n const drawn = outcomes.filter(\n (outcome): outcome is Extract<AiMediaSvgOutcome, { kind: 'drawn' }> => outcome.kind === 'drawn',\n )\n const declined = outcomes.filter((outcome) => outcome.kind === 'declined')\n unmade = input.count - drawn.length\n declineReason =\n (declined[0] as Extract<AiMediaSvgOutcome, { kind: 'declined' }> | undefined)?.reason ?? null\n made = drawn.map((outcome) => ({\n base64: Buffer.from(outcome.svg, 'utf8').toString('base64'),\n mimeType: 'image/svg+xml',\n alt: aiImageAltText(outcome.alt || input.prompt),\n usage: outcome.usage,\n }))\n if (!made.length) {\n if (declined.length) {\n // The model declined: what it spent is a declined request.\n await recordDecline(sumUsage(declined.map((outcome) => outcome.usage)), model)\n return Response.json({ error: declineReason, reason: 'safety' }, { status: 422 })\n }\n // Every picture failed its check twice. Ours, so nothing is charged.\n await release()\n return Response.json({ error: AI_MEDIA_OURS_COPY, reason: 'ours' }, { status: 502 })\n }\n }\n\n const stored: Array<{ mediaId: string; picture: MadePicture }> = []\n let refusal: { error: string; status: number } | null = null\n for (const [index, picture] of made.entries()) {\n const outcome = await storeThroughUploadRoute(\n request,\n input,\n picture,\n aiImageFileName(input.prompt, index, picture.mimeType),\n )\n if ('mediaId' in outcome) stored.push({ mediaId: outcome.mediaId, picture })\n else refusal ??= outcome\n }\n\n if (!stored.length) {\n // Made and not kept: the library refused every picture (its storage\n // band, a lockdown, a permission), so nothing is charged.\n await release()\n return Response.json(\n { error: refusal?.error ?? 'The pictures could not be saved.' },\n { status: refusal?.status ?? 502 },\n )\n }\n\n // Only what was stored is settled: a photo's pictures at the per-picture\n // rate with the request's prompt and thinking, an illustration's tokens.\n const usage =\n input.mode === 'photo'\n ? { ...photoUsage, images: stored.length, imageSize }\n : sumUsage(stored.map((entry) => entry.picture.usage))\n let signalId: string | null = null\n try {\n signalId = await recordAssistCost(gate.firestore, gate.orgId, {\n route: '/api/ai/media/images',\n hostId: input.hostId,\n model,\n tier: 'entitled',\n usage,\n docsPaths: [],\n stopReason: 'end_turn',\n free: gate.reservation.free ?? null,\n uid: gate.uid,\n kind: 'image',\n })\n } catch (error) {\n // The pictures are in the library and were paid for by us; a meter\n // that could not be written is not a reason to hide them.\n console.error('ai media usage not recorded', { orgId: gate.orgId, error })\n }\n\n await Promise.all(\n stored.map(({ mediaId, picture }) =>\n annotateStoredImage(gate, input, mediaId, picture.alt, {\n model,\n prompt: input.prompt,\n mode: input.mode,\n ...(style ? { style } : {}),\n aspectRatio,\n signalId,\n generatedBy: gate.uid,\n }).catch((error) => console.error('ai media alt text not written', { mediaId, error })),\n ),\n )\n\n const warning =\n refusal?.error ??\n (declineReason && unmade > 0 ? declineReason : unmade > 0 && input.mode === 'illustration'\n ? AI_MEDIA_OURS_COPY\n : null)\n return Response.json({\n mediaIds: stored.map((entry) => entry.mediaId),\n filtered: unmade,\n failed: made.length - stored.length,\n // What was charged, as the meter priced it.\n credits: assistCreditsFromUsd(estimateAiBilledUsd(usage, model)),\n ...(warning ? { warning } : {}),\n })\n}\n"],"names":["mediaFilterKeys","resolveEffectivePlan","MEDIA_ALT_MAX_LENGTH","aiMediaCreditsPerPicture","aiSvgThemePalette","isAiSvgColor","estimateAiBilledUsd","AI_UPSTREAM_FAILURE_COPY","AiUpstreamError","AI_IMAGE_ASPECT_RATIOS","AI_IMAGE_MAX_COUNT","AI_IMAGE_PROMPT_MAX_CHARS","AI_SVG_MAX_COLORS","AI_SVG_STYLES","AiImageSafetyRefusal","aiImageSizeForPlan","isAiImageAspectRatio","isAiRasterStyle","vertexImageProvider","aiGateLadder","aiInventoryTheme","assistBandRefuses","assistCreditsFromUsd","publicAssistQuota","recordAssistCost","releaseAssistMessage","aiMediaRasterPrompt","aiMediaSvgModel","generateAiMediaSvg","RATE_LIMIT","key","limit","windowMs","AI_MEDIA_OURS_COPY","AI_MEDIA_NO_PHOTOS_COPY","imageProvider","cleanAiImagePrompt","value","out","char","String","code","charCodeAt","replace","trim","slice","aiImageAltText","text","clean","sentence","charAt","toUpperCase","length","cut","space","lastIndexOf","EXTENSIONS","aiImageFileName","prompt","index","mimeType","words","toLowerCase","normalize","split","filter","Boolean","join","parseAiMediaImageInput","body","count","Math","floor","Number","palette","colors","Array","isArray","orgId","library","hostId","folderId","mode","aspectRatio","isFinite","style","source","map","color","storeThroughUploadRoute","request","input","image","fileName","scope","forHostId","response","fetch","URL","url","method","headers","Authorization","get","JSON","stringify","contentType","data","base64","error","status","payload","json","catch","mediaId","ok","annotateStoredImage","gate","alt","provenance","scopeRef","firestore","collection","doc","mediaRef","stored","update","description","aiGenerated","createdAt","Date","illustrationPalette","snapshot","exists","ZERO_USAGE","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","sumUsage","usages","reduce","sum","usage","POST","provider","svgModel","undefined","configured","Response","reason","feature","releaseFlag","lockdownFeature","permission","rateLimit","release","reservation","refuse","extra","includes","every","model","defaultModel","imageSize","org","perPicture","estimate","credits","remaining","quota","recordDecline","declinedModel","route","tier","images","docsPaths","stopReason","free","uid","kind","meterError","console","made","unmade","photoUsage","declineReason","result","generate","size","filtered","message","accountProblem","declined","svgStyle","settled","Promise","allSettled","from","_unused","variant","outcomes","flatMap","entry","drawn","outcome","Buffer","svg","toString","refusal","picture","entries","push","signalId","all","generatedBy","warning","mediaIds","failed"],"mappings":"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"}
|
|
@@ -155,6 +155,10 @@ export function aiMediaSvgTool() {
|
|
|
155
155
|
],
|
|
156
156
|
tool: aiMediaSvgTool(),
|
|
157
157
|
maxTokens: AI_MEDIA_SVG_MAX_TOKENS,
|
|
158
|
+
// The ceiling holds the drawing alone, as it did on a fast tier that
|
|
159
|
+
// could not think; the fast tier's model now thinks unless told not to
|
|
160
|
+
// (AGL-3781).
|
|
161
|
+
thinking: 'off',
|
|
158
162
|
check: (answer)=>checkAiMediaSvgAnswer(answer, input.aspectRatio, input.style)
|
|
159
163
|
};
|
|
160
164
|
const result = await runValidatedGeneration(AI_MEDIA_SVG_KIND, generation);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-media-svg.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { aiDefaultModelFor } from '../providers/catalog'\nimport type { AiSystemBlock, AiTool, AiUsage } from '../providers/contract'\nimport type { AiImageAspectRatio, AiSvgStyle } from '../providers/image-contract'\nimport { resolveAiProvider } from '../providers/routing'\nimport { AI_SVG_MAX_BYTES, AI_SVG_VIEWBOX, checkAiSvgMarkup } from '../model/ai-svg'\nimport type { AiDoctrineViolation } from '../runtime/ai-doctrine-validators'\nimport {\n runValidatedGeneration,\n type AiCustomGenerationInput,\n type AiGenerationCheckResult,\n type AiValidatedGeneration,\n} from '../runtime/ai-doctrine'\n\n/**\n * Illustrations in Media (AGL-3602): an SVG drawn by the TEXT provider every\n * other AI door uses — the same registry, the same meter, the same data flow\n * the published subprocessor row for the text provider already describes —\n * so this mode needs no new vendor and runs wherever text AI does.\n *\n * One generation per picture, through `runValidatedGeneration`: the answer\n * arrives through a strict tool, `checkAiSvgMarkup` is its check, and an\n * answer with a problem is sent back once with what to fix. A second answer\n * with a problem is not stored and, being our failure, not charged.\n *\n * It runs on the provider's fast tier: an SVG is a few thousand tokens of\n * markup and the door answers inside one request, so the model that writes\n * them quickest is the one that fits.\n */\n\n/** The doctrine kind the generation runs under; it reads no site tree. */\nexport const AI_MEDIA_SVG_KIND = 'media-svg'\n\n/** The name of the tool the picture arrives through. */\nexport const AI_MEDIA_SVG_TOOL_NAME = 'submit_svg'\n\n/** The answer ceiling: room for a compact picture and a re-ask's worth of care. */\nexport const AI_MEDIA_SVG_MAX_TOKENS = 6_000\n\n/** What each kind of illustration is, in the words the model is given. */\nconst STYLE_BRIEF: Readonly<Record<AiSvgStyle, string>> = {\n illustration:\n 'A spot illustration or small scene: flat or lightly shaded vector shapes, clear silhouettes, no photographic detail.',\n icon: 'A single icon: bold, simple geometry centered with even padding, in ONE color, or two tones of the palette at most. Readable at 24 pixels.',\n pattern:\n 'A seamless, tileable background pattern: every shape that leaves one edge re-enters at the opposite edge, so tiles join without a seam. Low contrast, suitable behind text.',\n logo: \"A simple abstract logo MARK: a symbol made of a few shapes, with no lettering at all. Never a real company's logo, a trademark, a brand's symbol or anything imitating one.\",\n}\n\nconst INSTRUCTIONS = [\n 'You draw SVG pictures for a website owner. You receive what to draw, which kind of picture it is, its shape and the colors to use, and you answer through the submit_svg tool.',\n '',\n 'The SVG you write must:',\n '- be exactly one <svg> element with xmlns=\"http://www.w3.org/2000/svg\" and the viewBox you are given, and no width or height attributes;',\n '- use only shapes, paths, groups, gradients, clipPaths and masks drawn in the document itself: never <image>, <foreignObject>, <script>, <a>, animation elements, event attributes, or a href or url(...) pointing anywhere but #ids in the same SVG;',\n '- use no web fonts, @import or @font-face; prefer no text at all, and use only generic font families (sans-serif, serif) when text is essential;',\n `- stay compact: well under ${Math.round(AI_SVG_MAX_BYTES / 1024 / 4)} KB, with few paths and coordinates rounded to at most one decimal place;`,\n '- use the colors given, plus white, black and transparency, and no others.',\n '',\n 'Write alt text: one plain sentence describing what the picture shows, for someone who cannot see it.',\n '',\n \"Decline instead of drawing — set declined to one short sentence and leave svg and alt empty — when you are asked for a real company's or organization's logo, a trademark, a recognizable brand symbol or anything imitating one; for a real, named person; or for anything sexual, hateful, violent or otherwise unsafe. Otherwise set declined to an empty string.\",\n].join('\\n')\n\nexport const AI_MEDIA_SVG_INSTRUCTIONS: AiSystemBlock[] = [{ text: INSTRUCTIONS, cacheBreakpoint: true }]\n\nexport function aiMediaSvgTool(): AiTool {\n return {\n name: AI_MEDIA_SVG_TOOL_NAME,\n description: 'Submit the SVG picture and its alt text, or decline.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['svg', 'alt', 'declined'],\n properties: {\n svg: { type: 'string', description: 'The whole SVG document, starting with <svg and ending with </svg>.' },\n alt: { type: 'string', description: 'One plain sentence describing the picture.' },\n declined: {\n type: 'string',\n description: 'Empty, or one short sentence saying why this picture is not drawn.',\n },\n },\n },\n }\n}\n\n/** The question one picture is asked as. */\nexport function aiMediaSvgPrompt(input: {\n prompt: string\n style: AiSvgStyle\n aspectRatio: AiImageAspectRatio\n palette: readonly string[]\n variant: number\n count: number\n}): string {\n const colors = input.palette.length\n ? `Colors: ${input.palette.join(', ')}.`\n : 'Colors: a restrained palette of three to five colors of your choice.'\n const lines = [\n `Kind: ${input.style}. ${STYLE_BRIEF[input.style]}`,\n `Shape: ${input.aspectRatio}, viewBox=\"${AI_SVG_VIEWBOX[input.aspectRatio]}\".`,\n colors,\n ]\n if (input.count > 1) {\n lines.push(\n `This is version ${input.variant + 1} of ${input.count}: make it a distinct take on the same description.`,\n )\n }\n lines.push('', 'Draw:', input.prompt)\n return lines.join('\\n')\n}\n\n/** What one picture's answer holds once it is read. */\nexport type AiMediaSvgAnswer = { svg: string; alt: string } | { declined: string }\n\n/** The check one answer is held to: the SVG gate, or a decline. */\nexport function checkAiMediaSvgAnswer(\n answer: Record<string, unknown>,\n aspectRatio: AiImageAspectRatio,\n style: AiSvgStyle,\n): AiGenerationCheckResult<AiMediaSvgAnswer> {\n const declined = typeof answer['declined'] === 'string' ? answer['declined'].trim() : ''\n if (declined) return { value: { declined: declined.slice(0, 300) }, violations: [] }\n const result = checkAiSvgMarkup(answer['svg'], aspectRatio, style)\n const alt = typeof answer['alt'] === 'string' ? answer['alt'].replace(/\\s+/g, ' ').trim().slice(0, 300) : ''\n if (result.svg) return { value: { svg: result.svg, alt }, violations: [] }\n const violations: AiDoctrineViolation[] = result.problems.map((problem) => ({\n rule: null,\n code: `svg-${problem.code}`,\n message: 'The picture could not be drawn safely.',\n detail: problem.detail,\n }))\n return { value: { svg: '', alt }, violations, offending: { svg: '(see the problems listed)' } }\n}\n\n/** The model an illustration runs on: the text provider's fast tier. */\nexport function aiMediaSvgModel(): string | undefined {\n const provider = resolveAiProvider()\n return provider ? aiDefaultModelFor(provider.id, 'fast') : undefined\n}\n\n/** One picture's outcome. */\nexport type AiMediaSvgOutcome =\n | { kind: 'drawn'; svg: string; alt: string; usage: AiUsage; model: string }\n | { kind: 'declined'; reason: string; usage: AiUsage; model: string }\n /** The re-ask still failed the gate, or the model stopped: ours, never charged. */\n | { kind: 'failed'; usage: AiUsage; model: string }\n\n/** Draws one picture. A provider fault propagates for the door to answer. */\nexport async function generateAiMediaSvg(input: {\n model: string\n prompt: string\n style: AiSvgStyle\n aspectRatio: AiImageAspectRatio\n palette: readonly string[]\n variant: number\n count: number\n}): Promise<AiMediaSvgOutcome> {\n const generation: AiCustomGenerationInput<AiMediaSvgAnswer> = {\n model: input.model,\n instructions: AI_MEDIA_SVG_INSTRUCTIONS,\n inventory: undefined,\n messages: [{ role: 'user', content: aiMediaSvgPrompt(input) }],\n tool: aiMediaSvgTool(),\n maxTokens: AI_MEDIA_SVG_MAX_TOKENS,\n check: (answer) => checkAiMediaSvgAnswer(answer, input.aspectRatio, input.style),\n }\n const result = (await runValidatedGeneration(\n AI_MEDIA_SVG_KIND,\n generation,\n )) as AiValidatedGeneration<AiMediaSvgAnswer>\n const spent = { usage: result.usage, model: result.model }\n if (result.status === 'ok') {\n return 'declined' in result.value\n ? { kind: 'declined', reason: result.value.declined, ...spent }\n : { kind: 'drawn', svg: result.value.svg, alt: result.value.alt, ...spent }\n }\n if (result.status === 'refused') {\n return { kind: 'declined', reason: 'The assistant would not draw this.', ...spent }\n }\n return { kind: 'failed', ...spent }\n}\n"],"names":["aiDefaultModelFor","resolveAiProvider","AI_SVG_MAX_BYTES","AI_SVG_VIEWBOX","checkAiSvgMarkup","runValidatedGeneration","AI_MEDIA_SVG_KIND","AI_MEDIA_SVG_TOOL_NAME","AI_MEDIA_SVG_MAX_TOKENS","STYLE_BRIEF","illustration","icon","pattern","logo","INSTRUCTIONS","Math","round","join","AI_MEDIA_SVG_INSTRUCTIONS","text","cacheBreakpoint","aiMediaSvgTool","name","description","strict","inputSchema","type","additionalProperties","required","properties","svg","alt","declined","aiMediaSvgPrompt","input","colors","palette","length","lines","style","aspectRatio","count","push","variant","prompt","checkAiMediaSvgAnswer","answer","trim","value","slice","violations","result","replace","problems","map","problem","rule","code","message","detail","offending","aiMediaSvgModel","provider","id","undefined","generateAiMediaSvg","generation","model","instructions","inventory","messages","role","content","tool","maxTokens","check","spent","usage","status","kind","reason"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-media-svg.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { aiDefaultModelFor } from '../providers/catalog'\nimport type { AiSystemBlock, AiTool, AiUsage } from '../providers/contract'\nimport type { AiImageAspectRatio, AiSvgStyle } from '../providers/image-contract'\nimport { resolveAiProvider } from '../providers/routing'\nimport { AI_SVG_MAX_BYTES, AI_SVG_VIEWBOX, checkAiSvgMarkup } from '../model/ai-svg'\nimport type { AiDoctrineViolation } from '../runtime/ai-doctrine-validators'\nimport {\n runValidatedGeneration,\n type AiCustomGenerationInput,\n type AiGenerationCheckResult,\n type AiValidatedGeneration,\n} from '../runtime/ai-doctrine'\n\n/**\n * Illustrations in Media (AGL-3602): an SVG drawn by the TEXT provider every\n * other AI door uses — the same registry, the same meter, the same data flow\n * the published subprocessor row for the text provider already describes —\n * so this mode needs no new vendor and runs wherever text AI does.\n *\n * One generation per picture, through `runValidatedGeneration`: the answer\n * arrives through a strict tool, `checkAiSvgMarkup` is its check, and an\n * answer with a problem is sent back once with what to fix. A second answer\n * with a problem is not stored and, being our failure, not charged.\n *\n * It runs on the provider's fast tier: an SVG is a few thousand tokens of\n * markup and the door answers inside one request, so the model that writes\n * them quickest is the one that fits.\n */\n\n/** The doctrine kind the generation runs under; it reads no site tree. */\nexport const AI_MEDIA_SVG_KIND = 'media-svg'\n\n/** The name of the tool the picture arrives through. */\nexport const AI_MEDIA_SVG_TOOL_NAME = 'submit_svg'\n\n/** The answer ceiling: room for a compact picture and a re-ask's worth of care. */\nexport const AI_MEDIA_SVG_MAX_TOKENS = 6_000\n\n/** What each kind of illustration is, in the words the model is given. */\nconst STYLE_BRIEF: Readonly<Record<AiSvgStyle, string>> = {\n illustration:\n 'A spot illustration or small scene: flat or lightly shaded vector shapes, clear silhouettes, no photographic detail.',\n icon: 'A single icon: bold, simple geometry centered with even padding, in ONE color, or two tones of the palette at most. Readable at 24 pixels.',\n pattern:\n 'A seamless, tileable background pattern: every shape that leaves one edge re-enters at the opposite edge, so tiles join without a seam. Low contrast, suitable behind text.',\n logo: \"A simple abstract logo MARK: a symbol made of a few shapes, with no lettering at all. Never a real company's logo, a trademark, a brand's symbol or anything imitating one.\",\n}\n\nconst INSTRUCTIONS = [\n 'You draw SVG pictures for a website owner. You receive what to draw, which kind of picture it is, its shape and the colors to use, and you answer through the submit_svg tool.',\n '',\n 'The SVG you write must:',\n '- be exactly one <svg> element with xmlns=\"http://www.w3.org/2000/svg\" and the viewBox you are given, and no width or height attributes;',\n '- use only shapes, paths, groups, gradients, clipPaths and masks drawn in the document itself: never <image>, <foreignObject>, <script>, <a>, animation elements, event attributes, or a href or url(...) pointing anywhere but #ids in the same SVG;',\n '- use no web fonts, @import or @font-face; prefer no text at all, and use only generic font families (sans-serif, serif) when text is essential;',\n `- stay compact: well under ${Math.round(AI_SVG_MAX_BYTES / 1024 / 4)} KB, with few paths and coordinates rounded to at most one decimal place;`,\n '- use the colors given, plus white, black and transparency, and no others.',\n '',\n 'Write alt text: one plain sentence describing what the picture shows, for someone who cannot see it.',\n '',\n \"Decline instead of drawing — set declined to one short sentence and leave svg and alt empty — when you are asked for a real company's or organization's logo, a trademark, a recognizable brand symbol or anything imitating one; for a real, named person; or for anything sexual, hateful, violent or otherwise unsafe. Otherwise set declined to an empty string.\",\n].join('\\n')\n\nexport const AI_MEDIA_SVG_INSTRUCTIONS: AiSystemBlock[] = [{ text: INSTRUCTIONS, cacheBreakpoint: true }]\n\nexport function aiMediaSvgTool(): AiTool {\n return {\n name: AI_MEDIA_SVG_TOOL_NAME,\n description: 'Submit the SVG picture and its alt text, or decline.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['svg', 'alt', 'declined'],\n properties: {\n svg: { type: 'string', description: 'The whole SVG document, starting with <svg and ending with </svg>.' },\n alt: { type: 'string', description: 'One plain sentence describing the picture.' },\n declined: {\n type: 'string',\n description: 'Empty, or one short sentence saying why this picture is not drawn.',\n },\n },\n },\n }\n}\n\n/** The question one picture is asked as. */\nexport function aiMediaSvgPrompt(input: {\n prompt: string\n style: AiSvgStyle\n aspectRatio: AiImageAspectRatio\n palette: readonly string[]\n variant: number\n count: number\n}): string {\n const colors = input.palette.length\n ? `Colors: ${input.palette.join(', ')}.`\n : 'Colors: a restrained palette of three to five colors of your choice.'\n const lines = [\n `Kind: ${input.style}. ${STYLE_BRIEF[input.style]}`,\n `Shape: ${input.aspectRatio}, viewBox=\"${AI_SVG_VIEWBOX[input.aspectRatio]}\".`,\n colors,\n ]\n if (input.count > 1) {\n lines.push(\n `This is version ${input.variant + 1} of ${input.count}: make it a distinct take on the same description.`,\n )\n }\n lines.push('', 'Draw:', input.prompt)\n return lines.join('\\n')\n}\n\n/** What one picture's answer holds once it is read. */\nexport type AiMediaSvgAnswer = { svg: string; alt: string } | { declined: string }\n\n/** The check one answer is held to: the SVG gate, or a decline. */\nexport function checkAiMediaSvgAnswer(\n answer: Record<string, unknown>,\n aspectRatio: AiImageAspectRatio,\n style: AiSvgStyle,\n): AiGenerationCheckResult<AiMediaSvgAnswer> {\n const declined = typeof answer['declined'] === 'string' ? answer['declined'].trim() : ''\n if (declined) return { value: { declined: declined.slice(0, 300) }, violations: [] }\n const result = checkAiSvgMarkup(answer['svg'], aspectRatio, style)\n const alt = typeof answer['alt'] === 'string' ? answer['alt'].replace(/\\s+/g, ' ').trim().slice(0, 300) : ''\n if (result.svg) return { value: { svg: result.svg, alt }, violations: [] }\n const violations: AiDoctrineViolation[] = result.problems.map((problem) => ({\n rule: null,\n code: `svg-${problem.code}`,\n message: 'The picture could not be drawn safely.',\n detail: problem.detail,\n }))\n return { value: { svg: '', alt }, violations, offending: { svg: '(see the problems listed)' } }\n}\n\n/** The model an illustration runs on: the text provider's fast tier. */\nexport function aiMediaSvgModel(): string | undefined {\n const provider = resolveAiProvider()\n return provider ? aiDefaultModelFor(provider.id, 'fast') : undefined\n}\n\n/** One picture's outcome. */\nexport type AiMediaSvgOutcome =\n | { kind: 'drawn'; svg: string; alt: string; usage: AiUsage; model: string }\n | { kind: 'declined'; reason: string; usage: AiUsage; model: string }\n /** The re-ask still failed the gate, or the model stopped: ours, never charged. */\n | { kind: 'failed'; usage: AiUsage; model: string }\n\n/** Draws one picture. A provider fault propagates for the door to answer. */\nexport async function generateAiMediaSvg(input: {\n model: string\n prompt: string\n style: AiSvgStyle\n aspectRatio: AiImageAspectRatio\n palette: readonly string[]\n variant: number\n count: number\n}): Promise<AiMediaSvgOutcome> {\n const generation: AiCustomGenerationInput<AiMediaSvgAnswer> = {\n model: input.model,\n instructions: AI_MEDIA_SVG_INSTRUCTIONS,\n inventory: undefined,\n messages: [{ role: 'user', content: aiMediaSvgPrompt(input) }],\n tool: aiMediaSvgTool(),\n maxTokens: AI_MEDIA_SVG_MAX_TOKENS,\n // The ceiling holds the drawing alone, as it did on a fast tier that\n // could not think; the fast tier's model now thinks unless told not to\n // (AGL-3781).\n thinking: 'off',\n check: (answer) => checkAiMediaSvgAnswer(answer, input.aspectRatio, input.style),\n }\n const result = (await runValidatedGeneration(\n AI_MEDIA_SVG_KIND,\n generation,\n )) as AiValidatedGeneration<AiMediaSvgAnswer>\n const spent = { usage: result.usage, model: result.model }\n if (result.status === 'ok') {\n return 'declined' in result.value\n ? { kind: 'declined', reason: result.value.declined, ...spent }\n : { kind: 'drawn', svg: result.value.svg, alt: result.value.alt, ...spent }\n }\n if (result.status === 'refused') {\n return { kind: 'declined', reason: 'The assistant would not draw this.', ...spent }\n }\n return { kind: 'failed', ...spent }\n}\n"],"names":["aiDefaultModelFor","resolveAiProvider","AI_SVG_MAX_BYTES","AI_SVG_VIEWBOX","checkAiSvgMarkup","runValidatedGeneration","AI_MEDIA_SVG_KIND","AI_MEDIA_SVG_TOOL_NAME","AI_MEDIA_SVG_MAX_TOKENS","STYLE_BRIEF","illustration","icon","pattern","logo","INSTRUCTIONS","Math","round","join","AI_MEDIA_SVG_INSTRUCTIONS","text","cacheBreakpoint","aiMediaSvgTool","name","description","strict","inputSchema","type","additionalProperties","required","properties","svg","alt","declined","aiMediaSvgPrompt","input","colors","palette","length","lines","style","aspectRatio","count","push","variant","prompt","checkAiMediaSvgAnswer","answer","trim","value","slice","violations","result","replace","problems","map","problem","rule","code","message","detail","offending","aiMediaSvgModel","provider","id","undefined","generateAiMediaSvg","generation","model","instructions","inventory","messages","role","content","tool","maxTokens","thinking","check","spent","usage","status","kind","reason"],"mappings":"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"}
|
|
@@ -16,6 +16,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { buildRoute, Route } from "@aglyn/aglyn/app-utils/console-routes";
|
|
18
18
|
import { usageNotificationLevel } from "@aglyn/aglyn/app-utils/notifications";
|
|
19
|
+
import { platformConsoleOrigin } from "@aglyn/aglyn/app-utils/platform-brand";
|
|
19
20
|
import { sendEmail } from "@aglyn/shared-util-email";
|
|
20
21
|
import { withoutMailWithheldAccounts } from "@aglyn/tenant-data-admin/server/account-mail";
|
|
21
22
|
import { meterPlatformEmail } from "@aglyn/tenant-data-admin/server/email-metering";
|
|
@@ -30,10 +31,6 @@ const DEFAULT_CHANNELS = {
|
|
|
30
31
|
meter: meterPlatformEmail,
|
|
31
32
|
reachable: withoutMailWithheldAccounts
|
|
32
33
|
};
|
|
33
|
-
/** The console's absolute origin for an email link, or `''` when unset. */ function consoleOrigin() {
|
|
34
|
-
var _process_env_NEXT_PUBLIC_CONSOLE_URL;
|
|
35
|
-
return ((_process_env_NEXT_PUBLIC_CONSOLE_URL = process.env.NEXT_PUBLIC_CONSOLE_URL) != null ? _process_env_NEXT_PUBLIC_CONSOLE_URL : '').trim().replace(/\/+$/, '');
|
|
36
|
-
}
|
|
37
34
|
/** Take the marker for `threshold` this month; `false` when it was taken. */ export async function claimAiAllotmentAlert(firestore, orgId, subject, month, threshold) {
|
|
38
35
|
const ref = aiAllotmentRef(firestore.collection('orgs').doc(orgId), subject);
|
|
39
36
|
return firestore.runTransaction(async (tx)=>{
|
|
@@ -151,7 +148,7 @@ async function recipientsFor(firestore, orgId, standing, reachable) {
|
|
|
151
148
|
skipEmail: true
|
|
152
149
|
});
|
|
153
150
|
}
|
|
154
|
-
const origin =
|
|
151
|
+
const origin = platformConsoleOrigin();
|
|
155
152
|
const managerText = link && origin ? `${copy.body}\n\n${origin}${link}` : copy.body;
|
|
156
153
|
const managers = await filter(recipients.managerEmails);
|
|
157
154
|
if (managers.length) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/ai-allotment-alerts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport { usageNotificationLevel } from '@aglyn/aglyn/app-utils/notifications'\nimport { sendEmail } from '@aglyn/shared-util-email'\nimport { withoutMailWithheldAccounts } from '@aglyn/tenant-data-admin/server/account-mail'\nimport { meterPlatformEmail } from '@aglyn/tenant-data-admin/server/email-metering'\nimport { filterSuppressedEmails } from '@aglyn/tenant-data-admin/server/email-suppression'\nimport { notifyUsers } from '@aglyn/tenant-data-admin/server/notifications'\nimport {\n aiAllotmentAlertCopy,\n type AiAllotmentStanding,\n type AiAllotmentThreshold,\n} from '../model/ai-allotments'\nimport { aiAllotmentRef } from './ai-allotments'\n\n/**\n * A SOFT allotment's 75, 80, 90 and 100% steps (AGL-2942, AGL-3431),\n * through the alert pipeline's two channels: the console notification every\n * usage threshold writes (`billing.usage`, to `users/{uid}/notifications`)\n * and the email beside it, sent past the platform suppression list and\n * metered as platform mail — the same primitives the usage-alert sweep\n * composes.\n *\n * Told when the request that finds the subject at a threshold is admitted,\n * rather than on the next daily sweep: an allotment is a line a manager drew\n * for one person or one site, and a day's delay is most of what a soft line\n * is for. The sweep is untouched.\n *\n * ## Who is told\n *\n * The workspace's owners and admins — the audience of every usage alert —\n * and, for a member's or a collaborator's allotment, the person it is for.\n * The person gets their own words, in the second person and without the\n * Billing link or the \"make it hard\" step, neither of which they can use.\n * Both name the workspace, and a collaborator's the site, because an owner\n * of two workspaces cannot tell from a figure which one it is about.\n *\n * ## Once per threshold per month\n *\n * The marker is `alerted: { month, threshold }` on the allotment itself,\n * claimed in a transaction before anything is sent, so two requests at the\n * line cannot both announce it. A write to the allotment clears the marker,\n * so a raised allotment announces its own next crossing.\n */\n\n/** A crossing to announce, as the reservation measured it. */\nexport interface AiAllotmentAlertInput {\n orgId: string\n /** The workspace's slug, for the Billing link; `null` omits the link. */\n orgSlug: string | null\n month: string\n alerts: ReadonlyArray<{ standing: AiAllotmentStanding; threshold: AiAllotmentThreshold }>\n}\n\n/** The pipeline's primitives, replaceable in a spec. */\nexport interface AiAllotmentAlertChannels {\n notify: typeof notifyUsers\n send: typeof sendEmail\n filter: typeof filterSuppressedEmails\n meter: typeof meterPlatformEmail\n /** The uids given, minus locked or disabled accounts (AGL-3418). */\n reachable: typeof withoutMailWithheldAccounts\n}\n\nconst DEFAULT_CHANNELS: AiAllotmentAlertChannels = {\n notify: notifyUsers,\n send: sendEmail,\n filter: filterSuppressedEmails,\n meter: meterPlatformEmail,\n reachable: withoutMailWithheldAccounts,\n}\n\n/** The console's absolute origin for an email link, or `''` when unset. */\nfunction consoleOrigin(): string {\n return (process.env.NEXT_PUBLIC_CONSOLE_URL ?? '').trim().replace(/\\/+$/, '')\n}\n\n/** Take the marker for `threshold` this month; `false` when it was taken. */\nexport async function claimAiAllotmentAlert(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n subject: string,\n month: string,\n threshold: AiAllotmentThreshold,\n): Promise<boolean> {\n const ref = aiAllotmentRef(firestore.collection('orgs').doc(orgId), subject)\n return firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(ref)\n if (!snapshot.exists) return false\n const alerted = snapshot.get('alerted') as { month?: unknown; threshold?: unknown } | null\n if (alerted?.month === month && Number(alerted?.threshold) >= threshold) return false\n tx.set(ref, { alerted: { month, threshold } }, { merge: true })\n return true\n })\n}\n\ninterface Recipients {\n managerUids: string[]\n managerEmails: string[]\n subjectUid: string | null\n subjectEmail: string | null\n subjectName: string\n /** The workspace's name, for the copy. */\n workspaceName: string\n /** The site a collaborator's allotment is on, for the copy. */\n siteName: string\n}\n\n/** The name a site's managers know it by, then its address, then its id. */\nasync function siteName(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n): Promise<string> {\n const host = await firestore.collection('hosts').doc(hostId).get()\n return (\n String(host.get('displayName') ?? '').trim() ||\n String(host.get('subdomain') ?? '').trim() ||\n hostId\n )\n}\n\nasync function recipientsFor(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n standing: AiAllotmentStanding,\n reachable: AiAllotmentAlertChannels['reachable'],\n): Promise<Recipients> {\n const orgRef = firestore.collection('orgs').doc(orgId)\n const [org, members] = await Promise.all([orgRef.get(), orgRef.collection('members').get()])\n // A locked or disabled account is told nothing, by either channel; it is\n // still named when the alert is about it.\n const open = new Set(await reachable(members.docs.map((member) => member.id)))\n const managerUids: string[] = []\n const managerEmails: string[] = []\n let subjectEmail: string | null = null\n let subjectName = ''\n for (const member of members.docs) {\n const role = member.get('role')\n const email = String(member.get('email') ?? '').trim()\n if ((role === 'owner' || role === 'admin') && open.has(member.id)) {\n managerUids.push(member.id)\n if (email.includes('@')) managerEmails.push(email)\n }\n if (standing.uid && member.id === standing.uid) {\n subjectEmail = email.includes('@') && open.has(member.id) ? email : null\n subjectName = String(member.get('displayName') ?? '').trim() || email\n }\n }\n const site = standing.hostId ? await siteName(firestore, standing.hostId) : ''\n if (standing.scope === 'host') subjectName = site\n return {\n managerUids,\n managerEmails,\n subjectUid:\n standing.scope === 'host' || !standing.uid || !open.has(standing.uid)\n ? null\n : standing.uid,\n subjectEmail: standing.scope === 'host' ? null : subjectEmail,\n subjectName: subjectName || (standing.scope === 'host' ? 'A site' : 'A member'),\n workspaceName:\n String(org.get('name') ?? '').trim() ||\n String(org.get('slug') ?? '').trim() ||\n 'your workspace',\n siteName: site,\n }\n}\n\n/**\n * Announce every crossing the reservation found. Never throws, and answers\n * how many were announced — one failing crossing does not stop the next.\n */\nexport async function announceAiAllotmentAlerts(\n firestore: FirebaseFirestore.Firestore,\n input: AiAllotmentAlertInput,\n channels: Partial<AiAllotmentAlertChannels> = {},\n): Promise<number> {\n const { notify, send, filter, meter, reachable } = { ...DEFAULT_CHANNELS, ...channels }\n let announced = 0\n for (const { standing, threshold } of input.alerts) {\n try {\n const claimed = await claimAiAllotmentAlert(\n firestore,\n input.orgId,\n standing.subject,\n input.month,\n threshold,\n )\n if (!claimed) continue\n const recipients = await recipientsFor(firestore, input.orgId, standing, reachable)\n const words = {\n scope: standing.scope,\n threshold,\n used: standing.used,\n credits: standing.credits,\n name: recipients.subjectName,\n workspace: recipients.workspaceName,\n site: recipients.siteName,\n }\n const copy = aiAllotmentAlertCopy(words)\n const ownCopy = aiAllotmentAlertCopy({ ...words, reader: 'subject' })\n const link = input.orgSlug\n ? `${buildRoute(Route.MANAGE_BILLING_USAGE, { orgSlug: input.orgSlug })}#ai-allotments`\n : null\n // `skipEmail`: the email below is this crossing's email, so the\n // per-person channel does not send a second one to anybody who switched\n // billing email on (AGL-3431).\n await notify(\n recipients.managerUids,\n {\n type: 'billing.usage',\n title: copy.title,\n body: copy.body,\n level: usageNotificationLevel(threshold),\n orgId: input.orgId,\n ...(link ? { link } : {}),\n },\n { skipEmail: true },\n )\n if (recipients.subjectUid && !recipients.managerUids.includes(recipients.subjectUid)) {\n await notify(\n [recipients.subjectUid],\n {\n type: 'billing.usage',\n title: ownCopy.title,\n body: ownCopy.body,\n level: usageNotificationLevel(threshold),\n orgId: input.orgId,\n },\n { skipEmail: true },\n )\n }\n const origin = consoleOrigin()\n const managerText = link && origin ? `${copy.body}\\n\\n${origin}${link}` : copy.body\n const managers = await filter(recipients.managerEmails)\n if (managers.length) {\n const result = await send({\n to: managers,\n subject: copy.title,\n text: managerText,\n context: 'ai-allotment-alert',\n })\n if (result.sent) await meter().catch(() => undefined)\n }\n const subjectAddress = recipients.subjectEmail\n ? (await filter([recipients.subjectEmail])).filter(\n (address) => !managers.includes(address),\n )\n : []\n if (subjectAddress.length) {\n const result = await send({\n to: subjectAddress,\n subject: ownCopy.title,\n text: ownCopy.body,\n context: 'ai-allotment-alert',\n })\n if (result.sent) await meter().catch(() => undefined)\n }\n announced += 1\n } catch (error) {\n console.error('[ai-allotments] announcing a crossing failed', input.orgId, standing.subject, error)\n }\n }\n return announced\n}\n"],"names":["buildRoute","Route","usageNotificationLevel","sendEmail","withoutMailWithheldAccounts","meterPlatformEmail","filterSuppressedEmails","notifyUsers","aiAllotmentAlertCopy","aiAllotmentRef","DEFAULT_CHANNELS","notify","send","filter","meter","reachable","consoleOrigin","process","env","NEXT_PUBLIC_CONSOLE_URL","trim","replace","claimAiAllotmentAlert","firestore","orgId","subject","month","threshold","ref","collection","doc","runTransaction","tx","snapshot","get","exists","alerted","Number","set","merge","siteName","hostId","host","String","recipientsFor","standing","org","orgRef","members","Promise","all","open","Set","docs","map","member","id","managerUids","managerEmails","subjectEmail","subjectName","role","email","has","push","includes","uid","site","scope","subjectUid","workspaceName","announceAiAllotmentAlerts","input","channels","announced","alerts","claimed","recipients","words","used","credits","name","workspace","copy","ownCopy","reader","link","orgSlug","MANAGE_BILLING_USAGE","type","title","body","level","skipEmail","origin","managerText","managers","length","result","to","text","context","sent","catch","undefined","subjectAddress","address","error","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/usage/ai-allotment-alerts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport { usageNotificationLevel } from '@aglyn/aglyn/app-utils/notifications'\nimport { platformConsoleOrigin } from '@aglyn/aglyn/app-utils/platform-brand'\nimport { sendEmail } from '@aglyn/shared-util-email'\nimport { withoutMailWithheldAccounts } from '@aglyn/tenant-data-admin/server/account-mail'\nimport { meterPlatformEmail } from '@aglyn/tenant-data-admin/server/email-metering'\nimport { filterSuppressedEmails } from '@aglyn/tenant-data-admin/server/email-suppression'\nimport { notifyUsers } from '@aglyn/tenant-data-admin/server/notifications'\nimport {\n aiAllotmentAlertCopy,\n type AiAllotmentStanding,\n type AiAllotmentThreshold,\n} from '../model/ai-allotments'\nimport { aiAllotmentRef } from './ai-allotments'\n\n/**\n * A SOFT allotment's 75, 80, 90 and 100% steps (AGL-2942, AGL-3431),\n * through the alert pipeline's two channels: the console notification every\n * usage threshold writes (`billing.usage`, to `users/{uid}/notifications`)\n * and the email beside it, sent past the platform suppression list and\n * metered as platform mail — the same primitives the usage-alert sweep\n * composes.\n *\n * Told when the request that finds the subject at a threshold is admitted,\n * rather than on the next daily sweep: an allotment is a line a manager drew\n * for one person or one site, and a day's delay is most of what a soft line\n * is for. The sweep is untouched.\n *\n * ## Who is told\n *\n * The workspace's owners and admins — the audience of every usage alert —\n * and, for a member's or a collaborator's allotment, the person it is for.\n * The person gets their own words, in the second person and without the\n * Billing link or the \"make it hard\" step, neither of which they can use.\n * Both name the workspace, and a collaborator's the site, because an owner\n * of two workspaces cannot tell from a figure which one it is about.\n *\n * ## Once per threshold per month\n *\n * The marker is `alerted: { month, threshold }` on the allotment itself,\n * claimed in a transaction before anything is sent, so two requests at the\n * line cannot both announce it. A write to the allotment clears the marker,\n * so a raised allotment announces its own next crossing.\n */\n\n/** A crossing to announce, as the reservation measured it. */\nexport interface AiAllotmentAlertInput {\n orgId: string\n /** The workspace's slug, for the Billing link; `null` omits the link. */\n orgSlug: string | null\n month: string\n alerts: ReadonlyArray<{ standing: AiAllotmentStanding; threshold: AiAllotmentThreshold }>\n}\n\n/** The pipeline's primitives, replaceable in a spec. */\nexport interface AiAllotmentAlertChannels {\n notify: typeof notifyUsers\n send: typeof sendEmail\n filter: typeof filterSuppressedEmails\n meter: typeof meterPlatformEmail\n /** The uids given, minus locked or disabled accounts (AGL-3418). */\n reachable: typeof withoutMailWithheldAccounts\n}\n\nconst DEFAULT_CHANNELS: AiAllotmentAlertChannels = {\n notify: notifyUsers,\n send: sendEmail,\n filter: filterSuppressedEmails,\n meter: meterPlatformEmail,\n reachable: withoutMailWithheldAccounts,\n}\n\n\n/** Take the marker for `threshold` this month; `false` when it was taken. */\nexport async function claimAiAllotmentAlert(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n subject: string,\n month: string,\n threshold: AiAllotmentThreshold,\n): Promise<boolean> {\n const ref = aiAllotmentRef(firestore.collection('orgs').doc(orgId), subject)\n return firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(ref)\n if (!snapshot.exists) return false\n const alerted = snapshot.get('alerted') as { month?: unknown; threshold?: unknown } | null\n if (alerted?.month === month && Number(alerted?.threshold) >= threshold) return false\n tx.set(ref, { alerted: { month, threshold } }, { merge: true })\n return true\n })\n}\n\ninterface Recipients {\n managerUids: string[]\n managerEmails: string[]\n subjectUid: string | null\n subjectEmail: string | null\n subjectName: string\n /** The workspace's name, for the copy. */\n workspaceName: string\n /** The site a collaborator's allotment is on, for the copy. */\n siteName: string\n}\n\n/** The name a site's managers know it by, then its address, then its id. */\nasync function siteName(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n): Promise<string> {\n const host = await firestore.collection('hosts').doc(hostId).get()\n return (\n String(host.get('displayName') ?? '').trim() ||\n String(host.get('subdomain') ?? '').trim() ||\n hostId\n )\n}\n\nasync function recipientsFor(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n standing: AiAllotmentStanding,\n reachable: AiAllotmentAlertChannels['reachable'],\n): Promise<Recipients> {\n const orgRef = firestore.collection('orgs').doc(orgId)\n const [org, members] = await Promise.all([orgRef.get(), orgRef.collection('members').get()])\n // A locked or disabled account is told nothing, by either channel; it is\n // still named when the alert is about it.\n const open = new Set(await reachable(members.docs.map((member) => member.id)))\n const managerUids: string[] = []\n const managerEmails: string[] = []\n let subjectEmail: string | null = null\n let subjectName = ''\n for (const member of members.docs) {\n const role = member.get('role')\n const email = String(member.get('email') ?? '').trim()\n if ((role === 'owner' || role === 'admin') && open.has(member.id)) {\n managerUids.push(member.id)\n if (email.includes('@')) managerEmails.push(email)\n }\n if (standing.uid && member.id === standing.uid) {\n subjectEmail = email.includes('@') && open.has(member.id) ? email : null\n subjectName = String(member.get('displayName') ?? '').trim() || email\n }\n }\n const site = standing.hostId ? await siteName(firestore, standing.hostId) : ''\n if (standing.scope === 'host') subjectName = site\n return {\n managerUids,\n managerEmails,\n subjectUid:\n standing.scope === 'host' || !standing.uid || !open.has(standing.uid)\n ? null\n : standing.uid,\n subjectEmail: standing.scope === 'host' ? null : subjectEmail,\n subjectName: subjectName || (standing.scope === 'host' ? 'A site' : 'A member'),\n workspaceName:\n String(org.get('name') ?? '').trim() ||\n String(org.get('slug') ?? '').trim() ||\n 'your workspace',\n siteName: site,\n }\n}\n\n/**\n * Announce every crossing the reservation found. Never throws, and answers\n * how many were announced — one failing crossing does not stop the next.\n */\nexport async function announceAiAllotmentAlerts(\n firestore: FirebaseFirestore.Firestore,\n input: AiAllotmentAlertInput,\n channels: Partial<AiAllotmentAlertChannels> = {},\n): Promise<number> {\n const { notify, send, filter, meter, reachable } = { ...DEFAULT_CHANNELS, ...channels }\n let announced = 0\n for (const { standing, threshold } of input.alerts) {\n try {\n const claimed = await claimAiAllotmentAlert(\n firestore,\n input.orgId,\n standing.subject,\n input.month,\n threshold,\n )\n if (!claimed) continue\n const recipients = await recipientsFor(firestore, input.orgId, standing, reachable)\n const words = {\n scope: standing.scope,\n threshold,\n used: standing.used,\n credits: standing.credits,\n name: recipients.subjectName,\n workspace: recipients.workspaceName,\n site: recipients.siteName,\n }\n const copy = aiAllotmentAlertCopy(words)\n const ownCopy = aiAllotmentAlertCopy({ ...words, reader: 'subject' })\n const link = input.orgSlug\n ? `${buildRoute(Route.MANAGE_BILLING_USAGE, { orgSlug: input.orgSlug })}#ai-allotments`\n : null\n // `skipEmail`: the email below is this crossing's email, so the\n // per-person channel does not send a second one to anybody who switched\n // billing email on (AGL-3431).\n await notify(\n recipients.managerUids,\n {\n type: 'billing.usage',\n title: copy.title,\n body: copy.body,\n level: usageNotificationLevel(threshold),\n orgId: input.orgId,\n ...(link ? { link } : {}),\n },\n { skipEmail: true },\n )\n if (recipients.subjectUid && !recipients.managerUids.includes(recipients.subjectUid)) {\n await notify(\n [recipients.subjectUid],\n {\n type: 'billing.usage',\n title: ownCopy.title,\n body: ownCopy.body,\n level: usageNotificationLevel(threshold),\n orgId: input.orgId,\n },\n { skipEmail: true },\n )\n }\n const origin = platformConsoleOrigin()\n const managerText = link && origin ? `${copy.body}\\n\\n${origin}${link}` : copy.body\n const managers = await filter(recipients.managerEmails)\n if (managers.length) {\n const result = await send({\n to: managers,\n subject: copy.title,\n text: managerText,\n context: 'ai-allotment-alert',\n })\n if (result.sent) await meter().catch(() => undefined)\n }\n const subjectAddress = recipients.subjectEmail\n ? (await filter([recipients.subjectEmail])).filter(\n (address) => !managers.includes(address),\n )\n : []\n if (subjectAddress.length) {\n const result = await send({\n to: subjectAddress,\n subject: ownCopy.title,\n text: ownCopy.body,\n context: 'ai-allotment-alert',\n })\n if (result.sent) await meter().catch(() => undefined)\n }\n announced += 1\n } catch (error) {\n console.error('[ai-allotments] announcing a crossing failed', input.orgId, standing.subject, error)\n }\n }\n return announced\n}\n"],"names":["buildRoute","Route","usageNotificationLevel","platformConsoleOrigin","sendEmail","withoutMailWithheldAccounts","meterPlatformEmail","filterSuppressedEmails","notifyUsers","aiAllotmentAlertCopy","aiAllotmentRef","DEFAULT_CHANNELS","notify","send","filter","meter","reachable","claimAiAllotmentAlert","firestore","orgId","subject","month","threshold","ref","collection","doc","runTransaction","tx","snapshot","get","exists","alerted","Number","set","merge","siteName","hostId","host","String","trim","recipientsFor","standing","org","orgRef","members","Promise","all","open","Set","docs","map","member","id","managerUids","managerEmails","subjectEmail","subjectName","role","email","has","push","includes","uid","site","scope","subjectUid","workspaceName","announceAiAllotmentAlerts","input","channels","announced","alerts","claimed","recipients","words","used","credits","name","workspace","copy","ownCopy","reader","link","orgSlug","MANAGE_BILLING_USAGE","type","title","body","level","skipEmail","origin","managerText","managers","length","result","to","text","context","sent","catch","undefined","subjectAddress","address","error","console"],"mappings":"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"}
|