@aglyn/plugins-ai 1.0.0-beta.285 → 1.0.0-beta.288

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/package.json +17 -17
  2. package/src/lib/assets/site-styles/nonprofit.webp +0 -0
  3. package/src/lib/assets/site-styles/professional.webp +0 -0
  4. package/src/lib/assets/site-styles/store.webp +0 -0
  5. package/src/lib/assets/site-styles/trades.webp +0 -0
  6. package/src/lib/components/ai-theme-proposal-card.component.js +12 -3
  7. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  8. package/src/lib/declarations.js +5 -2
  9. package/src/lib/declarations.js.map +1 -1
  10. package/src/lib/declarations.server.js +12 -0
  11. package/src/lib/declarations.server.js.map +1 -1
  12. package/src/lib/jobs/ai-build-capabilities.d.ts +3 -1
  13. package/src/lib/jobs/ai-build-capabilities.js +27 -2
  14. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  15. package/src/lib/jobs/ai-business-profile-prefill.js +6 -3
  16. package/src/lib/jobs/ai-business-profile-prefill.js.map +1 -1
  17. package/src/lib/jobs/ai-image-reads.d.ts +59 -0
  18. package/src/lib/jobs/ai-image-reads.js +138 -0
  19. package/src/lib/jobs/ai-image-reads.js.map +1 -0
  20. package/src/lib/jobs/ai-job-build-step.d.ts +29 -0
  21. package/src/lib/jobs/ai-job-build-step.js +83 -12
  22. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  23. package/src/lib/jobs/ai-job-duplicate-step.d.ts +2 -1
  24. package/src/lib/jobs/ai-job-duplicate-step.js +6 -6
  25. package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -1
  26. package/src/lib/jobs/ai-job-import-step.d.ts +4 -2
  27. package/src/lib/jobs/ai-job-import-step.js +15 -11
  28. package/src/lib/jobs/ai-job-import-step.js.map +1 -1
  29. package/src/lib/jobs/ai-job-insight-step.d.ts +10 -0
  30. package/src/lib/jobs/ai-job-insight-step.js +23 -5
  31. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  32. package/src/lib/jobs/ai-job-layout-language.js +2 -1
  33. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  34. package/src/lib/jobs/ai-job-media-step.d.ts +2 -1
  35. package/src/lib/jobs/ai-job-media-step.js +6 -8
  36. package/src/lib/jobs/ai-job-media-step.js.map +1 -1
  37. package/src/lib/jobs/ai-job-navigation-step.d.ts +2 -1
  38. package/src/lib/jobs/ai-job-navigation-step.js +7 -9
  39. package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
  40. package/src/lib/jobs/ai-job-page-language.js +7 -1
  41. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  42. package/src/lib/jobs/ai-job-page-sections.js +16 -3
  43. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  44. package/src/lib/jobs/ai-job-page-step.js +9 -1
  45. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  46. package/src/lib/jobs/ai-job-plan-step.d.ts +30 -8
  47. package/src/lib/jobs/ai-job-plan-step.js +78 -13
  48. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  49. package/src/lib/jobs/ai-job-privacy-step.d.ts +4 -2
  50. package/src/lib/jobs/ai-job-privacy-step.js +14 -15
  51. package/src/lib/jobs/ai-job-privacy-step.js.map +1 -1
  52. package/src/lib/jobs/ai-job-products-step.js +5 -3
  53. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-settings-step.d.ts +2 -1
  55. package/src/lib/jobs/ai-job-settings-step.js +7 -8
  56. package/src/lib/jobs/ai-job-settings-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-site-content.d.ts +34 -1
  58. package/src/lib/jobs/ai-job-site-content.js +67 -8
  59. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  60. package/src/lib/jobs/ai-job-site-datasets.d.ts +53 -3
  61. package/src/lib/jobs/ai-job-site-datasets.js +300 -30
  62. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  63. package/src/lib/jobs/ai-job-site-step.js +6 -3
  64. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  65. package/src/lib/jobs/ai-job-site-store-pages.js +3 -3
  66. package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -1
  67. package/src/lib/jobs/ai-job-text-step.js +2 -1
  68. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  69. package/src/lib/jobs/ai-job-tracking-step.d.ts +2 -1
  70. package/src/lib/jobs/ai-job-tracking-step.js +7 -8
  71. package/src/lib/jobs/ai-job-tracking-step.js.map +1 -1
  72. package/src/lib/jobs/ai-jobs-beat.d.ts +11 -7
  73. package/src/lib/jobs/ai-jobs-beat.js +18 -8
  74. package/src/lib/jobs/ai-jobs-beat.js.map +1 -1
  75. package/src/lib/jobs/ai-jobs-notify.d.ts +66 -1
  76. package/src/lib/jobs/ai-jobs-notify.js +136 -3
  77. package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
  78. package/src/lib/jobs/ai-layout-stock-photos.d.ts +155 -7
  79. package/src/lib/jobs/ai-layout-stock-photos.js +1517 -220
  80. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  81. package/src/lib/jobs/ai-product-image.d.ts +14 -0
  82. package/src/lib/jobs/ai-product-image.js +73 -21
  83. package/src/lib/jobs/ai-product-image.js.map +1 -1
  84. package/src/lib/jobs/ai-theme-brand-inputs.d.ts +9 -1
  85. package/src/lib/jobs/ai-theme-brand-inputs.js +28 -2
  86. package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
  87. package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.d.ts +175 -0
  88. package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js +601 -0
  89. package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js.map +1 -0
  90. package/src/lib/jobs/fixtures/ai-site-agl4065-pages.d.ts +46 -0
  91. package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js +2862 -0
  92. package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js.map +1 -0
  93. package/src/lib/jobs/fixtures/ai-site-agl4065-plans.d.ts +74 -0
  94. package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js +888 -0
  95. package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js.map +1 -0
  96. package/src/lib/jobs/fixtures/ai-site-agl4092-datasets.d.ts +120 -0
  97. package/src/lib/jobs/fixtures/ai-site-agl4092-datasets.js +410 -0
  98. package/src/lib/jobs/fixtures/ai-site-agl4092-datasets.js.map +1 -0
  99. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.d.ts +29 -0
  100. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js +81 -0
  101. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js.map +1 -0
  102. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.d.ts +87 -0
  103. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js +225 -0
  104. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js.map +1 -0
  105. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-recordings.d.ts +26 -0
  106. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-recordings.js +714 -0
  107. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-recordings.js.map +1 -0
  108. package/src/lib/layout-language/ai-layout-compiler.d.ts +11 -0
  109. package/src/lib/layout-language/ai-layout-compiler.js +194 -18
  110. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  111. package/src/lib/layout-language/ai-layout-copy.d.ts +37 -0
  112. package/src/lib/layout-language/ai-layout-copy.js +383 -3
  113. package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
  114. package/src/lib/layout-language/ai-layout-design.d.ts +19 -0
  115. package/src/lib/layout-language/ai-layout-design.js +31 -11
  116. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  117. package/src/lib/layout-language/ai-layout-listings.d.ts +7 -1
  118. package/src/lib/layout-language/ai-layout-listings.js +9 -1
  119. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  120. package/src/lib/layout-language/ai-layout-pictures.d.ts +39 -1
  121. package/src/lib/layout-language/ai-layout-pictures.js +141 -4
  122. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  123. package/src/lib/layout-language/ai-layout-storefront.d.ts +18 -4
  124. package/src/lib/layout-language/ai-layout-storefront.js +100 -20
  125. package/src/lib/layout-language/ai-layout-storefront.js.map +1 -1
  126. package/src/lib/model/ai-build-job.d.ts +7 -0
  127. package/src/lib/model/ai-build-job.js +36 -7
  128. package/src/lib/model/ai-build-job.js.map +1 -1
  129. package/src/lib/model/ai-build-plan.d.ts +28 -4
  130. package/src/lib/model/ai-build-plan.js +49 -9
  131. package/src/lib/model/ai-build-plan.js.map +1 -1
  132. package/src/lib/model/ai-build-progress.js +3 -1
  133. package/src/lib/model/ai-build-progress.js.map +1 -1
  134. package/src/lib/model/ai-job-notice.d.ts +8 -0
  135. package/src/lib/model/ai-job-notice.js +33 -0
  136. package/src/lib/model/ai-job-notice.js.map +1 -1
  137. package/src/lib/model/ai-job-plan-lines.d.ts +2 -1
  138. package/src/lib/model/ai-job-plan-lines.js +6 -1
  139. package/src/lib/model/ai-job-plan-lines.js.map +1 -1
  140. package/src/lib/model/ai-jobs.types.d.ts +8 -0
  141. package/src/lib/model/ai-jobs.types.js.map +1 -1
  142. package/src/lib/model/ai-permissions.d.ts +13 -0
  143. package/src/lib/model/ai-permissions.js +29 -1
  144. package/src/lib/model/ai-permissions.js.map +1 -1
  145. package/src/lib/model/ai-plan-said-sections.d.ts +12 -3
  146. package/src/lib/model/ai-plan-said-sections.js +22 -5
  147. package/src/lib/model/ai-plan-said-sections.js.map +1 -1
  148. package/src/lib/model/ai-products.d.ts +18 -0
  149. package/src/lib/model/ai-products.js +32 -0
  150. package/src/lib/model/ai-products.js.map +1 -1
  151. package/src/lib/model/ai-site-context.d.ts +6 -2
  152. package/src/lib/model/ai-site-context.js +8 -2
  153. package/src/lib/model/ai-site-context.js.map +1 -1
  154. package/src/lib/model/ai-site-inspiration.d.ts +3 -2
  155. package/src/lib/model/ai-site-inspiration.js +3 -2
  156. package/src/lib/model/ai-site-inspiration.js.map +1 -1
  157. package/src/lib/plugin.js +4 -13
  158. package/src/lib/plugin.js.map +1 -1
  159. package/src/lib/providers/anthropic.js +9 -2
  160. package/src/lib/providers/anthropic.js.map +1 -1
  161. package/src/lib/providers/catalog.js +14 -5
  162. package/src/lib/providers/catalog.js.map +1 -1
  163. package/src/lib/providers/contract.d.ts +24 -1
  164. package/src/lib/providers/contract.js.map +1 -1
  165. package/src/lib/providers/routing.js +5 -2
  166. package/src/lib/providers/routing.js.map +1 -1
  167. package/src/lib/runtime/ai-dataset-generation.d.ts +120 -1
  168. package/src/lib/runtime/ai-dataset-generation.js +352 -3
  169. package/src/lib/runtime/ai-dataset-generation.js.map +1 -1
  170. package/src/lib/runtime/ai-dev-replay.d.ts +1 -0
  171. package/src/lib/runtime/ai-dev-replay.js +4 -2
  172. package/src/lib/runtime/ai-dev-replay.js.map +1 -1
  173. package/src/lib/runtime/ai-doctrine-validators.d.ts +47 -3
  174. package/src/lib/runtime/ai-doctrine-validators.js +345 -28
  175. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  176. package/src/lib/runtime/ai-doctrine.d.ts +5 -4
  177. package/src/lib/runtime/ai-doctrine.js +10 -3
  178. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  179. package/src/lib/runtime/ai-icon-library.d.ts +27 -1
  180. package/src/lib/runtime/ai-icon-library.js +30 -3
  181. package/src/lib/runtime/ai-icon-library.js.map +1 -1
  182. package/src/lib/runtime/ai-products-generation.d.ts +11 -1
  183. package/src/lib/runtime/ai-products-generation.js +51 -4
  184. package/src/lib/runtime/ai-products-generation.js.map +1 -1
  185. package/src/lib/runtime/assist-answer-cache.d.ts +2 -0
  186. package/src/lib/runtime/assist-answer-cache.js +5 -2
  187. package/src/lib/runtime/assist-answer-cache.js.map +1 -1
  188. package/src/lib/server/ai-allotments.js +7 -2
  189. package/src/lib/server/ai-allotments.js.map +1 -1
  190. package/src/lib/server/ai-experiment-versions.js +5 -5
  191. package/src/lib/server/ai-experiment-versions.js.map +1 -1
  192. package/src/lib/server/ai-host-permissions-roster.d.ts +25 -0
  193. package/src/lib/server/ai-host-permissions-roster.js +33 -0
  194. package/src/lib/server/ai-host-permissions-roster.js.map +1 -0
  195. package/src/lib/server/ai-host-permissions.js +8 -6
  196. package/src/lib/server/ai-host-permissions.js.map +1 -1
  197. package/src/lib/server/ai-image-reads-route.d.ts +17 -0
  198. package/src/lib/server/ai-image-reads-route.js +106 -0
  199. package/src/lib/server/ai-image-reads-route.js.map +1 -0
  200. package/src/lib/server/ai-seo-apply.js +5 -5
  201. package/src/lib/server/ai-seo-apply.js.map +1 -1
  202. package/src/lib/server/ai-site-import-route.js +7 -4
  203. package/src/lib/server/ai-site-import-route.js.map +1 -1
  204. package/src/lib/server/assist-build.d.ts +15 -1
  205. package/src/lib/server/assist-build.js +30 -3
  206. package/src/lib/server/assist-build.js.map +1 -1
  207. package/src/lib/server/assist-chat.d.ts +11 -7
  208. package/src/lib/server/assist-chat.js +185 -48
  209. package/src/lib/server/assist-chat.js.map +1 -1
  210. package/src/lib/server/assist-docs-query.d.ts +51 -0
  211. package/src/lib/server/assist-docs-query.js +57 -0
  212. package/src/lib/server/assist-docs-query.js.map +1 -0
  213. package/src/lib/server/assist-jobs-context.d.ts +102 -0
  214. package/src/lib/server/assist-jobs-context.js +227 -0
  215. package/src/lib/server/assist-jobs-context.js.map +1 -0
  216. package/src/lib/server/assist-view-context.d.ts +8 -4
  217. package/src/lib/server/assist-view-context.js +12 -7
  218. package/src/lib/server/assist-view-context.js.map +1 -1
  219. package/src/lib/server.js +12 -0
  220. package/src/lib/server.js.map +1 -1
  221. package/src/lib/tools/ai-dataset-tool.d.ts +11 -1
  222. package/src/lib/tools/ai-dataset-tool.js +24 -7
  223. package/src/lib/tools/ai-dataset-tool.js.map +1 -1
  224. package/src/lib/tools/ai-duplicate-resource-tool.js +5 -6
  225. package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
  226. package/src/lib/components/ai-collaborator-permissions-column.component.d.ts +0 -32
  227. package/src/lib/components/ai-collaborator-permissions-column.component.js +0 -132
  228. package/src/lib/components/ai-collaborator-permissions-column.component.js.map +0 -1
@@ -133,7 +133,8 @@ export const AI_MODEL_CATALOG = [
133
133
  // `aiDefaultModelFor` answers, so Free, "Auto" and every step routed to
134
134
  // balanced run here. Rejects `thinking: {type: "disabled"}` and forced
135
135
  // tool choice: the adapter sends thinking off as `between_tools` and
136
- // every tool call with `tool_choice: auto`.
136
+ // every tool call with `tool_choice: auto`, a request's `toolChoice`
137
+ // included (no `forcedToolChoice`, AGL-4076).
137
138
  id: 'claude-sonnet-5-5',
138
139
  provider: ANTHROPIC,
139
140
  label: 'Claude Sonnet 5.5',
@@ -169,7 +170,9 @@ export const AI_MODEL_CATALOG = [
169
170
  id: 'claude-sonnet-5',
170
171
  provider: ANTHROPIC,
171
172
  label: 'Claude Sonnet 5',
172
- capabilities: anthropicCapabilities,
173
+ capabilities: _extends({}, anthropicCapabilities, {
174
+ forcedToolChoice: true
175
+ }),
173
176
  providerRates: aiRatesPerMTok(2, 10),
174
177
  billedRates: aiRatesPerMTok(3, 15),
175
178
  tier: 'balanced',
@@ -180,7 +183,9 @@ export const AI_MODEL_CATALOG = [
180
183
  id: 'claude-sonnet-4-6',
181
184
  provider: ANTHROPIC,
182
185
  label: 'Claude Sonnet 4.6',
183
- capabilities: anthropicCapabilities
186
+ capabilities: _extends({}, anthropicCapabilities, {
187
+ forcedToolChoice: true
188
+ })
184
189
  }, aiRatesAtList(3, 15), {
185
190
  tier: 'balanced',
186
191
  cacheMinTokens: 1024
@@ -228,7 +233,9 @@ export const AI_MODEL_CATALOG = [
228
233
  id: 'claude-opus-5',
229
234
  provider: ANTHROPIC,
230
235
  label: 'Claude Opus 5',
231
- capabilities: anthropicCapabilities
236
+ capabilities: _extends({}, anthropicCapabilities, {
237
+ forcedToolChoice: true
238
+ })
232
239
  }, aiRatesAtList(5, 25), {
233
240
  tier: 'deep',
234
241
  cacheMinTokens: 512
@@ -251,7 +258,9 @@ export const AI_MODEL_CATALOG = [
251
258
  id: 'claude-opus-4-8',
252
259
  provider: ANTHROPIC,
253
260
  label: 'Claude Opus 4.8',
254
- capabilities: anthropicCapabilities
261
+ capabilities: _extends({}, anthropicCapabilities, {
262
+ forcedToolChoice: true
263
+ })
255
264
  }, aiRatesAtList(5, 25), {
256
265
  tier: 'deep',
257
266
  cacheMinTokens: 1024
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/providers/catalog.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 type { PluginSubprocessorDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-subprocessors'\nimport type { AiModelDescriptor, AiUsage } from './contract'\nimport type { AiImageSize } from './image-contract'\n\n/**\n * The model catalog (AGL-2939): every model id the plugin will route to,\n * with its provider, its display name, its capabilities and its list rates.\n * The ONE place a model id is written outside a provider adapter — the\n * routing table maps a step kind to an id through here, the meter prices\n * usage by id through here, and a settings form offers ids from here.\n *\n * ## Two rates, because one figure was answering two questions\n *\n * Every row carries a PROVIDER rate and a BILLED rate (AGL-3015):\n *\n * - `providerRates` is what the vendor charges us. It is money leaving the\n * company, so cost accounting, the margin surfaces and every staff spend\n * meter price usage through this one and no other.\n * - `billedRates` is what a customer's credits are charged at. The credit\n * meter, the band, the overage ladder and the model selector price usage\n * through this one, because it is the figure a customer's balance moves by.\n *\n * They are equal on nearly every row, and a helper writes the pair from one\n * figure so an unmarked row cannot drift apart by a typo. A row may be\n * billed ABOVE its provider rate deliberately — the markup is carried in the\n * rate rather than in the credit conversion, which keeps a credit a fixed\n * quantity of billed spend on every model and leaves the band, the ladder\n * and the invoice on the one arithmetic they were sized with. A row billed\n * BELOW its provider rate would sell tokens at a loss, and\n * `billed-rate-is-not-provider-cost.spec.ts` refuses one.\n *\n * Both are keyed by model rather than fixed at one model's rates because the\n * default is an env override: a one-line incident swap to a dearer model\n * would otherwise keep reporting the cheaper money, and per-org cost would\n * read as roughly right — the failure mode this whole meter exists to\n * prevent. An unknown id falls back to the most EXPENSIVE known tier on\n * purpose, on both rates: an estimate that errs low is worse than one that\n * errs high, whether the figure is our bill or the customer's.\n */\n\nexport interface AiTokenRates {\n inputPerToken: number\n outputPerToken: number\n cacheReadPerToken: number\n cacheWritePerToken: number\n}\n\n/** List price per MTok → per-token rates, cache read at 0.1x, write 1.25x. */\nexport function aiRatesPerMTok(inputPerMTok: number, outputPerMTok: number): AiTokenRates {\n return {\n inputPerToken: inputPerMTok / 1_000_000,\n outputPerToken: outputPerMTok / 1_000_000,\n cacheReadPerToken: (inputPerMTok * 0.1) / 1_000_000,\n cacheWritePerToken: (inputPerMTok * 1.25) / 1_000_000,\n }\n}\n\n/**\n * The two rate tables a catalog row carries. Separate fields rather than one\n * table and a multiplier: a reader that wants our cost has to name it, and\n * a reader that wants the customer's has to name that, so neither can be\n * reached by accident.\n */\nexport interface AiCatalogRates {\n /** What the provider bills us for this model. */\n providerRates: AiTokenRates\n /** What a customer's credits are charged at for this model. */\n billedRates: AiTokenRates\n}\n\n/**\n * A row billed at exactly the provider's list rate — the usual case, written\n * once so the pair cannot drift.\n */\nexport function aiRatesAtList(\n inputPerMTok: number,\n outputPerMTok: number,\n): AiCatalogRates {\n return {\n providerRates: aiRatesPerMTok(inputPerMTok, outputPerMTok),\n billedRates: aiRatesPerMTok(inputPerMTok, outputPerMTok),\n }\n}\n\n/**\n * THE PLATFORM'S MARKUP over provider cost (AGL-3781): what a customer's\n * credits are charged for a model over what the model costs us, on every\n * figure. The balanced tier's list (Sonnet, $3 / $15 billed over $2 / $10),\n * a picture and the fast tier are billed at it, so a credit carries the same\n * margin whichever of them it is spent on.\n */\nexport const AI_BILLED_MARKUP = 1.5\n\n/** `rates` at the platform's markup: every figure times {@link AI_BILLED_MARKUP}. */\nexport function aiRatesMarkedUp(rates: AiTokenRates): AiTokenRates {\n return {\n inputPerToken: rates.inputPerToken * AI_BILLED_MARKUP,\n outputPerToken: rates.outputPerToken * AI_BILLED_MARKUP,\n cacheReadPerToken: rates.cacheReadPerToken * AI_BILLED_MARKUP,\n cacheWritePerToken: rates.cacheWritePerToken * AI_BILLED_MARKUP,\n }\n}\n\n/** A row billed at the platform's markup over the provider's list price per MTok. */\nexport function aiRatesAtMarkup(inputPerMTok: number, outputPerMTok: number): AiCatalogRates {\n const providerRates = aiRatesPerMTok(inputPerMTok, outputPerMTok)\n return { providerRates, billedRates: aiRatesMarkedUp(providerRates) }\n}\n\n/** {@link aiRatesAtList} for a model whose cache read is not a tenth of its input price. */\nexport function aiRatesAtListWithCacheRead(\n inputPerMTok: number,\n outputPerMTok: number,\n cacheReadPerMTok: number,\n): AiCatalogRates {\n const rates = { ...aiRatesPerMTok(inputPerMTok, outputPerMTok), cacheReadPerToken: cacheReadPerMTok / 1_000_000 }\n return { providerRates: rates, billedRates: { ...rates } }\n}\n\nexport interface AiCatalogEntry extends AiModelDescriptor, AiCatalogRates {\n /**\n * The tier a routing decision reads: `fast` for short, constrained\n * answers; `balanced` for the assistant and briefs; `deep` for the\n * dearest, slowest class.\n */\n tier: 'fast' | 'balanced' | 'deep'\n /**\n * The shortest prefix this model's provider will cache (AGL-2937). A\n * request whose cached span falls under it is served with the markers\n * honored and NOTHING cached: no error, no warning, and a usage report\n * that reads as a permanent cache miss.\n *\n * It is a property of the model rather than of the tier or the vendor,\n * and it does not fall as a generation advances — so the number has to be\n * written beside the model, and a door that means to cache has to be held\n * to it (`aiCachedPrefixCaches` in the runtime, and the door table in\n * `runtime/ai-prompt-cache.spec.ts`). Without it, \"this block carries a\n * breakpoint\" is a claim nothing checks, and a prompt can be designed\n * around a cache it never had.\n */\n cacheMinTokens: number\n /**\n * A RETIRED row names the model that serves in its place (AGL-3781). The\n * row stays so usage recorded under its id keeps its price, but nothing\n * offers it or routes to it: a setting, an environment override or a pick\n * that names it is served by the successor, so one model per tier holds\n * the tier's traffic and its one cache namespace.\n */\n successor?: string\n}\n\n/**\n * The sentinels a docs-only answer is metered under (AGL-2486): not\n * models, but ids the meter sees, priced at zero so a deflected turn lands\n * in the same rollup as a served one and a keyless deployment does not\n * read as the most efficient one on the platform.\n */\nexport const AI_METER_SENTINELS = {\n docsRetrieval: 'docs-retrieval',\n docsLinks: 'docs-links',\n answerCache: 'assist-cache',\n} as const\n\nconst ANTHROPIC = 'anthropic'\nconst OPENAI_COMPATIBLE = 'openai-compatible'\n\n/**\n * A provider's row on the published subprocessor list: every field but the\n * host, which the registered adapter reports as its `endpointHost`.\n * `{apiKeyEnv}` anywhere in the wording stands for the adapter's\n * `apiKeyEnv`, so a credential's name is written in its adapter alone and\n * never in this module, which the browser bundle carries.\n */\nexport type AiProviderSubprocessorWording = Omit<PluginSubprocessorDeclaration, 'host'>\n\n/** A provider the catalog carries models for. */\nexport interface AiCatalogProvider {\n id: string\n /** The name a settings form shows. */\n label: string\n /**\n * The vendor's row on the published subprocessor list, written here and\n * nowhere else: `aiSubprocessors` derives the plugin's declarations from\n * it. Absent for a provider whose endpoint an operator names, which has\n * no fixed recipient to publish.\n */\n subprocessor?: AiProviderSubprocessorWording\n}\n\n/**\n * The Anthropic row's data cell up to its CRM sentence: every door but CRM\n * assistance, worded once for both of the CRM sentences below.\n */\nconst ANTHROPIC_DATA_RECEIVED_LEAD =\n \"What the user submits — a question, instruction or brief, with the earlier messages of the same Assist conversation — and the content of the element, post, section or page being worked on, with the generated response. On Pro and above, the organization's name and the console route and host travel with an Assist question. For an edit the assistant proposes in the besigner, an outline of the open page, component or layout: element and component ids, layer names, shortened setting values and the selected element's styles. For a generation job, the site inventory: the names and addresses of its pages and collections, the names of its components, layouts, templates, forms and datasets with their prop and field names, and the theme's summary, colors and fonts. For a theme change, the site's current theme settings and brand colors as hex values, from the organization's brand settings, the site logo in the media library or a public page the brief links to. For features that review or write search information, the text and structure of the pages concerned. For product copy, the store's name, the product's name, type, description, tags, options and search listing, the store's category names, and the product's first media-library photo as a copy at most 768 px on its longer edge with its metadata stripped; never another media file, a price, stock, an order or a customer. For products, categories and discounts proposed from a brief, the store's name and its existing category names. For an automation drafted from a brief, which of CRM, webhooks and bookings the plan includes; for an explanation, the automation's outline (trigger, conditions, each step's text with the names of the lists, campaigns, workflows, webhooks and datasets it uses and whether each exists, and a workflow's function names and expressions) and, for a failed run, its time, steps and recorded errors, with email addresses removed and never the triggering event's data. For an insight, the figure reports available and aggregate tables: traffic with top page paths, referrers and campaign tags; form views and submissions; revenue, orders and best-selling product names; bookings by service; campaign subjects with delivery, open and click rates; A/B test and variant conversions; and, for a dataset the member can see, field names and types, record and fill counts, number ranges and totals grouped by a value at least three records share. Email addresses and phone numbers are removed, and no individual record is sent. \"\n\n/** What every CRM door adds after its record sentence: drafts and imports. */\nconst ANTHROPIC_DATA_RECEIVED_CRM_DRAFTS_AND_IMPORTS =\n \"An email draft adds the request and the record's merge field names; an import sends the field names and types and each column's header and value kind, never a row.\"\n\n/** How the data cell words CRM assistance: what a record sends, and what never leaves. */\nexport interface AnthropicCrmAssistanceWording {\n record: string\n never: string\n}\n\n/**\n * CRM assistance once the whole record is sent (AGL-3520): the open-ended\n * wording the Subprocessors row and Privacy Policy section 2 were\n * republished with on October 5, 2026 (legal v10), before\n * `release_crm_assist_whole_record` may be turned on. The catalog row below\n * carries it in `dataReceived`.\n */\nexport const ANTHROPIC_CRM_ASSISTANCE_WHOLE_RECORD: AnthropicCrmAssistanceWording = {\n record:\n 'For CRM assistance, the opened record as the CRM shows it, with all of its standard and custom fields, including contact details, notes, timeline and related records.',\n never: 'No account identifiers or authentication tokens, and no email address outside an opened CRM record.',\n}\n\n/** The Anthropic row's data cell, with CRM assistance worded as `crm` words it. */\nexport function anthropicDataReceived(crm: AnthropicCrmAssistanceWording): string {\n return `${ANTHROPIC_DATA_RECEIVED_LEAD}${crm.record} ${ANTHROPIC_DATA_RECEIVED_CRM_DRAFTS_AND_IMPORTS} ${crm.never}`\n}\n\n/**\n * The providers the catalog carries models for, with the name a settings\n * form shows. A form offers providers from here for the reason it offers\n * models from here: nothing outside this folder names a vendor. For the\n * same reason a vendor's subprocessor row is written here too.\n */\nexport const AI_CATALOG_PROVIDERS: readonly AiCatalogProvider[] = [\n {\n id: ANTHROPIC,\n label: 'Anthropic',\n subprocessor: {\n entity: 'Anthropic, PBC',\n region: 'United States',\n purpose:\n \"AI features of the Services, including the Aglyn Assist helper: creating, editing and reviewing content, drafts and settings for a customer’s site, store and CRM, and explaining and summarizing the customer’s own setup and data\",\n publishedOn: '2026-10-07',\n reason:\n \"Reached through the AI plugin's Anthropic adapter (`libs/plugins/ai/src/lib/providers/anthropic.ts`) by the doors that call the AI runtime. `libs/plugins/ai/src/lib/server/assist-chat.ts` is gated by `release_assist` AND the key, and a generation job's text step by `release_ai_generative`; `libs/plugins/ai/src/lib/server/ai-assist.ts` carries NO release flag, so setting `{apiKeyEnv}` in production is by itself what starts this flow. `assist-anthropic-subprocessor-gate.spec.ts` holds the per-door detail and is the deeper guard for this one vendor.\",\n dataReceived: anthropicDataReceived(ANTHROPIC_CRM_ASSISTANCE_WHOLE_RECORD),\n },\n },\n { id: OPENAI_COMPATIBLE, label: 'OpenAI-compatible endpoint' },\n]\n\n/**\n * `vision` is written on every row rather than assumed (AGL-2916): each model\n * below reads a picture in a user turn, in the four formats\n * `AI_IMAGE_MEDIA_TYPES` names, as its vendor documents. A row added without\n * it reads as a model that does not, and is never sent one.\n */\nconst anthropicCapabilities = {\n streaming: true,\n tools: true,\n thinking: true,\n promptCache: true,\n vision: true,\n}\n\nexport const AI_MODEL_CATALOG: readonly AiCatalogEntry[] = [\n {\n // THE BALANCED TIER'S DEFAULT (AGL-3660): the first balanced row is what\n // `aiDefaultModelFor` answers, so Free, \"Auto\" and every step routed to\n // balanced run here. Rejects `thinking: {type: \"disabled\"}` and forced\n // tool choice: the adapter sends thinking off as `between_tools` and\n // every tool call with `tool_choice: auto`.\n id: 'claude-sonnet-5-5',\n provider: ANTHROPIC,\n label: 'Claude Sonnet 5.5',\n capabilities: anthropicCapabilities,\n /*\n * THE ROWS BILLED ABOVE THEIR PROVIDER RATE (AGL-3015).\n *\n * The balanced tier prices most requests, so this markup is most of the\n * platform's AI margin. It is carried in the rate and not in the credit\n * conversion for the reason the module head gives: a credit stays a\n * fixed quantity of billed spend on every model, so the band, the\n * overage ladder and the invoice keep the arithmetic they were sized\n * with, and nothing below has to know which model served a request.\n *\n * Everything that reasons about what we PAY — the margin surfaces, the\n * staff spend meters, the discount guardrail's cost of goods — reads\n * `providerRates`, which is the vendor's published list. Sonnet 5.5's\n * list is Sonnet 5's, except a cache read: $0.10/MTok, a twentieth of\n * input. A customer is billed exactly what Sonnet 5 billed, so the\n * balanced tier's prices do not move when its default does.\n */\n providerRates: { ...aiRatesPerMTok(2, 10), cacheReadPerToken: 0.1 / 1_000_000 },\n billedRates: aiRatesPerMTok(3, 15),\n tier: 'balanced',\n cacheMinTokens: 512,\n },\n {\n // The balanced default until AGL-3660, retired to Sonnet 5.5 (AGL-3781):\n // the same price, a cache read half as dear, and a cache namespace of\n // its own that every request on it split the tier's prefixes into. Kept\n // so the usage and recordings that name it still price at its rates.\n id: 'claude-sonnet-5',\n provider: ANTHROPIC,\n label: 'Claude Sonnet 5',\n capabilities: anthropicCapabilities,\n providerRates: aiRatesPerMTok(2, 10),\n billedRates: aiRatesPerMTok(3, 15),\n tier: 'balanced',\n cacheMinTokens: 1_024,\n successor: 'claude-sonnet-5-5',\n },\n {\n id: 'claude-sonnet-4-6',\n provider: ANTHROPIC,\n label: 'Claude Sonnet 4.6',\n capabilities: anthropicCapabilities,\n ...aiRatesAtList(3, 15),\n tier: 'balanced',\n cacheMinTokens: 1_024,\n },\n {\n // THE FAST TIER'S DEFAULT (AGL-3781): the first fast row, so every step\n // routed to fast, a Free site's plan and look, and a page's listing run\n // here. Thinking is adaptive and ON when a request names none, so each\n // fast route says what it wants (`providers/routing.ts`); it accepts\n // `disabled` at effort high or below, refuses budgets, sampling\n // parameters and an assistant prefill, which no door sends, and has no\n // server-side fallback, which none asks for.\n //\n // The provider list is $0.10 / $0.50 for a prompt of 100,000 tokens or\n // fewer and five times that above it; no door sends a prompt near it.\n // Billed at the platform's markup on every figure, $0.15 / $0.75, as the\n // balanced tier and pictures are: Haiku 4.5 was billed at its cost, with\n // no margin at all. Its tokenizer counts the same text as about 30% more\n // tokens than Haiku 4.5's, which the credit figures derived from it say.\n id: 'claude-haiku-5-5',\n provider: ANTHROPIC,\n label: 'Claude Haiku 5.5',\n capabilities: anthropicCapabilities,\n ...aiRatesAtMarkup(0.1, 0.5),\n tier: 'fast',\n cacheMinTokens: 512,\n },\n {\n // The fast default until AGL-3781, retired to Haiku 5.5. Rejects an\n // explicit `thinking` setting; the runtime omits it.\n id: 'claude-haiku-4-5',\n provider: ANTHROPIC,\n label: 'Claude Haiku 4.5',\n capabilities: { ...anthropicCapabilities, thinking: false },\n ...aiRatesAtList(1, 5),\n tier: 'fast',\n // Four times the balanced tier's minimum: the cheapest model per token\n // was the hardest one to cache for, so no fast-tier prompt ever cached.\n cacheMinTokens: 4_096,\n successor: 'claude-haiku-5-5',\n },\n {\n id: 'claude-opus-5',\n provider: ANTHROPIC,\n label: 'Claude Opus 5',\n capabilities: anthropicCapabilities,\n ...aiRatesAtList(5, 25),\n tier: 'deep',\n cacheMinTokens: 512,\n },\n {\n // Listed for a side-by-side comparison (AGL-3660); the deep tier's\n // default stays claude-opus-5 above. Thinking cannot be disabled and its\n // effort defaults to medium, so the adapter omits thinking where a step\n // asks for it off and names effort high where a step names none — Opus\n // 5's own default. Forced tool choice is refused; the adapter sends auto.\n id: 'claude-opus-5-5',\n provider: ANTHROPIC,\n label: 'Claude Opus 5.5',\n capabilities: anthropicCapabilities,\n // At list, like its deep-tier siblings. A cache read is $0.20/MTok, a\n // twentieth of input rather than the usual tenth.\n ...aiRatesAtListWithCacheRead(4, 20, 0.2),\n tier: 'deep',\n cacheMinTokens: 512,\n },\n {\n id: 'claude-opus-4-8',\n provider: ANTHROPIC,\n label: 'Claude Opus 4.8',\n capabilities: anthropicCapabilities,\n ...aiRatesAtList(5, 25),\n tier: 'deep',\n cacheMinTokens: 1_024,\n },\n /**\n * The OpenAI-compatible adapter serves whatever the endpoint behind\n * `AI_OPENAI_COMPAT_BASE_URL` serves; these are the ids the routing table\n * may name on it. The figures are the vendor's public list, billed at\n * list: a deployment that reaches its own gateway at a negotiated price\n * is paying less than this says, so its margin reads low rather than\n * high — the direction a cost figure may be wrong in.\n */\n {\n id: 'gpt-5',\n provider: OPENAI_COMPATIBLE,\n label: 'GPT-5',\n capabilities: { streaming: true, tools: true, thinking: false, promptCache: true, vision: true },\n ...aiRatesAtList(1.25, 10),\n tier: 'balanced',\n cacheMinTokens: 1_024,\n },\n {\n id: 'gpt-5-mini',\n provider: OPENAI_COMPATIBLE,\n label: 'GPT-5 mini',\n capabilities: { streaming: true, tools: true, thinking: false, promptCache: true, vision: true },\n ...aiRatesAtList(0.25, 2),\n tier: 'fast',\n cacheMinTokens: 1_024,\n },\n]\n\n/**\n * The IMAGE models (AGL-3602): every model id the Media library's \"Create\n * with AI\" photo mode may route to — Google's Gemini image models on Vertex\n * AI — with a per-picture provider rate and a per-picture billed rate, and the\n * token rates the same request's prompt and thinking are billed at. Kept\n * apart from `AI_MODEL_CATALOG` because nothing that lists text models — the\n * model selector, the routing table, the allotments' allowlists — may ever\n * offer one of these for a conversation.\n *\n * ## The rates\n *\n * Google prices an output picture as image-output tokens: Gemini 3.1 Flash\n * Image writes a 1K picture as 1,120 tokens at $60 per million on the global\n * endpoint, so $0.0672 a picture. Its prompt is input tokens ($0.50 per\n * million) and its thinking is text output ($3.00 per million). Every figure\n * here is the global endpoint's list price, read from Google's Vertex AI\n * pricing page on 2026-10-06; a regional endpoint lists 10% higher, so a\n * deployment that moves off `global` reads its cost low by that much.\n *\n * Every billed figure is the provider figure at `AI_BILLED_MARKUP`:\n * the same ratio the balanced text tier is billed at over its provider rate\n * (Claude Sonnet 5, $3/$15 billed over $2/$10), so a credit buys the same\n * share of margin whether it is spent on words or on pictures. A picture is\n * NEVER billed at provider cost; `billed-rate-is-not-provider-cost.spec.ts`\n * refuses a row that is.\n *\n * A credit is $0.001 of billed spend (`ASSIST_CREDIT_COST_USD`), so the\n * default model's picture is 101 credits before its prompt and thinking.\n *\n * Every per-picture figure on a row is the 1K picture's. A 512 px picture —\n * what the Free plan makes — is written as fewer image tokens at the same\n * per-token rate (`AI_IMAGE_SIZE_OUTPUT_TOKENS`), so it is priced at the 1K\n * figure scaled by its share of tokens: $0.04482 from the provider and\n * $0.06723 billed on the default model, before its prompt and thinking.\n */\nexport interface AiImageCatalogEntry extends AiCatalogRates {\n id: string\n /** The image provider the model is served by. */\n provider: string\n label: string\n /** What the provider bills us for one generated picture. */\n providerUsdPerImage: number\n /** What one picture draws from a customer's credits. */\n billedUsdPerImage: number\n}\n\n/**\n * An image row's four figures from Google's list prices — USD per output\n * picture, and USD per million input and text-output tokens — each billed at\n * the platform's markup, `AI_BILLED_MARKUP`, so an image row cannot be written\n * at list by accident.\n */\nexport function aiImageRatesAtMarkup(\n providerUsdPerImage: number,\n inputPerMTok: number,\n outputPerMTok: number,\n): Pick<\n AiImageCatalogEntry,\n 'providerUsdPerImage' | 'billedUsdPerImage' | 'providerRates' | 'billedRates'\n> {\n return {\n providerUsdPerImage,\n billedUsdPerImage: roundUsd(providerUsdPerImage * AI_BILLED_MARKUP),\n ...aiRatesAtMarkup(inputPerMTok, outputPerMTok),\n }\n}\n\n/**\n * The image-output tokens Google writes a picture as, by size (AGL-3602),\n * read from the usage of real Gemini 3.1 Flash Image answers on 2026-10-07:\n * 747 at 512 px and 1,120 at 1K. A row's per-picture figures are the 1K\n * picture's; a 512 px picture is priced at its share of them.\n */\nexport const AI_IMAGE_SIZE_OUTPUT_TOKENS: Readonly<Record<AiImageSize, number>> = {\n '512': 747,\n '1K': 1_120,\n}\n\n/** A 1K per-picture figure at `size`, by the size's share of image tokens. */\nfunction atImageSize(usdAt1K: number, size: AiImageSize | undefined): number {\n const tokens = AI_IMAGE_SIZE_OUTPUT_TOKENS[size ?? '1K'] ?? AI_IMAGE_SIZE_OUTPUT_TOKENS['1K']\n return roundUsd((usdAt1K * tokens) / AI_IMAGE_SIZE_OUTPUT_TOKENS['1K'])\n}\n\n/** The image model a photo is made with when the operator names none. */\nexport const AI_IMAGE_DEFAULT_MODEL = 'gemini-3.1-flash-image'\n\n/** The image provider's id: Google's models on Vertex AI. */\nexport const AI_IMAGE_VERTEX_PROVIDER_ID = 'google-vertex'\n\n/**\n * The image providers' rows on the published subprocessor list, kept apart\n * from `AI_CATALOG_PROVIDERS` for the reason the image catalog is kept apart\n * from the text one: a settings form offers text providers from that list,\n * and an image provider answers no conversation. The host is the adapter's\n * published endpoint (`VERTEX_IMAGE_PUBLISHED_HOST`).\n *\n * Google's row is dated to its change-log entry on `/legal/subprocessors`,\n * published with the Privacy Policy §2 sentence naming Google for image\n * generation (legal v12).\n */\nexport const AI_IMAGE_CATALOG_PROVIDERS: readonly AiCatalogProvider[] = [\n {\n id: AI_IMAGE_VERTEX_PROVIDER_ID,\n label: 'Google Vertex AI',\n subprocessor: {\n entity: 'Google LLC (Google Cloud Vertex AI)',\n // The global endpoint, which Google documents as routing and\n // processing a request anywhere in the world, with no data residency\n // guarantee: Google, not Aglyn, chooses the location.\n region: 'Global — Google selects where requests are processed',\n purpose:\n \"AI image generation: creating images for a customer's media library from a description a user writes\",\n publishedOn: '2026-10-08',\n reason:\n \"Reached through the AI plugin's Vertex AI image adapter (`libs/plugins/ai/src/lib/providers/vertex-image.ts`) by the Media library's Create with AI door (`libs/plugins/ai/src/lib/server/ai-media-image.ts`) for every kind that is not drawn as SVG, as the platform's own service account. Off unless `AI_IMAGE_VERTEX_PROJECT` names a Google Cloud project; the console offers those kinds only where `NEXT_PUBLIC_AI_IMAGE_PHOTOS` is `on`. The request carries the description and the kind's fixed style wording, the shape and fixed settings, and nothing else.\",\n dataReceived:\n \"The description the user writes and the shape requested, and the generated image returned. No account identifiers, email addresses, or other content of the customer's site.\",\n },\n },\n]\n\nexport const AI_IMAGE_MODEL_CATALOG: readonly AiImageCatalogEntry[] = [\n {\n // The default: Google's own replacement for Imagen 4, which Vertex AI\n // discontinued in 2026.\n id: 'gemini-3.1-flash-image',\n provider: AI_IMAGE_VERTEX_PROVIDER_ID,\n label: 'Gemini 3.1 Flash Image',\n ...aiImageRatesAtMarkup(0.0672, 0.5, 3),\n },\n {\n id: 'gemini-3.1-flash-lite-image',\n provider: AI_IMAGE_VERTEX_PROVIDER_ID,\n label: 'Gemini 3.1 Flash-Lite Image',\n ...aiImageRatesAtMarkup(0.0336, 0.25, 1.5),\n },\n {\n id: 'gemini-3-pro-image',\n provider: AI_IMAGE_VERTEX_PROVIDER_ID,\n label: 'Gemini 3 Pro Image',\n ...aiImageRatesAtMarkup(0.1344, 2, 12),\n },\n]\n\n/**\n * The per-picture rates an unknown image model id is priced at: the dearest\n * row, on both rates, for the reason `AI_FALLBACK_RATES` gives — an estimate\n * that errs low is worse than one that errs high.\n */\nexport const AI_IMAGE_FALLBACK_RATES: Pick<\n AiImageCatalogEntry,\n 'providerUsdPerImage' | 'billedUsdPerImage'\n> = {\n providerUsdPerImage: Math.max(...AI_IMAGE_MODEL_CATALOG.map((row) => row.providerUsdPerImage)),\n billedUsdPerImage: Math.max(...AI_IMAGE_MODEL_CATALOG.map((row) => row.billedUsdPerImage)),\n}\n\n/** The image catalog row for a model id, or `undefined` for an unknown id. */\nexport function aiImageCatalogEntry(modelId: string): AiImageCatalogEntry | undefined {\n return AI_IMAGE_MODEL_CATALOG.find((entry) => entry.id === modelId)\n}\n\n/** What the provider charges us for one picture from `modelId`, at `size` (1K when absent). */\nexport function aiImageProviderUsdPerImage(modelId: string, size?: AiImageSize): number {\n return atImageSize((aiImageCatalogEntry(modelId) ?? AI_IMAGE_FALLBACK_RATES).providerUsdPerImage, size)\n}\n\n/** What one picture from `modelId` at `size` (1K when absent) draws from a customer's credits. */\nexport function aiImageBilledUsdPerImage(modelId: string, size?: AiImageSize): number {\n return atImageSize((aiImageCatalogEntry(modelId) ?? AI_IMAGE_FALLBACK_RATES).billedUsdPerImage, size)\n}\n\n/**\n * Usage as the meter prices it: a text exchange's tokens, and, for a door\n * that makes pictures, how many it delivered and at what size (1K when\n * absent). `images` is absent on every text exchange, which prices exactly\n * as it always did.\n */\nexport type AiMeteredUsage = AiUsage & { images?: number; imageSize?: AiImageSize }\n\n/** Pictures on a usage record: whole, finite, non-negative, else 0. */\nfunction aiImageCount(usage: AiMeteredUsage): number {\n const value = Number(usage.images ?? 0)\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0\n}\n\nfunction roundUsd(value: number): number {\n return Math.round(value * 1_000_000) / 1_000_000\n}\n\n/** The dearest known tier, used when a model id is not in the catalog. */\nexport const AI_FALLBACK_RATES: AiTokenRates = aiRatesPerMTok(10, 50)\n\n/**\n * The minimum an unknown model id is assumed to have: the largest one the\n * catalog knows. It errs the way the rate fallback errs — toward the answer\n * that costs more — so an unrecognized id reads as \"this prompt does not\n * cache\" rather than promising a saving nobody measured.\n */\nexport const AI_FALLBACK_CACHE_MIN_TOKENS = 4_096\n\n/** The shortest prefix a model's provider will cache; the dearest assumption for an unknown id. */\nexport function aiModelCacheMinTokens(modelId: string): number {\n return aiCatalogEntry(modelId)?.cacheMinTokens ?? AI_FALLBACK_CACHE_MIN_TOKENS\n}\n\nconst ZERO_RATES = aiRatesPerMTok(0, 0)\n\n/** The catalog row for a model id, or `undefined` for an unknown id. */\nexport function aiCatalogEntry(modelId: string): AiCatalogEntry | undefined {\n return AI_MODEL_CATALOG.find((entry) => entry.id === modelId)\n}\n\nconst isSentinel = (modelId: string): boolean =>\n (Object.values(AI_METER_SENTINELS) as string[]).includes(modelId)\n\n/**\n * What the PROVIDER charges for a model id — our bill. A meter sentinel is\n * free; an image model's prompt and thinking are priced at its image row's\n * token rates (AGL-3602); an unknown id is priced at the dearest tier.\n *\n * The figure every cost and margin surface prices usage at. Answering the\n * customer's question from here would report a margin the platform does not\n * earn, and answering ours from `aiBilledRatesForModel` reports one it does\n * not pay; the two are separate functions so neither is reachable by\n * forgetting which one was meant.\n */\nexport function aiProviderRatesForModel(modelId: string): AiTokenRates {\n if (isSentinel(modelId)) return ZERO_RATES\n return (\n aiCatalogEntry(modelId)?.providerRates ??\n aiImageCatalogEntry(modelId)?.providerRates ??\n AI_FALLBACK_RATES\n )\n}\n\n/**\n * What a CUSTOMER'S CREDITS are charged at for a model id. A meter sentinel\n * is free; an unknown id is priced at the dearest tier.\n *\n * The figure the credit meter, the band and the model selector price usage\n * at. It is at or above `aiProviderRatesForModel` on every row.\n */\nexport function aiBilledRatesForModel(modelId: string): AiTokenRates {\n if (isSentinel(modelId)) return ZERO_RATES\n return (\n aiCatalogEntry(modelId)?.billedRates ??\n aiImageCatalogEntry(modelId)?.billedRates ??\n AI_FALLBACK_RATES\n )\n}\n\nfunction priceUsage(usage: AiUsage, rate: AiTokenRates): number {\n const raw =\n usage.inputTokens * rate.inputPerToken +\n usage.outputTokens * rate.outputPerToken +\n usage.cacheReadTokens * rate.cacheReadPerToken +\n usage.cacheWriteTokens * rate.cacheWritePerToken\n return Math.round(raw * 1_000_000) / 1_000_000\n}\n\n/**\n * What one exchange COST US, at the serving model's provider rates, rounded\n * to 6dp. Real money, and the only figure a margin may be taken against.\n */\nexport function estimateAiProviderCostUsd(usage: AiMeteredUsage, modelId: string): number {\n return roundUsd(\n priceUsage(usage, aiProviderRatesForModel(modelId)) +\n aiImageCount(usage) * aiImageProviderUsdPerImage(modelId, usage.imageSize),\n )\n}\n\n/**\n * What one exchange DRAWS FROM A CUSTOMER, at the serving model's billed\n * rates, rounded to 6dp — the figure `assistCreditsFromUsd` turns into\n * credits. At or above `estimateAiProviderCostUsd` for the same exchange.\n */\nexport function estimateAiBilledUsd(usage: AiMeteredUsage, modelId: string): number {\n return roundUsd(\n priceUsage(usage, aiBilledRatesForModel(modelId)) +\n aiImageCount(usage) * aiImageBilledUsdPerImage(modelId, usage.imageSize),\n )\n}\n\n/** Every catalog id a provider serves. */\nexport function aiModelIdsForProvider(providerId: string): string[] {\n return AI_MODEL_CATALOG.filter((entry) => entry.provider === providerId).map(\n (entry) => entry.id,\n )\n}\n\n/**\n * What a step asks for, and what the routing table answers with. Every\n * door names its kind here rather than a model, so a model swap — an\n * incident override, an org's own setting, a cheaper model for a cheap\n * step — is one row rather than a search through the doors (AGL-2937).\n */\nexport type AiStepKind =\n | 'assist.chat'\n | 'copy.element'\n | 'copy.section'\n | 'copy.blog'\n | 'generate.section'\n | 'job.component'\n | 'job.crm'\n | 'job.edit'\n | 'job.experiment'\n | 'job.form'\n | 'job.insight'\n | 'job.layout'\n | 'job.logic'\n | 'job.template'\n | 'job.page'\n | 'job.products'\n | 'job.seo'\n | 'job.text'\n | 'job.theme'\n | 'job.plan'\n | 'job.email'\n | 'job.campaign'\n | 'job.workflow'\n\n/** The tier each step kind is served from when no setting overrides it. */\nexport const AI_STEP_TIERS: Record<AiStepKind, AiCatalogEntry['tier']> = {\n 'assist.chat': 'balanced',\n 'copy.element': 'fast',\n 'copy.section': 'balanced',\n 'copy.blog': 'balanced',\n 'generate.section': 'balanced',\n // A reusable component is one structured tree with the typed props it\n // declares, and choosing what becomes a prop is the judgment the step sells.\n 'job.component': 'balanced',\n // CRM by AI (AGL-2917): a record's summary and next step, a deal's stage, a\n // lead's standing, a one-to-one email draft and an import's column matches.\n // Short answers through a strict tool, held to the facts the CRM reports\n // about the record the member opened.\n 'job.crm': 'fast',\n // A change to a page or a layout the site has (AGL-3616): the Assist edit\n // rung's protocol run as a job, held to the closed world of the document's\n // own element ids and to the palette validators. Finding the element a\n // request means, and changing only it, is the judgment; the chat door that\n // proposes the same edits runs on this tier.\n 'job.edit': 'balanced',\n // A/B tests by AI (AGL-2914): variants of one piece of copy, and a\n // result put into words for a verdict code already reached. Short answers\n // through a strict tool, but the judgment a variant sells — which ONE idea\n // to vary so the result says something — is the balanced tier's.\n 'job.experiment': 'balanced',\n // A form is one small tree held to the building rules and to the contract\n // its submissions are read by; a re-ask costs more than the tier saves.\n 'job.form': 'balanced',\n // An insight (AGL-2915) chooses which figures answer a question and says\n // what they show; the trace holds every number, and the judgment of what is\n // worth saying is what the fast tier does worse.\n 'job.insight': 'balanced',\n // A layout or a page template is one structured tree held to every\n // building rule; the fast tier re-asks more than it saves.\n 'job.layout': 'balanced',\n // A site function or variable (AGL-3603): a small structured answer held\n // to the evaluator's grammar, the names it may read and a first run, whose\n // judgment is turning a description into conditions and arithmetic; the\n // fast tier re-asks more than it saves.\n 'job.logic': 'balanced',\n 'job.template': 'balanced',\n // A page section (AGL-2907) is held to the whole page's building rules on\n // every pass, for the same reason.\n 'job.page': 'balanced',\n // A product's copy, a store's first products, or its categories and\n // discounts (AGL-2916): judgment about what a photo shows and what a shopper\n // needs, held to storefront claim rules a re-ask costs more to meet than the\n // tier saves.\n 'job.products': 'balanced',\n // SEO fields, alt text and an audit's fixes (AGL-2910): short answers\n // through a strict tool, held to a length and to the page's own text.\n 'job.seo': 'fast',\n 'job.text': 'balanced',\n // A theme is one structured answer over a small, fixed control set; the\n // judgment is in the color choices, which the fast tier makes worse.\n 'job.theme': 'balanced',\n 'job.plan': 'balanced',\n // An email is one tree held to every building rule, with subject lines\n // whose judgment is the point of asking; the fast tier re-asks more than it\n // saves. A campaign is the same email, drafted into a campaign.\n 'job.email': 'balanced',\n 'job.campaign': 'balanced',\n // An automation is a small structured answer whose judgment is choosing the\n // trigger and steps a description means, and an explanation reads one\n // closely; the fast tier re-asks more than it saves on both (AGL-2919).\n 'job.workflow': 'balanced',\n}\n\n/** The first current catalog model of a tier on a provider, or the provider's first current model. */\nexport function aiDefaultModelFor(providerId: string, tier: AiCatalogEntry['tier']): string | undefined {\n const onProvider = AI_MODEL_CATALOG.filter((entry) => entry.provider === providerId && !entry.successor)\n return (onProvider.find((entry) => entry.tier === tier) ?? onProvider[0])?.id\n}\n"],"names":["aiRatesPerMTok","inputPerMTok","outputPerMTok","inputPerToken","outputPerToken","cacheReadPerToken","cacheWritePerToken","aiRatesAtList","providerRates","billedRates","AI_BILLED_MARKUP","aiRatesMarkedUp","rates","aiRatesAtMarkup","aiRatesAtListWithCacheRead","cacheReadPerMTok","AI_METER_SENTINELS","docsRetrieval","docsLinks","answerCache","ANTHROPIC","OPENAI_COMPATIBLE","ANTHROPIC_DATA_RECEIVED_LEAD","ANTHROPIC_DATA_RECEIVED_CRM_DRAFTS_AND_IMPORTS","ANTHROPIC_CRM_ASSISTANCE_WHOLE_RECORD","record","never","anthropicDataReceived","crm","AI_CATALOG_PROVIDERS","id","label","subprocessor","entity","region","purpose","publishedOn","reason","dataReceived","anthropicCapabilities","streaming","tools","thinking","promptCache","vision","AI_MODEL_CATALOG","provider","capabilities","tier","cacheMinTokens","successor","aiImageRatesAtMarkup","providerUsdPerImage","billedUsdPerImage","roundUsd","AI_IMAGE_SIZE_OUTPUT_TOKENS","atImageSize","usdAt1K","size","tokens","AI_IMAGE_DEFAULT_MODEL","AI_IMAGE_VERTEX_PROVIDER_ID","AI_IMAGE_CATALOG_PROVIDERS","AI_IMAGE_MODEL_CATALOG","AI_IMAGE_FALLBACK_RATES","Math","max","map","row","aiImageCatalogEntry","modelId","find","entry","aiImageProviderUsdPerImage","aiImageBilledUsdPerImage","aiImageCount","usage","value","Number","images","isFinite","floor","round","AI_FALLBACK_RATES","AI_FALLBACK_CACHE_MIN_TOKENS","aiModelCacheMinTokens","aiCatalogEntry","ZERO_RATES","isSentinel","Object","values","includes","aiProviderRatesForModel","aiBilledRatesForModel","priceUsage","rate","raw","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","estimateAiProviderCostUsd","imageSize","estimateAiBilledUsd","aiModelIdsForProvider","providerId","filter","AI_STEP_TIERS","aiDefaultModelFor","onProvider"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/providers/catalog.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 type { PluginSubprocessorDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-subprocessors'\nimport type { AiModelDescriptor, AiUsage } from './contract'\nimport type { AiImageSize } from './image-contract'\n\n/**\n * The model catalog (AGL-2939): every model id the plugin will route to,\n * with its provider, its display name, its capabilities and its list rates.\n * The ONE place a model id is written outside a provider adapter — the\n * routing table maps a step kind to an id through here, the meter prices\n * usage by id through here, and a settings form offers ids from here.\n *\n * ## Two rates, because one figure was answering two questions\n *\n * Every row carries a PROVIDER rate and a BILLED rate (AGL-3015):\n *\n * - `providerRates` is what the vendor charges us. It is money leaving the\n * company, so cost accounting, the margin surfaces and every staff spend\n * meter price usage through this one and no other.\n * - `billedRates` is what a customer's credits are charged at. The credit\n * meter, the band, the overage ladder and the model selector price usage\n * through this one, because it is the figure a customer's balance moves by.\n *\n * They are equal on nearly every row, and a helper writes the pair from one\n * figure so an unmarked row cannot drift apart by a typo. A row may be\n * billed ABOVE its provider rate deliberately — the markup is carried in the\n * rate rather than in the credit conversion, which keeps a credit a fixed\n * quantity of billed spend on every model and leaves the band, the ladder\n * and the invoice on the one arithmetic they were sized with. A row billed\n * BELOW its provider rate would sell tokens at a loss, and\n * `billed-rate-is-not-provider-cost.spec.ts` refuses one.\n *\n * Both are keyed by model rather than fixed at one model's rates because the\n * default is an env override: a one-line incident swap to a dearer model\n * would otherwise keep reporting the cheaper money, and per-org cost would\n * read as roughly right — the failure mode this whole meter exists to\n * prevent. An unknown id falls back to the most EXPENSIVE known tier on\n * purpose, on both rates: an estimate that errs low is worse than one that\n * errs high, whether the figure is our bill or the customer's.\n */\n\nexport interface AiTokenRates {\n inputPerToken: number\n outputPerToken: number\n cacheReadPerToken: number\n cacheWritePerToken: number\n}\n\n/** List price per MTok → per-token rates, cache read at 0.1x, write 1.25x. */\nexport function aiRatesPerMTok(inputPerMTok: number, outputPerMTok: number): AiTokenRates {\n return {\n inputPerToken: inputPerMTok / 1_000_000,\n outputPerToken: outputPerMTok / 1_000_000,\n cacheReadPerToken: (inputPerMTok * 0.1) / 1_000_000,\n cacheWritePerToken: (inputPerMTok * 1.25) / 1_000_000,\n }\n}\n\n/**\n * The two rate tables a catalog row carries. Separate fields rather than one\n * table and a multiplier: a reader that wants our cost has to name it, and\n * a reader that wants the customer's has to name that, so neither can be\n * reached by accident.\n */\nexport interface AiCatalogRates {\n /** What the provider bills us for this model. */\n providerRates: AiTokenRates\n /** What a customer's credits are charged at for this model. */\n billedRates: AiTokenRates\n}\n\n/**\n * A row billed at exactly the provider's list rate — the usual case, written\n * once so the pair cannot drift.\n */\nexport function aiRatesAtList(\n inputPerMTok: number,\n outputPerMTok: number,\n): AiCatalogRates {\n return {\n providerRates: aiRatesPerMTok(inputPerMTok, outputPerMTok),\n billedRates: aiRatesPerMTok(inputPerMTok, outputPerMTok),\n }\n}\n\n/**\n * THE PLATFORM'S MARKUP over provider cost (AGL-3781): what a customer's\n * credits are charged for a model over what the model costs us, on every\n * figure. The balanced tier's list (Sonnet, $3 / $15 billed over $2 / $10),\n * a picture and the fast tier are billed at it, so a credit carries the same\n * margin whichever of them it is spent on.\n */\nexport const AI_BILLED_MARKUP = 1.5\n\n/** `rates` at the platform's markup: every figure times {@link AI_BILLED_MARKUP}. */\nexport function aiRatesMarkedUp(rates: AiTokenRates): AiTokenRates {\n return {\n inputPerToken: rates.inputPerToken * AI_BILLED_MARKUP,\n outputPerToken: rates.outputPerToken * AI_BILLED_MARKUP,\n cacheReadPerToken: rates.cacheReadPerToken * AI_BILLED_MARKUP,\n cacheWritePerToken: rates.cacheWritePerToken * AI_BILLED_MARKUP,\n }\n}\n\n/** A row billed at the platform's markup over the provider's list price per MTok. */\nexport function aiRatesAtMarkup(inputPerMTok: number, outputPerMTok: number): AiCatalogRates {\n const providerRates = aiRatesPerMTok(inputPerMTok, outputPerMTok)\n return { providerRates, billedRates: aiRatesMarkedUp(providerRates) }\n}\n\n/** {@link aiRatesAtList} for a model whose cache read is not a tenth of its input price. */\nexport function aiRatesAtListWithCacheRead(\n inputPerMTok: number,\n outputPerMTok: number,\n cacheReadPerMTok: number,\n): AiCatalogRates {\n const rates = { ...aiRatesPerMTok(inputPerMTok, outputPerMTok), cacheReadPerToken: cacheReadPerMTok / 1_000_000 }\n return { providerRates: rates, billedRates: { ...rates } }\n}\n\nexport interface AiCatalogEntry extends AiModelDescriptor, AiCatalogRates {\n /**\n * The tier a routing decision reads: `fast` for short, constrained\n * answers; `balanced` for the assistant and briefs; `deep` for the\n * dearest, slowest class.\n */\n tier: 'fast' | 'balanced' | 'deep'\n /**\n * The shortest prefix this model's provider will cache (AGL-2937). A\n * request whose cached span falls under it is served with the markers\n * honored and NOTHING cached: no error, no warning, and a usage report\n * that reads as a permanent cache miss.\n *\n * It is a property of the model rather than of the tier or the vendor,\n * and it does not fall as a generation advances — so the number has to be\n * written beside the model, and a door that means to cache has to be held\n * to it (`aiCachedPrefixCaches` in the runtime, and the door table in\n * `runtime/ai-prompt-cache.spec.ts`). Without it, \"this block carries a\n * breakpoint\" is a claim nothing checks, and a prompt can be designed\n * around a cache it never had.\n */\n cacheMinTokens: number\n /**\n * A RETIRED row names the model that serves in its place (AGL-3781). The\n * row stays so usage recorded under its id keeps its price, but nothing\n * offers it or routes to it: a setting, an environment override or a pick\n * that names it is served by the successor, so one model per tier holds\n * the tier's traffic and its one cache namespace.\n */\n successor?: string\n}\n\n/**\n * The sentinels a docs-only answer is metered under (AGL-2486): not\n * models, but ids the meter sees, priced at zero so a deflected turn lands\n * in the same rollup as a served one and a keyless deployment does not\n * read as the most efficient one on the platform.\n */\nexport const AI_METER_SENTINELS = {\n docsRetrieval: 'docs-retrieval',\n docsLinks: 'docs-links',\n answerCache: 'assist-cache',\n} as const\n\nconst ANTHROPIC = 'anthropic'\nconst OPENAI_COMPATIBLE = 'openai-compatible'\n\n/**\n * A provider's row on the published subprocessor list: every field but the\n * host, which the registered adapter reports as its `endpointHost`.\n * `{apiKeyEnv}` anywhere in the wording stands for the adapter's\n * `apiKeyEnv`, so a credential's name is written in its adapter alone and\n * never in this module, which the browser bundle carries.\n */\nexport type AiProviderSubprocessorWording = Omit<PluginSubprocessorDeclaration, 'host'>\n\n/** A provider the catalog carries models for. */\nexport interface AiCatalogProvider {\n id: string\n /** The name a settings form shows. */\n label: string\n /**\n * The vendor's row on the published subprocessor list, written here and\n * nowhere else: `aiSubprocessors` derives the plugin's declarations from\n * it. Absent for a provider whose endpoint an operator names, which has\n * no fixed recipient to publish.\n */\n subprocessor?: AiProviderSubprocessorWording\n}\n\n/**\n * The Anthropic row's data cell up to its CRM sentence: every door but CRM\n * assistance, worded once for both of the CRM sentences below.\n */\nconst ANTHROPIC_DATA_RECEIVED_LEAD =\n \"What the user submits — a question, instruction or brief, with the earlier messages of the same Assist conversation — and the content of the element, post, section or page being worked on, with the generated response. On Pro and above, the organization's name and the console route and host travel with an Assist question. For an edit the assistant proposes in the besigner, an outline of the open page, component or layout: element and component ids, layer names, shortened setting values and the selected element's styles. For a generation job, the site inventory: the names and addresses of its pages and collections, the names of its components, layouts, templates, forms and datasets with their prop and field names, and the theme's summary, colors and fonts. For a theme change, the site's current theme settings and brand colors as hex values, from the organization's brand settings, the site logo in the media library or a public page the brief links to. For features that review or write search information, the text and structure of the pages concerned. For product copy, the store's name, the product's name, type, description, tags, options and search listing, the store's category names, and the product's first media-library photo as a copy at most 768 px on its longer edge with its metadata stripped; never another media file, a price, stock, an order or a customer. For products, categories and discounts proposed from a brief, the store's name and its existing category names. For an automation drafted from a brief, which of CRM, webhooks and bookings the plan includes; for an explanation, the automation's outline (trigger, conditions, each step's text with the names of the lists, campaigns, workflows, webhooks and datasets it uses and whether each exists, and a workflow's function names and expressions) and, for a failed run, its time, steps and recorded errors, with email addresses removed and never the triggering event's data. For an insight, the figure reports available and aggregate tables: traffic with top page paths, referrers and campaign tags; form views and submissions; revenue, orders and best-selling product names; bookings by service; campaign subjects with delivery, open and click rates; A/B test and variant conversions; and, for a dataset the member can see, field names and types, record and fill counts, number ranges and totals grouped by a value at least three records share. Email addresses and phone numbers are removed, and no individual record is sent. \"\n\n/** What every CRM door adds after its record sentence: drafts and imports. */\nconst ANTHROPIC_DATA_RECEIVED_CRM_DRAFTS_AND_IMPORTS =\n \"An email draft adds the request and the record's merge field names; an import sends the field names and types and each column's header and value kind, never a row.\"\n\n/** How the data cell words CRM assistance: what a record sends, and what never leaves. */\nexport interface AnthropicCrmAssistanceWording {\n record: string\n never: string\n}\n\n/**\n * CRM assistance once the whole record is sent (AGL-3520): the open-ended\n * wording the Subprocessors row and Privacy Policy section 2 were\n * republished with on October 5, 2026 (legal v10), before\n * `release_crm_assist_whole_record` may be turned on. The catalog row below\n * carries it in `dataReceived`.\n */\nexport const ANTHROPIC_CRM_ASSISTANCE_WHOLE_RECORD: AnthropicCrmAssistanceWording = {\n record:\n 'For CRM assistance, the opened record as the CRM shows it, with all of its standard and custom fields, including contact details, notes, timeline and related records.',\n never: 'No account identifiers or authentication tokens, and no email address outside an opened CRM record.',\n}\n\n/** The Anthropic row's data cell, with CRM assistance worded as `crm` words it. */\nexport function anthropicDataReceived(crm: AnthropicCrmAssistanceWording): string {\n return `${ANTHROPIC_DATA_RECEIVED_LEAD}${crm.record} ${ANTHROPIC_DATA_RECEIVED_CRM_DRAFTS_AND_IMPORTS} ${crm.never}`\n}\n\n/**\n * The providers the catalog carries models for, with the name a settings\n * form shows. A form offers providers from here for the reason it offers\n * models from here: nothing outside this folder names a vendor. For the\n * same reason a vendor's subprocessor row is written here too.\n */\nexport const AI_CATALOG_PROVIDERS: readonly AiCatalogProvider[] = [\n {\n id: ANTHROPIC,\n label: 'Anthropic',\n subprocessor: {\n entity: 'Anthropic, PBC',\n region: 'United States',\n purpose:\n \"AI features of the Services, including the Aglyn Assist helper: creating, editing and reviewing content, drafts and settings for a customer’s site, store and CRM, and explaining and summarizing the customer’s own setup and data\",\n publishedOn: '2026-10-07',\n reason:\n \"Reached through the AI plugin's Anthropic adapter (`libs/plugins/ai/src/lib/providers/anthropic.ts`) by the doors that call the AI runtime. `libs/plugins/ai/src/lib/server/assist-chat.ts` is gated by `release_assist` AND the key, and a generation job's text step by `release_ai_generative`; `libs/plugins/ai/src/lib/server/ai-assist.ts` carries NO release flag, so setting `{apiKeyEnv}` in production is by itself what starts this flow. `assist-anthropic-subprocessor-gate.spec.ts` holds the per-door detail and is the deeper guard for this one vendor.\",\n dataReceived: anthropicDataReceived(ANTHROPIC_CRM_ASSISTANCE_WHOLE_RECORD),\n },\n },\n { id: OPENAI_COMPATIBLE, label: 'OpenAI-compatible endpoint' },\n]\n\n/**\n * `vision` is written on every row rather than assumed (AGL-2916): each model\n * below reads a picture in a user turn, in the four formats\n * `AI_IMAGE_MEDIA_TYPES` names, as its vendor documents. A row added without\n * it reads as a model that does not, and is never sent one.\n */\nconst anthropicCapabilities = {\n streaming: true,\n tools: true,\n thinking: true,\n promptCache: true,\n vision: true,\n}\n\nexport const AI_MODEL_CATALOG: readonly AiCatalogEntry[] = [\n {\n // THE BALANCED TIER'S DEFAULT (AGL-3660): the first balanced row is what\n // `aiDefaultModelFor` answers, so Free, \"Auto\" and every step routed to\n // balanced run here. Rejects `thinking: {type: \"disabled\"}` and forced\n // tool choice: the adapter sends thinking off as `between_tools` and\n // every tool call with `tool_choice: auto`, a request's `toolChoice`\n // included (no `forcedToolChoice`, AGL-4076).\n id: 'claude-sonnet-5-5',\n provider: ANTHROPIC,\n label: 'Claude Sonnet 5.5',\n capabilities: anthropicCapabilities,\n /*\n * THE ROWS BILLED ABOVE THEIR PROVIDER RATE (AGL-3015).\n *\n * The balanced tier prices most requests, so this markup is most of the\n * platform's AI margin. It is carried in the rate and not in the credit\n * conversion for the reason the module head gives: a credit stays a\n * fixed quantity of billed spend on every model, so the band, the\n * overage ladder and the invoice keep the arithmetic they were sized\n * with, and nothing below has to know which model served a request.\n *\n * Everything that reasons about what we PAY — the margin surfaces, the\n * staff spend meters, the discount guardrail's cost of goods — reads\n * `providerRates`, which is the vendor's published list. Sonnet 5.5's\n * list is Sonnet 5's, except a cache read: $0.10/MTok, a twentieth of\n * input. A customer is billed exactly what Sonnet 5 billed, so the\n * balanced tier's prices do not move when its default does.\n */\n providerRates: { ...aiRatesPerMTok(2, 10), cacheReadPerToken: 0.1 / 1_000_000 },\n billedRates: aiRatesPerMTok(3, 15),\n tier: 'balanced',\n cacheMinTokens: 512,\n },\n {\n // The balanced default until AGL-3660, retired to Sonnet 5.5 (AGL-3781):\n // the same price, a cache read half as dear, and a cache namespace of\n // its own that every request on it split the tier's prefixes into. Kept\n // so the usage and recordings that name it still price at its rates.\n id: 'claude-sonnet-5',\n provider: ANTHROPIC,\n label: 'Claude Sonnet 5',\n capabilities: { ...anthropicCapabilities, forcedToolChoice: true },\n providerRates: aiRatesPerMTok(2, 10),\n billedRates: aiRatesPerMTok(3, 15),\n tier: 'balanced',\n cacheMinTokens: 1_024,\n successor: 'claude-sonnet-5-5',\n },\n {\n id: 'claude-sonnet-4-6',\n provider: ANTHROPIC,\n label: 'Claude Sonnet 4.6',\n capabilities: { ...anthropicCapabilities, forcedToolChoice: true },\n ...aiRatesAtList(3, 15),\n tier: 'balanced',\n cacheMinTokens: 1_024,\n },\n {\n // THE FAST TIER'S DEFAULT (AGL-3781): the first fast row, so every step\n // routed to fast, a Free site's plan and look, and a page's listing run\n // here. Thinking is adaptive and ON when a request names none, so each\n // fast route says what it wants (`providers/routing.ts`); it accepts\n // `disabled` at effort high or below, refuses budgets, sampling\n // parameters and an assistant prefill, which no door sends, and has no\n // server-side fallback, which none asks for.\n //\n // The provider list is $0.10 / $0.50 for a prompt of 100,000 tokens or\n // fewer and five times that above it; no door sends a prompt near it.\n // Billed at the platform's markup on every figure, $0.15 / $0.75, as the\n // balanced tier and pictures are: Haiku 4.5 was billed at its cost, with\n // no margin at all. Its tokenizer counts the same text as about 30% more\n // tokens than Haiku 4.5's, which the credit figures derived from it say.\n id: 'claude-haiku-5-5',\n provider: ANTHROPIC,\n label: 'Claude Haiku 5.5',\n capabilities: anthropicCapabilities,\n ...aiRatesAtMarkup(0.1, 0.5),\n tier: 'fast',\n cacheMinTokens: 512,\n },\n {\n // The fast default until AGL-3781, retired to Haiku 5.5. Rejects an\n // explicit `thinking` setting; the runtime omits it.\n id: 'claude-haiku-4-5',\n provider: ANTHROPIC,\n label: 'Claude Haiku 4.5',\n capabilities: { ...anthropicCapabilities, thinking: false },\n ...aiRatesAtList(1, 5),\n tier: 'fast',\n // Four times the balanced tier's minimum: the cheapest model per token\n // was the hardest one to cache for, so no fast-tier prompt ever cached.\n cacheMinTokens: 4_096,\n successor: 'claude-haiku-5-5',\n },\n {\n id: 'claude-opus-5',\n provider: ANTHROPIC,\n label: 'Claude Opus 5',\n capabilities: { ...anthropicCapabilities, forcedToolChoice: true },\n ...aiRatesAtList(5, 25),\n tier: 'deep',\n cacheMinTokens: 512,\n },\n {\n // Listed for a side-by-side comparison (AGL-3660); the deep tier's\n // default stays claude-opus-5 above. Thinking cannot be disabled and its\n // effort defaults to medium, so the adapter omits thinking where a step\n // asks for it off and names effort high where a step names none — Opus\n // 5's own default. Forced tool choice is refused; the adapter sends auto.\n id: 'claude-opus-5-5',\n provider: ANTHROPIC,\n label: 'Claude Opus 5.5',\n capabilities: anthropicCapabilities,\n // At list, like its deep-tier siblings. A cache read is $0.20/MTok, a\n // twentieth of input rather than the usual tenth.\n ...aiRatesAtListWithCacheRead(4, 20, 0.2),\n tier: 'deep',\n cacheMinTokens: 512,\n },\n {\n id: 'claude-opus-4-8',\n provider: ANTHROPIC,\n label: 'Claude Opus 4.8',\n capabilities: { ...anthropicCapabilities, forcedToolChoice: true },\n ...aiRatesAtList(5, 25),\n tier: 'deep',\n cacheMinTokens: 1_024,\n },\n /**\n * The OpenAI-compatible adapter serves whatever the endpoint behind\n * `AI_OPENAI_COMPAT_BASE_URL` serves; these are the ids the routing table\n * may name on it. The figures are the vendor's public list, billed at\n * list: a deployment that reaches its own gateway at a negotiated price\n * is paying less than this says, so its margin reads low rather than\n * high — the direction a cost figure may be wrong in.\n */\n {\n id: 'gpt-5',\n provider: OPENAI_COMPATIBLE,\n label: 'GPT-5',\n capabilities: { streaming: true, tools: true, thinking: false, promptCache: true, vision: true },\n ...aiRatesAtList(1.25, 10),\n tier: 'balanced',\n cacheMinTokens: 1_024,\n },\n {\n id: 'gpt-5-mini',\n provider: OPENAI_COMPATIBLE,\n label: 'GPT-5 mini',\n capabilities: { streaming: true, tools: true, thinking: false, promptCache: true, vision: true },\n ...aiRatesAtList(0.25, 2),\n tier: 'fast',\n cacheMinTokens: 1_024,\n },\n]\n\n/**\n * The IMAGE models (AGL-3602): every model id the Media library's \"Create\n * with AI\" photo mode may route to — Google's Gemini image models on Vertex\n * AI — with a per-picture provider rate and a per-picture billed rate, and the\n * token rates the same request's prompt and thinking are billed at. Kept\n * apart from `AI_MODEL_CATALOG` because nothing that lists text models — the\n * model selector, the routing table, the allotments' allowlists — may ever\n * offer one of these for a conversation.\n *\n * ## The rates\n *\n * Google prices an output picture as image-output tokens: Gemini 3.1 Flash\n * Image writes a 1K picture as 1,120 tokens at $60 per million on the global\n * endpoint, so $0.0672 a picture. Its prompt is input tokens ($0.50 per\n * million) and its thinking is text output ($3.00 per million). Every figure\n * here is the global endpoint's list price, read from Google's Vertex AI\n * pricing page on 2026-10-06; a regional endpoint lists 10% higher, so a\n * deployment that moves off `global` reads its cost low by that much.\n *\n * Every billed figure is the provider figure at `AI_BILLED_MARKUP`:\n * the same ratio the balanced text tier is billed at over its provider rate\n * (Claude Sonnet 5, $3/$15 billed over $2/$10), so a credit buys the same\n * share of margin whether it is spent on words or on pictures. A picture is\n * NEVER billed at provider cost; `billed-rate-is-not-provider-cost.spec.ts`\n * refuses a row that is.\n *\n * A credit is $0.001 of billed spend (`ASSIST_CREDIT_COST_USD`), so the\n * default model's picture is 101 credits before its prompt and thinking.\n *\n * Every per-picture figure on a row is the 1K picture's. A 512 px picture —\n * what the Free plan makes — is written as fewer image tokens at the same\n * per-token rate (`AI_IMAGE_SIZE_OUTPUT_TOKENS`), so it is priced at the 1K\n * figure scaled by its share of tokens: $0.04482 from the provider and\n * $0.06723 billed on the default model, before its prompt and thinking.\n */\nexport interface AiImageCatalogEntry extends AiCatalogRates {\n id: string\n /** The image provider the model is served by. */\n provider: string\n label: string\n /** What the provider bills us for one generated picture. */\n providerUsdPerImage: number\n /** What one picture draws from a customer's credits. */\n billedUsdPerImage: number\n}\n\n/**\n * An image row's four figures from Google's list prices — USD per output\n * picture, and USD per million input and text-output tokens — each billed at\n * the platform's markup, `AI_BILLED_MARKUP`, so an image row cannot be written\n * at list by accident.\n */\nexport function aiImageRatesAtMarkup(\n providerUsdPerImage: number,\n inputPerMTok: number,\n outputPerMTok: number,\n): Pick<\n AiImageCatalogEntry,\n 'providerUsdPerImage' | 'billedUsdPerImage' | 'providerRates' | 'billedRates'\n> {\n return {\n providerUsdPerImage,\n billedUsdPerImage: roundUsd(providerUsdPerImage * AI_BILLED_MARKUP),\n ...aiRatesAtMarkup(inputPerMTok, outputPerMTok),\n }\n}\n\n/**\n * The image-output tokens Google writes a picture as, by size (AGL-3602),\n * read from the usage of real Gemini 3.1 Flash Image answers on 2026-10-07:\n * 747 at 512 px and 1,120 at 1K. A row's per-picture figures are the 1K\n * picture's; a 512 px picture is priced at its share of them.\n */\nexport const AI_IMAGE_SIZE_OUTPUT_TOKENS: Readonly<Record<AiImageSize, number>> = {\n '512': 747,\n '1K': 1_120,\n}\n\n/** A 1K per-picture figure at `size`, by the size's share of image tokens. */\nfunction atImageSize(usdAt1K: number, size: AiImageSize | undefined): number {\n const tokens = AI_IMAGE_SIZE_OUTPUT_TOKENS[size ?? '1K'] ?? AI_IMAGE_SIZE_OUTPUT_TOKENS['1K']\n return roundUsd((usdAt1K * tokens) / AI_IMAGE_SIZE_OUTPUT_TOKENS['1K'])\n}\n\n/** The image model a photo is made with when the operator names none. */\nexport const AI_IMAGE_DEFAULT_MODEL = 'gemini-3.1-flash-image'\n\n/** The image provider's id: Google's models on Vertex AI. */\nexport const AI_IMAGE_VERTEX_PROVIDER_ID = 'google-vertex'\n\n/**\n * The image providers' rows on the published subprocessor list, kept apart\n * from `AI_CATALOG_PROVIDERS` for the reason the image catalog is kept apart\n * from the text one: a settings form offers text providers from that list,\n * and an image provider answers no conversation. The host is the adapter's\n * published endpoint (`VERTEX_IMAGE_PUBLISHED_HOST`).\n *\n * Google's row is dated to its change-log entry on `/legal/subprocessors`,\n * published with the Privacy Policy §2 sentence naming Google for image\n * generation (legal v12).\n */\nexport const AI_IMAGE_CATALOG_PROVIDERS: readonly AiCatalogProvider[] = [\n {\n id: AI_IMAGE_VERTEX_PROVIDER_ID,\n label: 'Google Vertex AI',\n subprocessor: {\n entity: 'Google LLC (Google Cloud Vertex AI)',\n // The global endpoint, which Google documents as routing and\n // processing a request anywhere in the world, with no data residency\n // guarantee: Google, not Aglyn, chooses the location.\n region: 'Global — Google selects where requests are processed',\n purpose:\n \"AI image generation: creating images for a customer's media library from a description a user writes\",\n publishedOn: '2026-10-08',\n reason:\n \"Reached through the AI plugin's Vertex AI image adapter (`libs/plugins/ai/src/lib/providers/vertex-image.ts`) by the Media library's Create with AI door (`libs/plugins/ai/src/lib/server/ai-media-image.ts`) for every kind that is not drawn as SVG, as the platform's own service account. Off unless `AI_IMAGE_VERTEX_PROJECT` names a Google Cloud project; the console offers those kinds only where `NEXT_PUBLIC_AI_IMAGE_PHOTOS` is `on`. The request carries the description and the kind's fixed style wording, the shape and fixed settings, and nothing else.\",\n dataReceived:\n \"The description the user writes and the shape requested, and the generated image returned. No account identifiers, email addresses, or other content of the customer's site.\",\n },\n },\n]\n\nexport const AI_IMAGE_MODEL_CATALOG: readonly AiImageCatalogEntry[] = [\n {\n // The default: Google's own replacement for Imagen 4, which Vertex AI\n // discontinued in 2026.\n id: 'gemini-3.1-flash-image',\n provider: AI_IMAGE_VERTEX_PROVIDER_ID,\n label: 'Gemini 3.1 Flash Image',\n ...aiImageRatesAtMarkup(0.0672, 0.5, 3),\n },\n {\n id: 'gemini-3.1-flash-lite-image',\n provider: AI_IMAGE_VERTEX_PROVIDER_ID,\n label: 'Gemini 3.1 Flash-Lite Image',\n ...aiImageRatesAtMarkup(0.0336, 0.25, 1.5),\n },\n {\n id: 'gemini-3-pro-image',\n provider: AI_IMAGE_VERTEX_PROVIDER_ID,\n label: 'Gemini 3 Pro Image',\n ...aiImageRatesAtMarkup(0.1344, 2, 12),\n },\n]\n\n/**\n * The per-picture rates an unknown image model id is priced at: the dearest\n * row, on both rates, for the reason `AI_FALLBACK_RATES` gives — an estimate\n * that errs low is worse than one that errs high.\n */\nexport const AI_IMAGE_FALLBACK_RATES: Pick<\n AiImageCatalogEntry,\n 'providerUsdPerImage' | 'billedUsdPerImage'\n> = {\n providerUsdPerImage: Math.max(...AI_IMAGE_MODEL_CATALOG.map((row) => row.providerUsdPerImage)),\n billedUsdPerImage: Math.max(...AI_IMAGE_MODEL_CATALOG.map((row) => row.billedUsdPerImage)),\n}\n\n/** The image catalog row for a model id, or `undefined` for an unknown id. */\nexport function aiImageCatalogEntry(modelId: string): AiImageCatalogEntry | undefined {\n return AI_IMAGE_MODEL_CATALOG.find((entry) => entry.id === modelId)\n}\n\n/** What the provider charges us for one picture from `modelId`, at `size` (1K when absent). */\nexport function aiImageProviderUsdPerImage(modelId: string, size?: AiImageSize): number {\n return atImageSize((aiImageCatalogEntry(modelId) ?? AI_IMAGE_FALLBACK_RATES).providerUsdPerImage, size)\n}\n\n/** What one picture from `modelId` at `size` (1K when absent) draws from a customer's credits. */\nexport function aiImageBilledUsdPerImage(modelId: string, size?: AiImageSize): number {\n return atImageSize((aiImageCatalogEntry(modelId) ?? AI_IMAGE_FALLBACK_RATES).billedUsdPerImage, size)\n}\n\n/**\n * Usage as the meter prices it: a text exchange's tokens, and, for a door\n * that makes pictures, how many it delivered and at what size (1K when\n * absent). `images` is absent on every text exchange, which prices exactly\n * as it always did.\n */\nexport type AiMeteredUsage = AiUsage & { images?: number; imageSize?: AiImageSize }\n\n/** Pictures on a usage record: whole, finite, non-negative, else 0. */\nfunction aiImageCount(usage: AiMeteredUsage): number {\n const value = Number(usage.images ?? 0)\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0\n}\n\nfunction roundUsd(value: number): number {\n return Math.round(value * 1_000_000) / 1_000_000\n}\n\n/** The dearest known tier, used when a model id is not in the catalog. */\nexport const AI_FALLBACK_RATES: AiTokenRates = aiRatesPerMTok(10, 50)\n\n/**\n * The minimum an unknown model id is assumed to have: the largest one the\n * catalog knows. It errs the way the rate fallback errs — toward the answer\n * that costs more — so an unrecognized id reads as \"this prompt does not\n * cache\" rather than promising a saving nobody measured.\n */\nexport const AI_FALLBACK_CACHE_MIN_TOKENS = 4_096\n\n/** The shortest prefix a model's provider will cache; the dearest assumption for an unknown id. */\nexport function aiModelCacheMinTokens(modelId: string): number {\n return aiCatalogEntry(modelId)?.cacheMinTokens ?? AI_FALLBACK_CACHE_MIN_TOKENS\n}\n\nconst ZERO_RATES = aiRatesPerMTok(0, 0)\n\n/** The catalog row for a model id, or `undefined` for an unknown id. */\nexport function aiCatalogEntry(modelId: string): AiCatalogEntry | undefined {\n return AI_MODEL_CATALOG.find((entry) => entry.id === modelId)\n}\n\nconst isSentinel = (modelId: string): boolean =>\n (Object.values(AI_METER_SENTINELS) as string[]).includes(modelId)\n\n/**\n * What the PROVIDER charges for a model id — our bill. A meter sentinel is\n * free; an image model's prompt and thinking are priced at its image row's\n * token rates (AGL-3602); an unknown id is priced at the dearest tier.\n *\n * The figure every cost and margin surface prices usage at. Answering the\n * customer's question from here would report a margin the platform does not\n * earn, and answering ours from `aiBilledRatesForModel` reports one it does\n * not pay; the two are separate functions so neither is reachable by\n * forgetting which one was meant.\n */\nexport function aiProviderRatesForModel(modelId: string): AiTokenRates {\n if (isSentinel(modelId)) return ZERO_RATES\n return (\n aiCatalogEntry(modelId)?.providerRates ??\n aiImageCatalogEntry(modelId)?.providerRates ??\n AI_FALLBACK_RATES\n )\n}\n\n/**\n * What a CUSTOMER'S CREDITS are charged at for a model id. A meter sentinel\n * is free; an unknown id is priced at the dearest tier.\n *\n * The figure the credit meter, the band and the model selector price usage\n * at. It is at or above `aiProviderRatesForModel` on every row.\n */\nexport function aiBilledRatesForModel(modelId: string): AiTokenRates {\n if (isSentinel(modelId)) return ZERO_RATES\n return (\n aiCatalogEntry(modelId)?.billedRates ??\n aiImageCatalogEntry(modelId)?.billedRates ??\n AI_FALLBACK_RATES\n )\n}\n\nfunction priceUsage(usage: AiUsage, rate: AiTokenRates): number {\n const raw =\n usage.inputTokens * rate.inputPerToken +\n usage.outputTokens * rate.outputPerToken +\n usage.cacheReadTokens * rate.cacheReadPerToken +\n usage.cacheWriteTokens * rate.cacheWritePerToken\n return Math.round(raw * 1_000_000) / 1_000_000\n}\n\n/**\n * What one exchange COST US, at the serving model's provider rates, rounded\n * to 6dp. Real money, and the only figure a margin may be taken against.\n */\nexport function estimateAiProviderCostUsd(usage: AiMeteredUsage, modelId: string): number {\n return roundUsd(\n priceUsage(usage, aiProviderRatesForModel(modelId)) +\n aiImageCount(usage) * aiImageProviderUsdPerImage(modelId, usage.imageSize),\n )\n}\n\n/**\n * What one exchange DRAWS FROM A CUSTOMER, at the serving model's billed\n * rates, rounded to 6dp — the figure `assistCreditsFromUsd` turns into\n * credits. At or above `estimateAiProviderCostUsd` for the same exchange.\n */\nexport function estimateAiBilledUsd(usage: AiMeteredUsage, modelId: string): number {\n return roundUsd(\n priceUsage(usage, aiBilledRatesForModel(modelId)) +\n aiImageCount(usage) * aiImageBilledUsdPerImage(modelId, usage.imageSize),\n )\n}\n\n/** Every catalog id a provider serves. */\nexport function aiModelIdsForProvider(providerId: string): string[] {\n return AI_MODEL_CATALOG.filter((entry) => entry.provider === providerId).map(\n (entry) => entry.id,\n )\n}\n\n/**\n * What a step asks for, and what the routing table answers with. Every\n * door names its kind here rather than a model, so a model swap — an\n * incident override, an org's own setting, a cheaper model for a cheap\n * step — is one row rather than a search through the doors (AGL-2937).\n */\nexport type AiStepKind =\n | 'assist.chat'\n | 'copy.element'\n | 'copy.section'\n | 'copy.blog'\n | 'generate.section'\n | 'job.component'\n | 'job.crm'\n | 'job.edit'\n | 'job.experiment'\n | 'job.form'\n | 'job.insight'\n | 'job.layout'\n | 'job.logic'\n | 'job.template'\n | 'job.page'\n | 'job.products'\n | 'job.seo'\n | 'job.text'\n | 'job.theme'\n | 'job.plan'\n | 'job.email'\n | 'job.campaign'\n | 'job.workflow'\n\n/** The tier each step kind is served from when no setting overrides it. */\nexport const AI_STEP_TIERS: Record<AiStepKind, AiCatalogEntry['tier']> = {\n 'assist.chat': 'balanced',\n 'copy.element': 'fast',\n 'copy.section': 'balanced',\n 'copy.blog': 'balanced',\n 'generate.section': 'balanced',\n // A reusable component is one structured tree with the typed props it\n // declares, and choosing what becomes a prop is the judgment the step sells.\n 'job.component': 'balanced',\n // CRM by AI (AGL-2917): a record's summary and next step, a deal's stage, a\n // lead's standing, a one-to-one email draft and an import's column matches.\n // Short answers through a strict tool, held to the facts the CRM reports\n // about the record the member opened.\n 'job.crm': 'fast',\n // A change to a page or a layout the site has (AGL-3616): the Assist edit\n // rung's protocol run as a job, held to the closed world of the document's\n // own element ids and to the palette validators. Finding the element a\n // request means, and changing only it, is the judgment; the chat door that\n // proposes the same edits runs on this tier.\n 'job.edit': 'balanced',\n // A/B tests by AI (AGL-2914): variants of one piece of copy, and a\n // result put into words for a verdict code already reached. Short answers\n // through a strict tool, but the judgment a variant sells — which ONE idea\n // to vary so the result says something — is the balanced tier's.\n 'job.experiment': 'balanced',\n // A form is one small tree held to the building rules and to the contract\n // its submissions are read by; a re-ask costs more than the tier saves.\n 'job.form': 'balanced',\n // An insight (AGL-2915) chooses which figures answer a question and says\n // what they show; the trace holds every number, and the judgment of what is\n // worth saying is what the fast tier does worse.\n 'job.insight': 'balanced',\n // A layout or a page template is one structured tree held to every\n // building rule; the fast tier re-asks more than it saves.\n 'job.layout': 'balanced',\n // A site function or variable (AGL-3603): a small structured answer held\n // to the evaluator's grammar, the names it may read and a first run, whose\n // judgment is turning a description into conditions and arithmetic; the\n // fast tier re-asks more than it saves.\n 'job.logic': 'balanced',\n 'job.template': 'balanced',\n // A page section (AGL-2907) is held to the whole page's building rules on\n // every pass, for the same reason.\n 'job.page': 'balanced',\n // A product's copy, a store's first products, or its categories and\n // discounts (AGL-2916): judgment about what a photo shows and what a shopper\n // needs, held to storefront claim rules a re-ask costs more to meet than the\n // tier saves.\n 'job.products': 'balanced',\n // SEO fields, alt text and an audit's fixes (AGL-2910): short answers\n // through a strict tool, held to a length and to the page's own text.\n 'job.seo': 'fast',\n 'job.text': 'balanced',\n // A theme is one structured answer over a small, fixed control set; the\n // judgment is in the color choices, which the fast tier makes worse.\n 'job.theme': 'balanced',\n 'job.plan': 'balanced',\n // An email is one tree held to every building rule, with subject lines\n // whose judgment is the point of asking; the fast tier re-asks more than it\n // saves. A campaign is the same email, drafted into a campaign.\n 'job.email': 'balanced',\n 'job.campaign': 'balanced',\n // An automation is a small structured answer whose judgment is choosing the\n // trigger and steps a description means, and an explanation reads one\n // closely; the fast tier re-asks more than it saves on both (AGL-2919).\n 'job.workflow': 'balanced',\n}\n\n/** The first current catalog model of a tier on a provider, or the provider's first current model. */\nexport function aiDefaultModelFor(providerId: string, tier: AiCatalogEntry['tier']): string | undefined {\n const onProvider = AI_MODEL_CATALOG.filter((entry) => entry.provider === providerId && !entry.successor)\n return (onProvider.find((entry) => entry.tier === tier) ?? onProvider[0])?.id\n}\n"],"names":["aiRatesPerMTok","inputPerMTok","outputPerMTok","inputPerToken","outputPerToken","cacheReadPerToken","cacheWritePerToken","aiRatesAtList","providerRates","billedRates","AI_BILLED_MARKUP","aiRatesMarkedUp","rates","aiRatesAtMarkup","aiRatesAtListWithCacheRead","cacheReadPerMTok","AI_METER_SENTINELS","docsRetrieval","docsLinks","answerCache","ANTHROPIC","OPENAI_COMPATIBLE","ANTHROPIC_DATA_RECEIVED_LEAD","ANTHROPIC_DATA_RECEIVED_CRM_DRAFTS_AND_IMPORTS","ANTHROPIC_CRM_ASSISTANCE_WHOLE_RECORD","record","never","anthropicDataReceived","crm","AI_CATALOG_PROVIDERS","id","label","subprocessor","entity","region","purpose","publishedOn","reason","dataReceived","anthropicCapabilities","streaming","tools","thinking","promptCache","vision","AI_MODEL_CATALOG","provider","capabilities","tier","cacheMinTokens","forcedToolChoice","successor","aiImageRatesAtMarkup","providerUsdPerImage","billedUsdPerImage","roundUsd","AI_IMAGE_SIZE_OUTPUT_TOKENS","atImageSize","usdAt1K","size","tokens","AI_IMAGE_DEFAULT_MODEL","AI_IMAGE_VERTEX_PROVIDER_ID","AI_IMAGE_CATALOG_PROVIDERS","AI_IMAGE_MODEL_CATALOG","AI_IMAGE_FALLBACK_RATES","Math","max","map","row","aiImageCatalogEntry","modelId","find","entry","aiImageProviderUsdPerImage","aiImageBilledUsdPerImage","aiImageCount","usage","value","Number","images","isFinite","floor","round","AI_FALLBACK_RATES","AI_FALLBACK_CACHE_MIN_TOKENS","aiModelCacheMinTokens","aiCatalogEntry","ZERO_RATES","isSentinel","Object","values","includes","aiProviderRatesForModel","aiBilledRatesForModel","priceUsage","rate","raw","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","estimateAiProviderCostUsd","imageSize","estimateAiBilledUsd","aiModelIdsForProvider","providerId","filter","AI_STEP_TIERS","aiDefaultModelFor","onProvider"],"mappings":"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"}
@@ -41,7 +41,11 @@ import { UpstreamServiceError } from '@aglyn/shared-util-errors';
41
41
  * A `tools` entry is sent strict, with additional properties forbidden on
42
42
  * its schema, so a tool call's `input` validates against the schema
43
43
  * exactly. Tool choice stays automatic — forced tool use is not portable
44
- * across models — and the caller's system text names the tool it wants. An
44
+ * across models — and the caller's own words name the tool it wants. A door
45
+ * that sends several tools may also name the one a call is for
46
+ * (`toolChoice`), which an adapter forces only on a model whose descriptor
47
+ * says it takes a forced choice (`forcedToolChoice`) and only with thinking
48
+ * off, and sends as automatic everywhere else (AGL-4076). An
45
49
  * adapter delivers `input` as an object; a stream assembles it from
46
50
  * fragments and parses the whole once, at the block's end. Nothing
47
51
  * string-matches the serialized form: models differ in how they escape it.
@@ -234,6 +238,18 @@ export interface AiRequestBase {
234
238
  * It changes no answer, so a replay key does not read it.
235
239
  */
236
240
  cacheConversation?: boolean;
241
+ /**
242
+ * The tool this call is for, by name: one of `tools` (AGL-4076). A step
243
+ * whose calls each want a different tool sends every one of them on every
244
+ * call, so the tools that open the cached prefix are the same bytes each
245
+ * time and the later calls read the prefix the first one wrote; this names
246
+ * the one wanted now. A tool choice sits outside the cached prefix, so
247
+ * naming a different one costs no cache read. The adapter forces it only
248
+ * where the model's descriptor says `forcedToolChoice` and the request
249
+ * thinks with thinking off, which a forced choice requires; everywhere else
250
+ * the choice stays automatic and the door's own words name the tool.
251
+ */
252
+ toolChoice?: string;
237
253
  }
238
254
  /** The request an adapter receives: the runtime has already validated it. */
239
255
  export interface AiProviderRequest extends AiRequestBase {
@@ -463,6 +479,13 @@ export interface AiModelDescriptor {
463
479
  * sent an image it did not say it can read.
464
480
  */
465
481
  vision?: boolean;
482
+ /**
483
+ * Whether the model takes a forced tool choice (AGL-4076): a request that
484
+ * names the one tool it must call. Absent reads as no, so a request that
485
+ * names its tool is sent an automatic choice instead. Claude Opus 5.5 and
486
+ * Claude Sonnet 5.5 refuse a forced choice outright.
487
+ */
488
+ forcedToolChoice?: boolean;
466
489
  };
467
490
  }
468
491
  /** The one sentence a customer may read when a provider fails. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/providers/contract.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 { UpstreamServiceError } from '@aglyn/shared-util-errors'\n\n/**\n * The provider contract (AGL-2939): the one shape every AI door speaks, and\n * the one thing an adapter has to implement. The runtime, the gate, the\n * jobs, the doctrine and the usage code import this and never a vendor\n * URL, header or model literal — Anthropic is one adapter behind it, and\n * an OpenAI-compatible endpoint is the second, which is what proves the\n * seam holds.\n *\n * ── Prompt caching, by construction ───────────────────────────────────────\n *\n * `system` is an ordered list of blocks, and caching is a PREFIX match: a\n * breakpoint covers every block before it, and one byte that differs per\n * request anywhere inside that prefix gives every distinct value its own\n * copy of it. The AGL-2352 rule is that no per-org byte may sit inside a\n * cached prefix — a white-label brand name interpolated into a cached block\n * multiplies the prefix count by the number of brands, each written off its\n * own traffic and expired before it is read. So a block marked `volatile`\n * may not sit at or before the last `cacheBreakpoint`, and the runtime\n * refuses the request before a byte leaves the process. A provider with no\n * cache honors the order and ignores the markers.\n *\n * ── Structured output ─────────────────────────────────────────────────────\n *\n * A `tools` entry is sent strict, with additional properties forbidden on\n * its schema, so a tool call's `input` validates against the schema\n * exactly. Tool choice stays automatic — forced tool use is not portable\n * across models — and the caller's system text names the tool it wants. An\n * adapter delivers `input` as an object; a stream assembles it from\n * fragments and parses the whole once, at the block's end. Nothing\n * string-matches the serialized form: models differ in how they escape it.\n * A provider bounds how much strict schema one request may carry, and an\n * adapter declares those bounds as `toolSchemaLimits`: three bounds a vendor\n * publishes and counts, and a fourth, the size of the grammar the schemas\n * compile to, that none publishes and only a live request settles.\n *\n * ── Thinking ──────────────────────────────────────────────────────────────\n *\n * `thinking: 'off'` and `'adaptive'` are sent when the provider has the\n * control; an omitted field sends NOTHING, leaving the model's own default\n * in force. The omission is deliberate rather than a gap: some models\n * reject an explicit setting, and the copy assistant serves its element\n * mode on such a model. `effort` rides the same rule.\n *\n * ── Pictures ──────────────────────────────────────────────────────────────\n *\n * A user turn may carry pictures beside its text (AGL-2916): `content` is\n * then an ordered list of parts, and a turn with nothing but text stays a\n * string, which every adapter passes through as it always has. A picture is\n * base64 bytes of one of `AI_IMAGE_MEDIA_TYPES`, never a URL, so no provider\n * fetches anything on the platform's behalf. The runtime refuses a picture\n * on an assistant turn, a picture for a model whose descriptor does not say\n * `vision`, and a picture past the size or count bounds, all before any\n * network is touched. Each adapter maps the parts onto its vendor's shape.\n */\n\n/**\n * One system block. `cacheBreakpoint` marks the end of a cacheable prefix;\n * `volatile` declares that the text carries a per-request or per-org byte\n * and therefore may never sit inside one.\n *\n * `site` declares a block that describes ONE site (AGL-3661): its business\n * profile, status and remembered preferences. As a breakpoint it is cached\n * once per site, so it must close the cached span — every platform-wide\n * block sits ahead of it, where every workspace still shares one entry —\n * and the runtime refuses a request that puts another breakpoint after it.\n */\nexport interface AiSystemBlock {\n text: string\n cacheBreakpoint?: true\n volatile?: true\n site?: true\n}\n\n/** A structured-output tool. `strict` is required, not optional. */\nexport interface AiTool {\n name: string\n description: string\n /** A JSON schema of `type: 'object'`; `additionalProperties` is forced off. */\n inputSchema: Record<string, unknown>\n strict: true\n}\n\n/**\n * How much of a request's strict tools a provider can compile (AGL-3096). A\n * provider that constrains decoding to a tool's schema compiles every strict\n * schema of a request together, before the model runs, and refuses a request\n * whose schemas are past its bounds. That refusal is permanent: the schema is\n * the same on every retry. Each bound is a TOTAL over every strict tool one\n * request sends, as `aiToolSchemaCounts` counts them. A bound left out is one\n * the provider does not state.\n *\n * A provider may also refuse a request whose schemas compile to too large a\n * grammar, which none of the published counts can see: a request counting 1\n * strict tool, 0 optional parameters and 1 union was refused for exactly that\n * (AGL-3096). `compiledSchemaBytes` stands in for it, on live evidence.\n */\nexport interface AiToolSchemaLimits {\n /** The most strict tools one request may send. */\n strictTools?: number\n /** The most properties, across every strict schema, left out of their object's `required`. */\n optionalParameters?: number\n /** The most schemas, across every strict schema, that are a union: an `anyOf` or `oneOf`, or a list of types. */\n unionParameters?: number\n /**\n * The most bytes of schema a request's grammar may be compiled from, as\n * `aiToolSchemaCompiledBytes` measures them.\n *\n * Unlike the bounds above, this one is EMPIRICAL. A provider refuses a\n * request whose compiled grammar is too large without publishing the size,\n * so the number here is not read off a vendor's page: it is the largest a\n * live request has been watched to carry, and every number above it is\n * unknown rather than safe. Raising it on an offline measurement alone\n * puts the guard back where it could not see the break.\n */\n compiledSchemaBytes?: number\n}\n\n/** A request's strict tools, counted on the measures `AiToolSchemaLimits` bounds. */\nexport type AiToolSchemaCounts = Required<AiToolSchemaLimits>\n\n/**\n * One schema as a grammar compiler reads it: every local `$ref` written out\n * at each place it is used, and every description, title and `$defs` dropped,\n * since none of them constrains a single token. A schema that refers to\n * itself is written out once around and then left as the reference it is.\n */\nfunction compiledForm(node: unknown, root: Record<string, unknown>, resolving: ReadonlySet<string>): unknown {\n if (Array.isArray(node)) return node.map((entry) => compiledForm(entry, root, resolving))\n if (!isSchemaObject(node)) return node\n const ref = node['$ref']\n if (typeof ref === 'string') {\n if (resolving.has(ref)) return { $ref: ref }\n return compiledForm(localSchemaAt(root, ref), root, new Set([...resolving, ref]))\n }\n const written: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(node)) {\n if (key === 'description' || key === 'title' || key === '$defs') continue\n written[key] = compiledForm(value, root, resolving)\n }\n return written\n}\n\n/**\n * The bytes of schema a request's grammar is compiled from, over all its\n * strict tools. It is a stand-in for the size of the grammar itself, which\n * nothing outside the provider can measure, and it errs toward more: a `$ref`\n * counts at each place it is used, because a compiler that writes it out\n * there pays for it there.\n */\nexport function aiToolSchemaCompiledBytes(tools: readonly AiTool[]): number {\n return tools.reduce(\n (bytes, tool) => bytes + JSON.stringify(compiledForm(tool.inputSchema, tool.inputSchema, new Set())).length,\n 0,\n )\n}\n\nfunction isSchemaObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** The schema a local `$ref` (`#/…`) points at, or `undefined`. */\nfunction localSchemaAt(root: Record<string, unknown>, ref: string): unknown {\n if (!ref.startsWith('#/')) return undefined\n return ref\n .slice(2)\n .split('/')\n .map((key) => key.replace(/~1/g, '/').replace(/~0/g, '~'))\n .reduce<unknown>((node, key) => (isSchemaObject(node) ? node[key] : undefined), root)\n}\n\nfunction countSchema(\n node: unknown,\n root: Record<string, unknown>,\n counts: AiToolSchemaCounts,\n resolving: ReadonlySet<string>,\n): void {\n if (!isSchemaObject(node)) return\n if (Array.isArray(node['anyOf']) || Array.isArray(node['oneOf']) || Array.isArray(node['type'])) {\n counts.unionParameters += 1\n }\n const ref = node['$ref']\n if (typeof ref === 'string' && !resolving.has(ref)) {\n countSchema(localSchemaAt(root, ref), root, counts, new Set([...resolving, ref]))\n }\n const properties = node['properties']\n if (isSchemaObject(properties)) {\n const required = Array.isArray(node['required']) ? node['required'] : []\n for (const [key, property] of Object.entries(properties)) {\n if (!required.includes(key)) counts.optionalParameters += 1\n countSchema(property, root, counts, resolving)\n }\n }\n const items = node['items']\n for (const item of Array.isArray(items) ? items : [items]) countSchema(item, root, counts, resolving)\n for (const key of ['anyOf', 'allOf', 'oneOf']) {\n const branches = node[key]\n if (Array.isArray(branches)) for (const branch of branches) countSchema(branch, root, counts, resolving)\n }\n}\n\n/**\n * The strict tools of one request, counted on each measure a provider bounds.\n * Every tool the contract sends is strict. Each schema is walked whole: every\n * property, every `items`, every branch of an `anyOf`, `allOf` or `oneOf`, and\n * a local `$ref` at each place it is used. A union nested in an array's items\n * or in another union's branch counts on its own, as it does for a provider\n * that compiles it; a `$ref` counted where it is used errs toward more.\n */\nexport function aiToolSchemaCounts(tools: readonly AiTool[]): AiToolSchemaCounts {\n const counts: AiToolSchemaCounts = {\n strictTools: tools.length,\n optionalParameters: 0,\n unionParameters: 0,\n compiledSchemaBytes: aiToolSchemaCompiledBytes(tools),\n }\n for (const tool of tools) countSchema(tool.inputSchema, tool.inputSchema, counts, new Set())\n return counts\n}\n\n/**\n * Each bound, as one, as many, and where its number comes from — a published\n * bound reads as the provider's own, a measured one as what has been proved.\n */\nconst TOOL_SCHEMA_MEASURES: ReadonlyArray<[keyof AiToolSchemaLimits, string, string, string]> = [\n ['strictTools', 'strict tool', 'strict tools', 'the provider compiles'],\n ['optionalParameters', 'optional parameter', 'optional parameters', 'the provider compiles'],\n ['unionParameters', 'union-typed parameter', 'union-typed parameters', 'the provider compiles'],\n ['compiledSchemaBytes', 'byte of compiled schema', 'bytes of compiled schema', 'a live request has proved'],\n]\n\n/** Each bound a request's tools are past, as a sentence; empty when every one holds. */\nexport function aiToolSchemaBreaches(\n tools: readonly AiTool[],\n limits: AiToolSchemaLimits | undefined,\n): string[] {\n if (!limits) return []\n const counts = aiToolSchemaCounts(tools)\n return TOOL_SCHEMA_MEASURES.flatMap(([measure, one, many, source]) => {\n const limit = limits[measure]\n const count = counts[measure]\n return limit !== undefined && count > limit\n ? [`${count} ${count === 1 ? one : many} in one request, over the ${limit} ${source}`]\n : []\n })\n}\n\nexport type AiEffort = 'low' | 'medium' | 'high'\nexport type AiThinking = 'off' | 'adaptive'\n\n/**\n * The picture formats a request may carry (AGL-2916): the four every model\n * the catalog marks as reading images accepts. A door converts anything else\n * — an AVIF, an SVG — before it attaches the picture, or attaches none.\n */\nexport const AI_IMAGE_MEDIA_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'] as const\nexport type AiImageMediaType = (typeof AI_IMAGE_MEDIA_TYPES)[number]\n\n/**\n * The largest picture one part may carry, in decoded bytes: the ceiling the\n * providers behind the contract accept for one image. A door that attaches a\n * picture resizes it far below this; the runtime refuses anything above it\n * before a byte leaves the process, rather than letting the provider do it\n * after the request was paid for in time.\n */\nexport const AI_IMAGE_MAX_BYTES = 5 * 1024 * 1024\n\n/** The most pictures one request may carry. */\nexport const AI_REQUEST_MAX_IMAGES = 8\n\n/** Text in a message made of parts. */\nexport interface AiTextPart {\n type: 'text'\n text: string\n}\n\n/**\n * A picture in a message (AGL-2916): its bytes, base64-encoded, and their\n * format. Only a USER turn carries one, and only to a model whose descriptor\n * says it reads images; the runtime refuses any other request before a byte\n * leaves the process, so a door cannot send a picture by accident to a model\n * that would reject it, or to a turn the model wrote.\n */\nexport interface AiImagePart {\n type: 'image'\n mediaType: AiImageMediaType\n /** The picture's bytes, base64-encoded, with no `data:` prefix. */\n data: string\n}\n\nexport type AiMessagePart = AiTextPart | AiImagePart\n\nexport interface AiMessage {\n role: 'user' | 'assistant'\n /**\n * The turn's text, or its parts in order — text and pictures — for a door\n * that shows the model an image. A door with nothing but text sends a\n * string, which every adapter passes through as it always has.\n */\n content: string | readonly AiMessagePart[]\n}\n\n/** A message's text: its parts' text joined, with every picture left out. */\nexport function aiMessageText(message: Pick<AiMessage, 'content'>): string {\n if (typeof message.content === 'string') return message.content\n return message.content\n .filter((part): part is AiTextPart => part.type === 'text')\n .map((part) => part.text)\n .join('\\n\\n')\n}\n\n/** Every picture a list of messages carries, in order. */\nexport function aiMessageImages(messages: readonly Pick<AiMessage, 'content'>[]): AiImagePart[] {\n return messages.flatMap((message) =>\n typeof message.content === 'string'\n ? []\n : message.content.filter((part): part is AiImagePart => part.type === 'image'),\n )\n}\n\n/** The decoded size of a base64 string, without decoding it. */\nexport function aiBase64Bytes(data: string): number {\n const padding = data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0\n return Math.max(0, Math.floor((data.length * 3) / 4) - padding)\n}\n\n/** What every door hands the runtime; `provider` and `model` resolve there. */\nexport interface AiRequestBase {\n model: string\n system: AiSystemBlock[]\n messages: AiMessage[]\n tools?: AiTool[]\n maxTokens: number\n effort?: AiEffort\n thinking?: AiThinking\n signal?: AbortSignal\n /**\n * How long a cache entry the SHARED breakpoints write should live, where\n * the provider offers a choice: the provider's default (five minutes on the\n * Messages API) when absent. A step a site job runs asks for `1h` through\n * its routing row (`AiRoutingRow.cacheTtl`, AGL-3830), in production too,\n * and a development live run asks for it on every request\n * (`runtime/ai-dev-env.ts`). A job's own markers keep five minutes whatever\n * this says (`aiCacheMarkerTtls`, AGL-4050). It changes no answer, only what\n * a request within the hour pays for the prefix, so a replay key does not\n * read it.\n */\n cacheTtl?: '1h'\n /**\n * Cache the conversation through its last message as well as the system\n * blocks (AGL-3809), for a request the door expects to continue: a\n * generation that looks the site up or is asked again re-sends its whole\n * first turn, which without this is billed at the full input rate on every\n * continuation. One more breakpoint, on the last message, counted against\n * the same limit as the system's; a provider without a cache ignores it.\n * It changes no answer, so a replay key does not read it.\n */\n cacheConversation?: boolean\n}\n\n/** The request an adapter receives: the runtime has already validated it. */\nexport interface AiProviderRequest extends AiRequestBase {\n /** The provider's own key, resolved by the runtime from the environment or the org's setting. */\n apiKey: string\n}\n\n/** How long one cache marker's entry lives: the provider's five minutes, or the hour. */\nexport type AiCacheTtl = '5m' | '1h'\n\n/**\n * Who reads a cache marker's entry (AGL-4050). A `shared` marker closes a\n * span of the platform's own prompt (the tools, the doctrine, a door's\n * rules), the same bytes for every org and every job, so the next job can\n * read what this one wrote: it lives as long as the request's `cacheTtl`\n * asks. A `job` marker closes bytes only one job reads: a site's block\n * (`site`) or the request's own conversation (`cacheConversation`). That job\n * reads it again within minutes and no other job ever does, so it keeps the\n * provider's five minutes whatever the route asks; at the hour, its 2x write\n * would never be read back.\n */\nexport type AiCacheScope = 'shared' | 'job'\n\n/** A system block's marker scope: a site's block is one job's, every other block the platform's. */\nexport function aiCacheScopeOf(block: Pick<AiSystemBlock, 'site'>): AiCacheScope {\n return block.site ? 'job' : 'shared'\n}\n\n/**\n * The lifetime of every cache marker a request carries (AGL-4050), in prefix\n * order: one per system block, `null` where the block marks none, then the\n * conversation's, `null` where it is not cached. Tools carry no marker of\n * their own; they sit ahead of the first system marker and live as long as it.\n *\n * A provider refuses a request whose longer-lived entry comes after a\n * shorter-lived one, so once a marker is five minutes every marker after it\n * is too. A shared marker behind a job's is downgraded, never sent at the\n * hour. The runtime already keeps a site's block last among the system\n * breakpoints, so a door's request never meets the downgrade; it is the\n * adapter's guarantee all the same.\n */\nexport function aiCacheMarkerTtls(\n request: Pick<AiRequestBase, 'system' | 'messages' | 'cacheTtl' | 'cacheConversation'>,\n): { system: Array<AiCacheTtl | null>; conversation: AiCacheTtl | null } {\n let ceiling: AiCacheTtl = request.cacheTtl === '1h' ? '1h' : '5m'\n const lifetime = (scope: AiCacheScope): AiCacheTtl => {\n if (scope === 'job') ceiling = '5m'\n return ceiling\n }\n const system = request.system.map((block) => (block.cacheBreakpoint ? lifetime(aiCacheScopeOf(block)) : null))\n const conversation = request.cacheConversation && request.messages.length > 0 ? lifetime('job') : null\n return { system, conversation }\n}\n\n/**\n * Token usage in the meter's shape. Cache reads and writes are separate\n * because they are priced separately; a provider with no cache reports 0.\n */\nexport interface AiUsage {\n inputTokens: number\n outputTokens: number\n cacheReadTokens: number\n cacheWriteTokens: number\n /**\n * How many of `cacheWriteTokens` went to an hour-long cache entry\n * (AGL-3830), where the provider breaks the writes down; absent where none\n * did or the provider does not say.\n *\n * Inside `cacheWriteTokens`, never beside it, like `thinkingTokens` below.\n * An hour-long write is billed at twice the input price where a rate\n * table's `cacheWritePerToken` is the five-minute 1.25 times, so a price\n * reads this to add the difference (`aiCacheWriteHourSurchargeUsd`).\n */\n cacheWriteHourTokens?: number\n /**\n * How many of `outputTokens` the model spent thinking, where the provider\n * reports it; absent where it does not (AGL-3143).\n *\n * Inside `outputTokens`, never beside it: a rate table prices the four\n * figures above and would charge these twice if it read this one.\n *\n * It is here because it is the figure that says where a generation's\n * output went. A tool call that comes back far smaller than the tokens it\n * cost has three shapes, and only this separates the first from the other\n * two: the model thought and then answered briefly, which is a model\n * working as asked; a runaway string the partial parse discarded; or\n * decoding that wrote nothing usable. Kept on every answer, because a\n * generation burns its budget thinking without ever reaching its ceiling,\n * and `rawOutput` is only there when it does.\n */\n thinkingTokens?: number\n}\n\nexport interface AiToolUse {\n name: string\n input: Record<string, unknown>\n}\n\n/** A completed, non-streaming answer. */\nexport interface AiCompletion {\n kind: 'completion'\n /** Every text block, joined. */\n text: string\n toolUse: AiToolUse[]\n usage: AiUsage\n /**\n * The exchange at the serving model's BILLED rates — what a customer's\n * credits are drawn from, and never a margin's cost side (AGL-3015). What\n * the same exchange cost us is `estimateAiProviderCostUsd`, recorded by\n * the meter; a result travels no further than the credit path, so it\n * carries the one figure that path needs.\n */\n estCostUsd: number\n stopReason: string | null\n /**\n * WHERE THE OUTPUT WENT (AGL-3143). The provider's own output for this\n * call, as it answered it, kept ONLY when the call stopped at its ceiling\n * and cut to `AI_RAW_OUTPUT_MAX_CHARS`.\n *\n * A tool call truncated mid-answer reaches `toolUse` as whatever the\n * provider could still parse, and the parsed input alone cannot say where\n * the tokens went: a runaway string the partial parse discarded and a\n * decoding stall that wrote nothing usable both leave a small input\n * against a spent ceiling. This is what tells them apart. These are the\n * model's own words, so they belong to the trace alone: never to a reader,\n * and never to a log, which carries the figures that separate the two\n * shapes — `outputTokens`, `parsedChars`, `rawChars`.\n */\n rawOutput?: string\n}\n\n/**\n * Whether a model call stopped because it reached the output ceiling it was\n * asked for (AGL-3042). `max_tokens` is the contract's word, and the\n * OpenAI-compatible adapter maps its endpoint's `length` onto it; `length` is\n * read as well, for an adapter that passes its provider's own word through.\n */\nexport function aiStoppedAtCeiling(stopReason: string | null): boolean {\n return stopReason === 'max_tokens' || stopReason === 'length'\n}\n\n/** The most characters of raw output a call cut off at its ceiling keeps. */\nexport const AI_RAW_OUTPUT_MAX_CHARS = 20_000\n\n/**\n * A cut-off call's output as the trace keeps it: what the provider answered,\n * serialized where it is not already text, and cut to the bound with an\n * ellipsis so a reader can tell a cut trace from a short answer.\n */\nexport function aiRawOutputOf(output: unknown): string {\n const text = typeof output === 'string' ? output : (JSON.stringify(output) ?? '')\n return text.length > AI_RAW_OUTPUT_MAX_CHARS ? `${text.slice(0, AI_RAW_OUTPUT_MAX_CHARS)}…` : text\n}\n\n/**\n * WHERE THE OUTPUT WENT, as a log may carry it (AGL-3143): the figures that\n * separate the shapes a call cut off at its ceiling comes in, and nothing\n * else.\n *\n * Thinking accounts for the spend first, because the thinking budget is\n * drawn from the same ceiling as the answer: a call can spend nearly all of\n * one thinking and still be cut off with a correct, tiny tool input, which\n * is a ceiling to raise rather than a decoder to fix. What thinking does not\n * account for is told apart by how much was written — long, and a runaway\n * string the partial parse discarded; short, and decoding wrote nothing\n * usable.\n *\n * It takes the raw output rather than a count on purpose. The bytes are the\n * model's own words and therefore the site's content, so they belong to the\n * trace alone and never to a log; passing them through one function that can\n * only ever return their LENGTH is what holds that rule at every door,\n * instead of asking each one to remember it.\n */\nexport function aiCutOffFigures(call: {\n usage: AiUsage\n /** The tool input that still parsed, or null where none did. */\n parsed: unknown\n rawOutput: string | null | undefined\n}): {\n outputTokens: number\n thinkingTokens: number | null\n parsedChars: number\n rawChars: number | null\n} {\n const parsed =\n call.parsed === null || call.parsed === undefined ? '' : (JSON.stringify(call.parsed) ?? '')\n return {\n outputTokens: call.usage.outputTokens,\n thinkingTokens: call.usage.thinkingTokens ?? null,\n parsedChars: parsed.length,\n rawChars: call.rawOutput?.length ?? null,\n }\n}\n\n/**\n * The model declined. Tokens were spent, so the usage is real and the\n * caller meters it; the text is whatever partial answer preceded the stop,\n * usually nothing. Every adapter maps its provider's own decline signal —\n * a refusal stop, a content filter — onto this one kind.\n */\nexport interface AiRefusal {\n kind: 'refusal'\n text: string\n usage: AiUsage\n estCostUsd: number\n stopReason: 'refusal'\n}\n\nexport type AiResult = AiCompletion | AiRefusal\n\nexport type AiStreamEvent =\n | { type: 'delta'; text: string }\n | { type: 'tool'; name: string; input: Record<string, unknown> }\n /**\n * The provider's in-stream error. The stream is still open and ends with\n * `done`, so the caller decides what the reader is told; the provider's\n * own words went to the log, not here.\n */\n | { type: 'error'; retryable: boolean }\n | {\n type: 'done'\n usage: AiUsage\n estCostUsd: number\n stopReason: string | null\n /**\n * WHERE THE OUTPUT WENT, for a stream (AGL-3143): the bytes the\n * stream's tool calls were written in, kept ONLY when the stream\n * stopped at its ceiling and cut to `AI_RAW_OUTPUT_MAX_CHARS`. The\n * streaming counterpart of `AiCompletion.rawOutput`.\n *\n * A `tool` event carries what `aiToolInputOf` could still parse, which\n * for a call cut mid-answer is a prefix of the object or nothing at\n * all; the fragments those events were assembled from are the only\n * record of what the model actually emitted, and without them a\n * runaway string and a decoding stall look identical from the\n * outside. These are the model's own words, so they belong to the\n * trace alone: never to a reader, and never to a log, which carries\n * the figures that separate the two shapes.\n */\n rawOutput?: string\n }\n\n/** What a provider says about one of its models; the catalog carries both rates. */\nexport interface AiModelDescriptor {\n id: string\n /** The provider id the model is served by. */\n provider: string\n label: string\n capabilities: {\n streaming: boolean\n tools: boolean\n /** Whether the provider accepts an explicit `thinking` setting for this model. */\n thinking: boolean\n /** Whether the provider caches a system prefix for this model. */\n promptCache: boolean\n /**\n * Whether the model reads a picture in a user turn (AGL-2916). Absent\n * reads as no, so a provider registered before the field existed is never\n * sent an image it did not say it can read.\n */\n vision?: boolean\n }\n}\n\n/** The one sentence a customer may read when a provider fails. */\nexport const AI_UPSTREAM_FAILURE_COPY = 'The AI request failed — try again.'\n\n/**\n * A non-2xx answer from the provider. `message` is the fixed customer-safe\n * sentence; the provider's payload is in the log under `requestId`.\n */\nexport class AiUpstreamError extends UpstreamServiceError {\n override readonly name = 'AiUpstreamError'\n /**\n * A failure about the PLATFORM's account with the provider rather than the\n * request (AGL-3377): the key was refused, or the account is out of\n * credit. Every AI feature on the install fails until the operator fixes\n * it, so the runtime tells them. Each adapter classifies its own vendor's\n * answers; absent is an ordinary failure.\n */\n readonly accountProblem: AiAccountProblem | null\n\n constructor(\n status: number | null,\n retryable: boolean,\n requestId: string | null,\n accountProblem: AiAccountProblem | null = null,\n ) {\n super(AI_UPSTREAM_FAILURE_COPY, { status, retryable, requestId })\n this.accountProblem = accountProblem\n }\n}\n\n/** See {@link AiUpstreamError.accountProblem}. */\nexport type AiAccountProblem = 'credentials' | 'credit'\n\n/**\n * A request the runtime refuses to send. Thrown synchronously before any\n * network is touched, and it names the block at fault: this is a\n * programming error in the caller's prompt assembly, not a condition to\n * handle at runtime.\n */\nexport class AiRequestShapeError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AiRequestShapeError'\n }\n}\n\n/**\n * A provider: one vendor's wire protocol behind the contract. `id` is the\n * routing key (`anthropic`, `openai-compatible`); `models` is what the\n * provider can serve; `complete` and `stream` are the two shapes every door\n * uses. An adapter maps its vendor's errors onto `AiUpstreamError` and its\n * declines onto `AiRefusal`, and reports usage in the meter's shape.\n */\nexport interface AiProvider {\n readonly id: string\n readonly label: string\n /** The environment variable that carries this provider's key, for operator messages. */\n readonly apiKeyEnv: string\n /**\n * The key from the server environment, or `undefined` when the provider\n * is not configured to answer. Each adapter reads its own variable by\n * name in its own module, so every key read is one the key-exposure\n * guard can see (AGL-2240) and no door names a vendor's variable.\n */\n readApiKey(): string | undefined\n /** The host the provider is reached at, for the subprocessor inventory. */\n readonly endpointHost: string\n /**\n * How much of one request's strict tools this provider compiles, where it\n * states a bound. The doors never read it: a spec holds every tool set a\n * door sends to every registered provider's limits, so a schema a provider\n * would refuse is red before it ships rather than a 400 on every request.\n */\n readonly toolSchemaLimits?: AiToolSchemaLimits\n models(): readonly AiModelDescriptor[]\n complete(request: AiProviderRequest): Promise<AiResult>\n /**\n * Resolves once the provider has ACCEPTED the request — after the\n * headers, before the first token — with an iterator of events. A non-2xx\n * answer rejects with `AiUpstreamError` before the iterator exists, so a\n * door can still send a plain error status and hand back its reservation.\n */\n stream(request: AiProviderRequest): Promise<AsyncIterable<AiStreamEvent>>\n /**\n * Many requests answered together, asynchronously, at the provider's batch\n * discount, where it has one: each entry is that request's result, or the\n * error it failed with, in the order asked. Only a development run's live\n * eval calls it (`runtime/ai-live-batch.ts`); no door does.\n */\n completeBatch?(requests: readonly AiProviderRequest[], options?: AiBatchOptions): Promise<Array<AiResult | Error>>\n}\n\n/** How a batch is waited on. */\nexport interface AiBatchOptions {\n /** Between two looks at a batch still processing; 10 s when absent. */\n pollMs?: number\n /**\n * The most a batch is waited for; an hour when absent. Past it the batch\n * is canceled, which keeps every answer it already has, and each request\n * it did not answer comes back as an `AiBatchUnfinishedError` (AGL-3781).\n */\n timeoutMs?: number\n /** How long a canceled batch is given to end, so its answers can be read; 10 minutes when absent. */\n cancelGraceMs?: number\n}\n\n/**\n * A request a batch did not answer (AGL-3781): canceled when its wait ran\n * out, expired, errored, or missing from the results. Not the request's own\n * failure — the caller answers it with a request of its own rather than\n * losing it, and a failure that request meets is the real one.\n */\nexport class AiBatchUnfinishedError extends Error {\n override readonly name = 'AiBatchUnfinishedError'\n constructor(\n readonly batchId: string,\n readonly reason: string,\n ) {\n super(`batch ${batchId} did not answer this request (${reason})`)\n }\n}\n\n/** A count off the wire: finite, non-negative, whole; anything else is 0. */\nexport function aiTokenCount(value: unknown): number {\n const parsed = Number(value ?? 0)\n return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0\n}\n\n/**\n * A tool call's `input` as an object. A provider sends an object or, for\n * the one shape a stream assembles by hand, a JSON string.\n *\n * A string that does not parse to an object is an EMPTY input, not a throw.\n * The case that produces one is a stream cut off mid-call by the output\n * ceiling, and a throw there would end the stream before its `done` event —\n * the event that carries the usage a door must meter for tokens already\n * spent. An empty input is one every door's own validation refuses.\n */\nexport function aiToolInputOf(value: unknown): Record<string, unknown> {\n if (typeof value === 'string') {\n if (!value.trim()) return {}\n try {\n const parsed: unknown = JSON.parse(value)\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {}\n } catch {\n return {}\n }\n }\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n}\n"],"names":["UpstreamServiceError","compiledForm","node","root","resolving","Array","isArray","map","entry","isSchemaObject","ref","has","$ref","localSchemaAt","Set","written","key","value","Object","entries","aiToolSchemaCompiledBytes","tools","reduce","bytes","tool","JSON","stringify","inputSchema","length","startsWith","undefined","slice","split","replace","countSchema","counts","unionParameters","properties","required","property","includes","optionalParameters","items","item","branches","branch","aiToolSchemaCounts","strictTools","compiledSchemaBytes","TOOL_SCHEMA_MEASURES","aiToolSchemaBreaches","limits","flatMap","measure","one","many","source","limit","count","AI_IMAGE_MEDIA_TYPES","AI_IMAGE_MAX_BYTES","AI_REQUEST_MAX_IMAGES","aiMessageText","message","content","filter","part","type","text","join","aiMessageImages","messages","aiBase64Bytes","data","padding","endsWith","Math","max","floor","aiCacheScopeOf","block","site","aiCacheMarkerTtls","request","ceiling","cacheTtl","lifetime","scope","system","cacheBreakpoint","conversation","cacheConversation","aiStoppedAtCeiling","stopReason","AI_RAW_OUTPUT_MAX_CHARS","aiRawOutputOf","output","aiCutOffFigures","call","parsed","outputTokens","usage","thinkingTokens","parsedChars","rawChars","rawOutput","AI_UPSTREAM_FAILURE_COPY","AiUpstreamError","status","retryable","requestId","accountProblem","name","AiRequestShapeError","Error","AiBatchUnfinishedError","batchId","reason","aiTokenCount","Number","isFinite","aiToolInputOf","trim","parse"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/providers/contract.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 { UpstreamServiceError } from '@aglyn/shared-util-errors'\n\n/**\n * The provider contract (AGL-2939): the one shape every AI door speaks, and\n * the one thing an adapter has to implement. The runtime, the gate, the\n * jobs, the doctrine and the usage code import this and never a vendor\n * URL, header or model literal — Anthropic is one adapter behind it, and\n * an OpenAI-compatible endpoint is the second, which is what proves the\n * seam holds.\n *\n * ── Prompt caching, by construction ───────────────────────────────────────\n *\n * `system` is an ordered list of blocks, and caching is a PREFIX match: a\n * breakpoint covers every block before it, and one byte that differs per\n * request anywhere inside that prefix gives every distinct value its own\n * copy of it. The AGL-2352 rule is that no per-org byte may sit inside a\n * cached prefix — a white-label brand name interpolated into a cached block\n * multiplies the prefix count by the number of brands, each written off its\n * own traffic and expired before it is read. So a block marked `volatile`\n * may not sit at or before the last `cacheBreakpoint`, and the runtime\n * refuses the request before a byte leaves the process. A provider with no\n * cache honors the order and ignores the markers.\n *\n * ── Structured output ─────────────────────────────────────────────────────\n *\n * A `tools` entry is sent strict, with additional properties forbidden on\n * its schema, so a tool call's `input` validates against the schema\n * exactly. Tool choice stays automatic — forced tool use is not portable\n * across models — and the caller's own words name the tool it wants. A door\n * that sends several tools may also name the one a call is for\n * (`toolChoice`), which an adapter forces only on a model whose descriptor\n * says it takes a forced choice (`forcedToolChoice`) and only with thinking\n * off, and sends as automatic everywhere else (AGL-4076). An\n * adapter delivers `input` as an object; a stream assembles it from\n * fragments and parses the whole once, at the block's end. Nothing\n * string-matches the serialized form: models differ in how they escape it.\n * A provider bounds how much strict schema one request may carry, and an\n * adapter declares those bounds as `toolSchemaLimits`: three bounds a vendor\n * publishes and counts, and a fourth, the size of the grammar the schemas\n * compile to, that none publishes and only a live request settles.\n *\n * ── Thinking ──────────────────────────────────────────────────────────────\n *\n * `thinking: 'off'` and `'adaptive'` are sent when the provider has the\n * control; an omitted field sends NOTHING, leaving the model's own default\n * in force. The omission is deliberate rather than a gap: some models\n * reject an explicit setting, and the copy assistant serves its element\n * mode on such a model. `effort` rides the same rule.\n *\n * ── Pictures ──────────────────────────────────────────────────────────────\n *\n * A user turn may carry pictures beside its text (AGL-2916): `content` is\n * then an ordered list of parts, and a turn with nothing but text stays a\n * string, which every adapter passes through as it always has. A picture is\n * base64 bytes of one of `AI_IMAGE_MEDIA_TYPES`, never a URL, so no provider\n * fetches anything on the platform's behalf. The runtime refuses a picture\n * on an assistant turn, a picture for a model whose descriptor does not say\n * `vision`, and a picture past the size or count bounds, all before any\n * network is touched. Each adapter maps the parts onto its vendor's shape.\n */\n\n/**\n * One system block. `cacheBreakpoint` marks the end of a cacheable prefix;\n * `volatile` declares that the text carries a per-request or per-org byte\n * and therefore may never sit inside one.\n *\n * `site` declares a block that describes ONE site (AGL-3661): its business\n * profile, status and remembered preferences. As a breakpoint it is cached\n * once per site, so it must close the cached span — every platform-wide\n * block sits ahead of it, where every workspace still shares one entry —\n * and the runtime refuses a request that puts another breakpoint after it.\n */\nexport interface AiSystemBlock {\n text: string\n cacheBreakpoint?: true\n volatile?: true\n site?: true\n}\n\n/** A structured-output tool. `strict` is required, not optional. */\nexport interface AiTool {\n name: string\n description: string\n /** A JSON schema of `type: 'object'`; `additionalProperties` is forced off. */\n inputSchema: Record<string, unknown>\n strict: true\n}\n\n/**\n * How much of a request's strict tools a provider can compile (AGL-3096). A\n * provider that constrains decoding to a tool's schema compiles every strict\n * schema of a request together, before the model runs, and refuses a request\n * whose schemas are past its bounds. That refusal is permanent: the schema is\n * the same on every retry. Each bound is a TOTAL over every strict tool one\n * request sends, as `aiToolSchemaCounts` counts them. A bound left out is one\n * the provider does not state.\n *\n * A provider may also refuse a request whose schemas compile to too large a\n * grammar, which none of the published counts can see: a request counting 1\n * strict tool, 0 optional parameters and 1 union was refused for exactly that\n * (AGL-3096). `compiledSchemaBytes` stands in for it, on live evidence.\n */\nexport interface AiToolSchemaLimits {\n /** The most strict tools one request may send. */\n strictTools?: number\n /** The most properties, across every strict schema, left out of their object's `required`. */\n optionalParameters?: number\n /** The most schemas, across every strict schema, that are a union: an `anyOf` or `oneOf`, or a list of types. */\n unionParameters?: number\n /**\n * The most bytes of schema a request's grammar may be compiled from, as\n * `aiToolSchemaCompiledBytes` measures them.\n *\n * Unlike the bounds above, this one is EMPIRICAL. A provider refuses a\n * request whose compiled grammar is too large without publishing the size,\n * so the number here is not read off a vendor's page: it is the largest a\n * live request has been watched to carry, and every number above it is\n * unknown rather than safe. Raising it on an offline measurement alone\n * puts the guard back where it could not see the break.\n */\n compiledSchemaBytes?: number\n}\n\n/** A request's strict tools, counted on the measures `AiToolSchemaLimits` bounds. */\nexport type AiToolSchemaCounts = Required<AiToolSchemaLimits>\n\n/**\n * One schema as a grammar compiler reads it: every local `$ref` written out\n * at each place it is used, and every description, title and `$defs` dropped,\n * since none of them constrains a single token. A schema that refers to\n * itself is written out once around and then left as the reference it is.\n */\nfunction compiledForm(node: unknown, root: Record<string, unknown>, resolving: ReadonlySet<string>): unknown {\n if (Array.isArray(node)) return node.map((entry) => compiledForm(entry, root, resolving))\n if (!isSchemaObject(node)) return node\n const ref = node['$ref']\n if (typeof ref === 'string') {\n if (resolving.has(ref)) return { $ref: ref }\n return compiledForm(localSchemaAt(root, ref), root, new Set([...resolving, ref]))\n }\n const written: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(node)) {\n if (key === 'description' || key === 'title' || key === '$defs') continue\n written[key] = compiledForm(value, root, resolving)\n }\n return written\n}\n\n/**\n * The bytes of schema a request's grammar is compiled from, over all its\n * strict tools. It is a stand-in for the size of the grammar itself, which\n * nothing outside the provider can measure, and it errs toward more: a `$ref`\n * counts at each place it is used, because a compiler that writes it out\n * there pays for it there.\n */\nexport function aiToolSchemaCompiledBytes(tools: readonly AiTool[]): number {\n return tools.reduce(\n (bytes, tool) => bytes + JSON.stringify(compiledForm(tool.inputSchema, tool.inputSchema, new Set())).length,\n 0,\n )\n}\n\nfunction isSchemaObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** The schema a local `$ref` (`#/…`) points at, or `undefined`. */\nfunction localSchemaAt(root: Record<string, unknown>, ref: string): unknown {\n if (!ref.startsWith('#/')) return undefined\n return ref\n .slice(2)\n .split('/')\n .map((key) => key.replace(/~1/g, '/').replace(/~0/g, '~'))\n .reduce<unknown>((node, key) => (isSchemaObject(node) ? node[key] : undefined), root)\n}\n\nfunction countSchema(\n node: unknown,\n root: Record<string, unknown>,\n counts: AiToolSchemaCounts,\n resolving: ReadonlySet<string>,\n): void {\n if (!isSchemaObject(node)) return\n if (Array.isArray(node['anyOf']) || Array.isArray(node['oneOf']) || Array.isArray(node['type'])) {\n counts.unionParameters += 1\n }\n const ref = node['$ref']\n if (typeof ref === 'string' && !resolving.has(ref)) {\n countSchema(localSchemaAt(root, ref), root, counts, new Set([...resolving, ref]))\n }\n const properties = node['properties']\n if (isSchemaObject(properties)) {\n const required = Array.isArray(node['required']) ? node['required'] : []\n for (const [key, property] of Object.entries(properties)) {\n if (!required.includes(key)) counts.optionalParameters += 1\n countSchema(property, root, counts, resolving)\n }\n }\n const items = node['items']\n for (const item of Array.isArray(items) ? items : [items]) countSchema(item, root, counts, resolving)\n for (const key of ['anyOf', 'allOf', 'oneOf']) {\n const branches = node[key]\n if (Array.isArray(branches)) for (const branch of branches) countSchema(branch, root, counts, resolving)\n }\n}\n\n/**\n * The strict tools of one request, counted on each measure a provider bounds.\n * Every tool the contract sends is strict. Each schema is walked whole: every\n * property, every `items`, every branch of an `anyOf`, `allOf` or `oneOf`, and\n * a local `$ref` at each place it is used. A union nested in an array's items\n * or in another union's branch counts on its own, as it does for a provider\n * that compiles it; a `$ref` counted where it is used errs toward more.\n */\nexport function aiToolSchemaCounts(tools: readonly AiTool[]): AiToolSchemaCounts {\n const counts: AiToolSchemaCounts = {\n strictTools: tools.length,\n optionalParameters: 0,\n unionParameters: 0,\n compiledSchemaBytes: aiToolSchemaCompiledBytes(tools),\n }\n for (const tool of tools) countSchema(tool.inputSchema, tool.inputSchema, counts, new Set())\n return counts\n}\n\n/**\n * Each bound, as one, as many, and where its number comes from — a published\n * bound reads as the provider's own, a measured one as what has been proved.\n */\nconst TOOL_SCHEMA_MEASURES: ReadonlyArray<[keyof AiToolSchemaLimits, string, string, string]> = [\n ['strictTools', 'strict tool', 'strict tools', 'the provider compiles'],\n ['optionalParameters', 'optional parameter', 'optional parameters', 'the provider compiles'],\n ['unionParameters', 'union-typed parameter', 'union-typed parameters', 'the provider compiles'],\n ['compiledSchemaBytes', 'byte of compiled schema', 'bytes of compiled schema', 'a live request has proved'],\n]\n\n/** Each bound a request's tools are past, as a sentence; empty when every one holds. */\nexport function aiToolSchemaBreaches(\n tools: readonly AiTool[],\n limits: AiToolSchemaLimits | undefined,\n): string[] {\n if (!limits) return []\n const counts = aiToolSchemaCounts(tools)\n return TOOL_SCHEMA_MEASURES.flatMap(([measure, one, many, source]) => {\n const limit = limits[measure]\n const count = counts[measure]\n return limit !== undefined && count > limit\n ? [`${count} ${count === 1 ? one : many} in one request, over the ${limit} ${source}`]\n : []\n })\n}\n\nexport type AiEffort = 'low' | 'medium' | 'high'\nexport type AiThinking = 'off' | 'adaptive'\n\n/**\n * The picture formats a request may carry (AGL-2916): the four every model\n * the catalog marks as reading images accepts. A door converts anything else\n * — an AVIF, an SVG — before it attaches the picture, or attaches none.\n */\nexport const AI_IMAGE_MEDIA_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'] as const\nexport type AiImageMediaType = (typeof AI_IMAGE_MEDIA_TYPES)[number]\n\n/**\n * The largest picture one part may carry, in decoded bytes: the ceiling the\n * providers behind the contract accept for one image. A door that attaches a\n * picture resizes it far below this; the runtime refuses anything above it\n * before a byte leaves the process, rather than letting the provider do it\n * after the request was paid for in time.\n */\nexport const AI_IMAGE_MAX_BYTES = 5 * 1024 * 1024\n\n/** The most pictures one request may carry. */\nexport const AI_REQUEST_MAX_IMAGES = 8\n\n/** Text in a message made of parts. */\nexport interface AiTextPart {\n type: 'text'\n text: string\n}\n\n/**\n * A picture in a message (AGL-2916): its bytes, base64-encoded, and their\n * format. Only a USER turn carries one, and only to a model whose descriptor\n * says it reads images; the runtime refuses any other request before a byte\n * leaves the process, so a door cannot send a picture by accident to a model\n * that would reject it, or to a turn the model wrote.\n */\nexport interface AiImagePart {\n type: 'image'\n mediaType: AiImageMediaType\n /** The picture's bytes, base64-encoded, with no `data:` prefix. */\n data: string\n}\n\nexport type AiMessagePart = AiTextPart | AiImagePart\n\nexport interface AiMessage {\n role: 'user' | 'assistant'\n /**\n * The turn's text, or its parts in order — text and pictures — for a door\n * that shows the model an image. A door with nothing but text sends a\n * string, which every adapter passes through as it always has.\n */\n content: string | readonly AiMessagePart[]\n}\n\n/** A message's text: its parts' text joined, with every picture left out. */\nexport function aiMessageText(message: Pick<AiMessage, 'content'>): string {\n if (typeof message.content === 'string') return message.content\n return message.content\n .filter((part): part is AiTextPart => part.type === 'text')\n .map((part) => part.text)\n .join('\\n\\n')\n}\n\n/** Every picture a list of messages carries, in order. */\nexport function aiMessageImages(messages: readonly Pick<AiMessage, 'content'>[]): AiImagePart[] {\n return messages.flatMap((message) =>\n typeof message.content === 'string'\n ? []\n : message.content.filter((part): part is AiImagePart => part.type === 'image'),\n )\n}\n\n/** The decoded size of a base64 string, without decoding it. */\nexport function aiBase64Bytes(data: string): number {\n const padding = data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0\n return Math.max(0, Math.floor((data.length * 3) / 4) - padding)\n}\n\n/** What every door hands the runtime; `provider` and `model` resolve there. */\nexport interface AiRequestBase {\n model: string\n system: AiSystemBlock[]\n messages: AiMessage[]\n tools?: AiTool[]\n maxTokens: number\n effort?: AiEffort\n thinking?: AiThinking\n signal?: AbortSignal\n /**\n * How long a cache entry the SHARED breakpoints write should live, where\n * the provider offers a choice: the provider's default (five minutes on the\n * Messages API) when absent. A step a site job runs asks for `1h` through\n * its routing row (`AiRoutingRow.cacheTtl`, AGL-3830), in production too,\n * and a development live run asks for it on every request\n * (`runtime/ai-dev-env.ts`). A job's own markers keep five minutes whatever\n * this says (`aiCacheMarkerTtls`, AGL-4050). It changes no answer, only what\n * a request within the hour pays for the prefix, so a replay key does not\n * read it.\n */\n cacheTtl?: '1h'\n /**\n * Cache the conversation through its last message as well as the system\n * blocks (AGL-3809), for a request the door expects to continue: a\n * generation that looks the site up or is asked again re-sends its whole\n * first turn, which without this is billed at the full input rate on every\n * continuation. One more breakpoint, on the last message, counted against\n * the same limit as the system's; a provider without a cache ignores it.\n * It changes no answer, so a replay key does not read it.\n */\n cacheConversation?: boolean\n /**\n * The tool this call is for, by name: one of `tools` (AGL-4076). A step\n * whose calls each want a different tool sends every one of them on every\n * call, so the tools that open the cached prefix are the same bytes each\n * time and the later calls read the prefix the first one wrote; this names\n * the one wanted now. A tool choice sits outside the cached prefix, so\n * naming a different one costs no cache read. The adapter forces it only\n * where the model's descriptor says `forcedToolChoice` and the request\n * thinks with thinking off, which a forced choice requires; everywhere else\n * the choice stays automatic and the door's own words name the tool.\n */\n toolChoice?: string\n}\n\n/** The request an adapter receives: the runtime has already validated it. */\nexport interface AiProviderRequest extends AiRequestBase {\n /** The provider's own key, resolved by the runtime from the environment or the org's setting. */\n apiKey: string\n}\n\n/** How long one cache marker's entry lives: the provider's five minutes, or the hour. */\nexport type AiCacheTtl = '5m' | '1h'\n\n/**\n * Who reads a cache marker's entry (AGL-4050). A `shared` marker closes a\n * span of the platform's own prompt (the tools, the doctrine, a door's\n * rules), the same bytes for every org and every job, so the next job can\n * read what this one wrote: it lives as long as the request's `cacheTtl`\n * asks. A `job` marker closes bytes only one job reads: a site's block\n * (`site`) or the request's own conversation (`cacheConversation`). That job\n * reads it again within minutes and no other job ever does, so it keeps the\n * provider's five minutes whatever the route asks; at the hour, its 2x write\n * would never be read back.\n */\nexport type AiCacheScope = 'shared' | 'job'\n\n/** A system block's marker scope: a site's block is one job's, every other block the platform's. */\nexport function aiCacheScopeOf(block: Pick<AiSystemBlock, 'site'>): AiCacheScope {\n return block.site ? 'job' : 'shared'\n}\n\n/**\n * The lifetime of every cache marker a request carries (AGL-4050), in prefix\n * order: one per system block, `null` where the block marks none, then the\n * conversation's, `null` where it is not cached. Tools carry no marker of\n * their own; they sit ahead of the first system marker and live as long as it.\n *\n * A provider refuses a request whose longer-lived entry comes after a\n * shorter-lived one, so once a marker is five minutes every marker after it\n * is too. A shared marker behind a job's is downgraded, never sent at the\n * hour. The runtime already keeps a site's block last among the system\n * breakpoints, so a door's request never meets the downgrade; it is the\n * adapter's guarantee all the same.\n */\nexport function aiCacheMarkerTtls(\n request: Pick<AiRequestBase, 'system' | 'messages' | 'cacheTtl' | 'cacheConversation'>,\n): { system: Array<AiCacheTtl | null>; conversation: AiCacheTtl | null } {\n let ceiling: AiCacheTtl = request.cacheTtl === '1h' ? '1h' : '5m'\n const lifetime = (scope: AiCacheScope): AiCacheTtl => {\n if (scope === 'job') ceiling = '5m'\n return ceiling\n }\n const system = request.system.map((block) => (block.cacheBreakpoint ? lifetime(aiCacheScopeOf(block)) : null))\n const conversation = request.cacheConversation && request.messages.length > 0 ? lifetime('job') : null\n return { system, conversation }\n}\n\n/**\n * Token usage in the meter's shape. Cache reads and writes are separate\n * because they are priced separately; a provider with no cache reports 0.\n */\nexport interface AiUsage {\n inputTokens: number\n outputTokens: number\n cacheReadTokens: number\n cacheWriteTokens: number\n /**\n * How many of `cacheWriteTokens` went to an hour-long cache entry\n * (AGL-3830), where the provider breaks the writes down; absent where none\n * did or the provider does not say.\n *\n * Inside `cacheWriteTokens`, never beside it, like `thinkingTokens` below.\n * An hour-long write is billed at twice the input price where a rate\n * table's `cacheWritePerToken` is the five-minute 1.25 times, so a price\n * reads this to add the difference (`aiCacheWriteHourSurchargeUsd`).\n */\n cacheWriteHourTokens?: number\n /**\n * How many of `outputTokens` the model spent thinking, where the provider\n * reports it; absent where it does not (AGL-3143).\n *\n * Inside `outputTokens`, never beside it: a rate table prices the four\n * figures above and would charge these twice if it read this one.\n *\n * It is here because it is the figure that says where a generation's\n * output went. A tool call that comes back far smaller than the tokens it\n * cost has three shapes, and only this separates the first from the other\n * two: the model thought and then answered briefly, which is a model\n * working as asked; a runaway string the partial parse discarded; or\n * decoding that wrote nothing usable. Kept on every answer, because a\n * generation burns its budget thinking without ever reaching its ceiling,\n * and `rawOutput` is only there when it does.\n */\n thinkingTokens?: number\n}\n\nexport interface AiToolUse {\n name: string\n input: Record<string, unknown>\n}\n\n/** A completed, non-streaming answer. */\nexport interface AiCompletion {\n kind: 'completion'\n /** Every text block, joined. */\n text: string\n toolUse: AiToolUse[]\n usage: AiUsage\n /**\n * The exchange at the serving model's BILLED rates — what a customer's\n * credits are drawn from, and never a margin's cost side (AGL-3015). What\n * the same exchange cost us is `estimateAiProviderCostUsd`, recorded by\n * the meter; a result travels no further than the credit path, so it\n * carries the one figure that path needs.\n */\n estCostUsd: number\n stopReason: string | null\n /**\n * WHERE THE OUTPUT WENT (AGL-3143). The provider's own output for this\n * call, as it answered it, kept ONLY when the call stopped at its ceiling\n * and cut to `AI_RAW_OUTPUT_MAX_CHARS`.\n *\n * A tool call truncated mid-answer reaches `toolUse` as whatever the\n * provider could still parse, and the parsed input alone cannot say where\n * the tokens went: a runaway string the partial parse discarded and a\n * decoding stall that wrote nothing usable both leave a small input\n * against a spent ceiling. This is what tells them apart. These are the\n * model's own words, so they belong to the trace alone: never to a reader,\n * and never to a log, which carries the figures that separate the two\n * shapes — `outputTokens`, `parsedChars`, `rawChars`.\n */\n rawOutput?: string\n}\n\n/**\n * Whether a model call stopped because it reached the output ceiling it was\n * asked for (AGL-3042). `max_tokens` is the contract's word, and the\n * OpenAI-compatible adapter maps its endpoint's `length` onto it; `length` is\n * read as well, for an adapter that passes its provider's own word through.\n */\nexport function aiStoppedAtCeiling(stopReason: string | null): boolean {\n return stopReason === 'max_tokens' || stopReason === 'length'\n}\n\n/** The most characters of raw output a call cut off at its ceiling keeps. */\nexport const AI_RAW_OUTPUT_MAX_CHARS = 20_000\n\n/**\n * A cut-off call's output as the trace keeps it: what the provider answered,\n * serialized where it is not already text, and cut to the bound with an\n * ellipsis so a reader can tell a cut trace from a short answer.\n */\nexport function aiRawOutputOf(output: unknown): string {\n const text = typeof output === 'string' ? output : (JSON.stringify(output) ?? '')\n return text.length > AI_RAW_OUTPUT_MAX_CHARS ? `${text.slice(0, AI_RAW_OUTPUT_MAX_CHARS)}…` : text\n}\n\n/**\n * WHERE THE OUTPUT WENT, as a log may carry it (AGL-3143): the figures that\n * separate the shapes a call cut off at its ceiling comes in, and nothing\n * else.\n *\n * Thinking accounts for the spend first, because the thinking budget is\n * drawn from the same ceiling as the answer: a call can spend nearly all of\n * one thinking and still be cut off with a correct, tiny tool input, which\n * is a ceiling to raise rather than a decoder to fix. What thinking does not\n * account for is told apart by how much was written — long, and a runaway\n * string the partial parse discarded; short, and decoding wrote nothing\n * usable.\n *\n * It takes the raw output rather than a count on purpose. The bytes are the\n * model's own words and therefore the site's content, so they belong to the\n * trace alone and never to a log; passing them through one function that can\n * only ever return their LENGTH is what holds that rule at every door,\n * instead of asking each one to remember it.\n */\nexport function aiCutOffFigures(call: {\n usage: AiUsage\n /** The tool input that still parsed, or null where none did. */\n parsed: unknown\n rawOutput: string | null | undefined\n}): {\n outputTokens: number\n thinkingTokens: number | null\n parsedChars: number\n rawChars: number | null\n} {\n const parsed =\n call.parsed === null || call.parsed === undefined ? '' : (JSON.stringify(call.parsed) ?? '')\n return {\n outputTokens: call.usage.outputTokens,\n thinkingTokens: call.usage.thinkingTokens ?? null,\n parsedChars: parsed.length,\n rawChars: call.rawOutput?.length ?? null,\n }\n}\n\n/**\n * The model declined. Tokens were spent, so the usage is real and the\n * caller meters it; the text is whatever partial answer preceded the stop,\n * usually nothing. Every adapter maps its provider's own decline signal —\n * a refusal stop, a content filter — onto this one kind.\n */\nexport interface AiRefusal {\n kind: 'refusal'\n text: string\n usage: AiUsage\n estCostUsd: number\n stopReason: 'refusal'\n}\n\nexport type AiResult = AiCompletion | AiRefusal\n\nexport type AiStreamEvent =\n | { type: 'delta'; text: string }\n | { type: 'tool'; name: string; input: Record<string, unknown> }\n /**\n * The provider's in-stream error. The stream is still open and ends with\n * `done`, so the caller decides what the reader is told; the provider's\n * own words went to the log, not here.\n */\n | { type: 'error'; retryable: boolean }\n | {\n type: 'done'\n usage: AiUsage\n estCostUsd: number\n stopReason: string | null\n /**\n * WHERE THE OUTPUT WENT, for a stream (AGL-3143): the bytes the\n * stream's tool calls were written in, kept ONLY when the stream\n * stopped at its ceiling and cut to `AI_RAW_OUTPUT_MAX_CHARS`. The\n * streaming counterpart of `AiCompletion.rawOutput`.\n *\n * A `tool` event carries what `aiToolInputOf` could still parse, which\n * for a call cut mid-answer is a prefix of the object or nothing at\n * all; the fragments those events were assembled from are the only\n * record of what the model actually emitted, and without them a\n * runaway string and a decoding stall look identical from the\n * outside. These are the model's own words, so they belong to the\n * trace alone: never to a reader, and never to a log, which carries\n * the figures that separate the two shapes.\n */\n rawOutput?: string\n }\n\n/** What a provider says about one of its models; the catalog carries both rates. */\nexport interface AiModelDescriptor {\n id: string\n /** The provider id the model is served by. */\n provider: string\n label: string\n capabilities: {\n streaming: boolean\n tools: boolean\n /** Whether the provider accepts an explicit `thinking` setting for this model. */\n thinking: boolean\n /** Whether the provider caches a system prefix for this model. */\n promptCache: boolean\n /**\n * Whether the model reads a picture in a user turn (AGL-2916). Absent\n * reads as no, so a provider registered before the field existed is never\n * sent an image it did not say it can read.\n */\n vision?: boolean\n /**\n * Whether the model takes a forced tool choice (AGL-4076): a request that\n * names the one tool it must call. Absent reads as no, so a request that\n * names its tool is sent an automatic choice instead. Claude Opus 5.5 and\n * Claude Sonnet 5.5 refuse a forced choice outright.\n */\n forcedToolChoice?: boolean\n }\n}\n\n/** The one sentence a customer may read when a provider fails. */\nexport const AI_UPSTREAM_FAILURE_COPY = 'The AI request failed — try again.'\n\n/**\n * A non-2xx answer from the provider. `message` is the fixed customer-safe\n * sentence; the provider's payload is in the log under `requestId`.\n */\nexport class AiUpstreamError extends UpstreamServiceError {\n override readonly name = 'AiUpstreamError'\n /**\n * A failure about the PLATFORM's account with the provider rather than the\n * request (AGL-3377): the key was refused, or the account is out of\n * credit. Every AI feature on the install fails until the operator fixes\n * it, so the runtime tells them. Each adapter classifies its own vendor's\n * answers; absent is an ordinary failure.\n */\n readonly accountProblem: AiAccountProblem | null\n\n constructor(\n status: number | null,\n retryable: boolean,\n requestId: string | null,\n accountProblem: AiAccountProblem | null = null,\n ) {\n super(AI_UPSTREAM_FAILURE_COPY, { status, retryable, requestId })\n this.accountProblem = accountProblem\n }\n}\n\n/** See {@link AiUpstreamError.accountProblem}. */\nexport type AiAccountProblem = 'credentials' | 'credit'\n\n/**\n * A request the runtime refuses to send. Thrown synchronously before any\n * network is touched, and it names the block at fault: this is a\n * programming error in the caller's prompt assembly, not a condition to\n * handle at runtime.\n */\nexport class AiRequestShapeError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AiRequestShapeError'\n }\n}\n\n/**\n * A provider: one vendor's wire protocol behind the contract. `id` is the\n * routing key (`anthropic`, `openai-compatible`); `models` is what the\n * provider can serve; `complete` and `stream` are the two shapes every door\n * uses. An adapter maps its vendor's errors onto `AiUpstreamError` and its\n * declines onto `AiRefusal`, and reports usage in the meter's shape.\n */\nexport interface AiProvider {\n readonly id: string\n readonly label: string\n /** The environment variable that carries this provider's key, for operator messages. */\n readonly apiKeyEnv: string\n /**\n * The key from the server environment, or `undefined` when the provider\n * is not configured to answer. Each adapter reads its own variable by\n * name in its own module, so every key read is one the key-exposure\n * guard can see (AGL-2240) and no door names a vendor's variable.\n */\n readApiKey(): string | undefined\n /** The host the provider is reached at, for the subprocessor inventory. */\n readonly endpointHost: string\n /**\n * How much of one request's strict tools this provider compiles, where it\n * states a bound. The doors never read it: a spec holds every tool set a\n * door sends to every registered provider's limits, so a schema a provider\n * would refuse is red before it ships rather than a 400 on every request.\n */\n readonly toolSchemaLimits?: AiToolSchemaLimits\n models(): readonly AiModelDescriptor[]\n complete(request: AiProviderRequest): Promise<AiResult>\n /**\n * Resolves once the provider has ACCEPTED the request — after the\n * headers, before the first token — with an iterator of events. A non-2xx\n * answer rejects with `AiUpstreamError` before the iterator exists, so a\n * door can still send a plain error status and hand back its reservation.\n */\n stream(request: AiProviderRequest): Promise<AsyncIterable<AiStreamEvent>>\n /**\n * Many requests answered together, asynchronously, at the provider's batch\n * discount, where it has one: each entry is that request's result, or the\n * error it failed with, in the order asked. Only a development run's live\n * eval calls it (`runtime/ai-live-batch.ts`); no door does.\n */\n completeBatch?(requests: readonly AiProviderRequest[], options?: AiBatchOptions): Promise<Array<AiResult | Error>>\n}\n\n/** How a batch is waited on. */\nexport interface AiBatchOptions {\n /** Between two looks at a batch still processing; 10 s when absent. */\n pollMs?: number\n /**\n * The most a batch is waited for; an hour when absent. Past it the batch\n * is canceled, which keeps every answer it already has, and each request\n * it did not answer comes back as an `AiBatchUnfinishedError` (AGL-3781).\n */\n timeoutMs?: number\n /** How long a canceled batch is given to end, so its answers can be read; 10 minutes when absent. */\n cancelGraceMs?: number\n}\n\n/**\n * A request a batch did not answer (AGL-3781): canceled when its wait ran\n * out, expired, errored, or missing from the results. Not the request's own\n * failure — the caller answers it with a request of its own rather than\n * losing it, and a failure that request meets is the real one.\n */\nexport class AiBatchUnfinishedError extends Error {\n override readonly name = 'AiBatchUnfinishedError'\n constructor(\n readonly batchId: string,\n readonly reason: string,\n ) {\n super(`batch ${batchId} did not answer this request (${reason})`)\n }\n}\n\n/** A count off the wire: finite, non-negative, whole; anything else is 0. */\nexport function aiTokenCount(value: unknown): number {\n const parsed = Number(value ?? 0)\n return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0\n}\n\n/**\n * A tool call's `input` as an object. A provider sends an object or, for\n * the one shape a stream assembles by hand, a JSON string.\n *\n * A string that does not parse to an object is an EMPTY input, not a throw.\n * The case that produces one is a stream cut off mid-call by the output\n * ceiling, and a throw there would end the stream before its `done` event —\n * the event that carries the usage a door must meter for tokens already\n * spent. An empty input is one every door's own validation refuses.\n */\nexport function aiToolInputOf(value: unknown): Record<string, unknown> {\n if (typeof value === 'string') {\n if (!value.trim()) return {}\n try {\n const parsed: unknown = JSON.parse(value)\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {}\n } catch {\n return {}\n }\n }\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n}\n"],"names":["UpstreamServiceError","compiledForm","node","root","resolving","Array","isArray","map","entry","isSchemaObject","ref","has","$ref","localSchemaAt","Set","written","key","value","Object","entries","aiToolSchemaCompiledBytes","tools","reduce","bytes","tool","JSON","stringify","inputSchema","length","startsWith","undefined","slice","split","replace","countSchema","counts","unionParameters","properties","required","property","includes","optionalParameters","items","item","branches","branch","aiToolSchemaCounts","strictTools","compiledSchemaBytes","TOOL_SCHEMA_MEASURES","aiToolSchemaBreaches","limits","flatMap","measure","one","many","source","limit","count","AI_IMAGE_MEDIA_TYPES","AI_IMAGE_MAX_BYTES","AI_REQUEST_MAX_IMAGES","aiMessageText","message","content","filter","part","type","text","join","aiMessageImages","messages","aiBase64Bytes","data","padding","endsWith","Math","max","floor","aiCacheScopeOf","block","site","aiCacheMarkerTtls","request","ceiling","cacheTtl","lifetime","scope","system","cacheBreakpoint","conversation","cacheConversation","aiStoppedAtCeiling","stopReason","AI_RAW_OUTPUT_MAX_CHARS","aiRawOutputOf","output","aiCutOffFigures","call","parsed","outputTokens","usage","thinkingTokens","parsedChars","rawChars","rawOutput","AI_UPSTREAM_FAILURE_COPY","AiUpstreamError","status","retryable","requestId","accountProblem","name","AiRequestShapeError","Error","AiBatchUnfinishedError","batchId","reason","aiTokenCount","Number","isFinite","aiToolInputOf","trim","parse"],"mappings":"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"}
@@ -107,8 +107,11 @@ import { aiProviderById, defaultAiProvider } from "./registry.js";
107
107
  // A scoped, latency-sensitive chat turn: no thinking, the low effort rung.
108
108
  thinking: 'off',
109
109
  effort: 'low',
110
- maxTokens: 1024,
111
- maxTokensBasis: 'a docs-grounded answer of a few short paragraphs; the edit rung raises it for its tool call',
110
+ // 2,048 since AGL-4075: a non-Latin script spends two to four times the
111
+ // tokens per character, and 5 of 17 non-English answers stopped at 1,024
112
+ // against 1 of 33 English ones. Output is billed only as it is written.
113
+ maxTokens: 2048,
114
+ maxTokensBasis: 'a docs-grounded answer of a few short paragraphs in any language; the edit rung raises it for its tool call',
112
115
  eval: {
113
116
  kinds: [
114
117
  'chat'