@aglyn/plugins-ai 1.0.0-beta.205 → 1.0.0-beta.207
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/declarations.server.js +9 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/operator-alerts.d.ts +25 -0
- package/src/lib/operator-alerts.js +37 -0
- package/src/lib/operator-alerts.js.map +1 -0
- package/src/lib/providers/anthropic.d.ts +9 -1
- package/src/lib/providers/anthropic.js +15 -1
- package/src/lib/providers/anthropic.js.map +1 -1
- package/src/lib/providers/contract.d.ts +11 -1
- package/src/lib/providers/contract.js +2 -1
- package/src/lib/providers/contract.js.map +1 -1
- package/src/lib/providers/openai-compatible.d.ts +11 -1
- package/src/lib/providers/openai-compatible.js +14 -1
- package/src/lib/providers/openai-compatible.js.map +1 -1
- package/src/lib/runtime/ai-runtime.js +32 -2
- package/src/lib/runtime/ai-runtime.js.map +1 -1
- package/src/lib/runtime/operator-alerts-seam.d.ts +28 -0
- package/src/lib/runtime/operator-alerts-seam.js +28 -0
- package/src/lib/runtime/operator-alerts-seam.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-ai",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.207",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,22 +25,22 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-ui-theme": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-errors": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
41
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
42
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
43
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.207",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.207",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.207",
|
|
31
|
+
"@aglyn/shared-data-types": "1.0.0-beta.207",
|
|
32
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.207",
|
|
33
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.207",
|
|
34
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.207",
|
|
35
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.207",
|
|
36
|
+
"@aglyn/shared-util-email": "1.0.0-beta.207",
|
|
37
|
+
"@aglyn/shared-util-errors": "1.0.0-beta.207",
|
|
38
|
+
"@aglyn/shared-util-http": "1.0.0-beta.207",
|
|
39
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.207",
|
|
40
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.207",
|
|
41
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.207",
|
|
42
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.207",
|
|
43
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.207",
|
|
44
44
|
"@swc/helpers": "0.5.23"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
@@ -21,7 +21,9 @@ import { listPluginEventHandlers, registerPluginEventHandler } from "@aglyn/agly
|
|
|
21
21
|
import { listPluginUserErasers, registerPluginUserEraser } from "@aglyn/aglyn/plugin-manager/plugin-user-erasure";
|
|
22
22
|
import { listUsageAlertContributors, registerUsageAlertContributor } from "@aglyn/aglyn/plugin-manager/usage-alert-contributors";
|
|
23
23
|
import { pluginMeteredLineOwner } from "@aglyn/aglyn/plugin-manager/plugin-metered-lines";
|
|
24
|
+
import { registerOperatorAlerts } from "@aglyn/aglyn/plugin-manager/operator-alerts";
|
|
24
25
|
import { AI_PLUGIN_ID } from "./constants.js";
|
|
26
|
+
import { AI_PROVIDER_UNAVAILABLE } from "./operator-alerts.js";
|
|
25
27
|
import { registerAiDeclarations } from "./declarations.js";
|
|
26
28
|
import { AI_OVERAGE_METER_LINE_ID, registerAiOverageMeteredLine } from "./billing/ai-overage-cutover.js";
|
|
27
29
|
/** The provider-spend staff alerts' contributor id under the plugin. */ const AI_USAGE_ALERTS_ID = 'provider-spend';
|
|
@@ -122,6 +124,13 @@ import { AI_OVERAGE_METER_LINE_ID, registerAiOverageMeteredLine } from "./billin
|
|
|
122
124
|
pluginId: AI_PLUGIN_ID
|
|
123
125
|
});
|
|
124
126
|
}
|
|
127
|
+
// The platform provider account alert (AGL-3377), listed on Staff →
|
|
128
|
+
// Operator alerts before the first one is ever raised.
|
|
129
|
+
registerOperatorAlerts([
|
|
130
|
+
AI_PROVIDER_UNAVAILABLE
|
|
131
|
+
], {
|
|
132
|
+
pluginId: AI_PLUGIN_ID
|
|
133
|
+
});
|
|
125
134
|
if (!listUsageAlertContributors().some((contributor)=>contributor.pluginId === AI_PLUGIN_ID && contributor.id === AI_USAGE_ALERTS_ID)) {
|
|
126
135
|
registerUsageAlertContributor({
|
|
127
136
|
pluginId: AI_PLUGIN_ID,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/declarations.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Each registry from its own module, not the `@aglyn/aglyn/server` barrel:\n// boot needs three registries rather than the whole server surface, and a\n// suite that stubs that barrel wholesale still loads these declarations\n// against the real registries.\nimport {\n listPluginEventHandlers,\n registerPluginEventHandler,\n} from '@aglyn/aglyn/plugin-manager/plugin-events'\nimport {\n listPluginUserErasers,\n registerPluginUserEraser,\n} from '@aglyn/aglyn/plugin-manager/plugin-user-erasure'\nimport {\n listUsageAlertContributors,\n registerUsageAlertContributor,\n} from '@aglyn/aglyn/plugin-manager/usage-alert-contributors'\nimport { pluginMeteredLineOwner } from '@aglyn/aglyn/plugin-manager/plugin-metered-lines'\nimport { AI_PLUGIN_ID } from './constants'\nimport { registerAiDeclarations } from './declarations'\nimport {\n AI_OVERAGE_METER_LINE_ID,\n registerAiOverageMeteredLine,\n} from './billing/ai-overage-cutover'\n\n/** The provider-spend staff alerts' contributor id under the plugin. */\nconst AI_USAGE_ALERTS_ID = 'provider-spend'\n\n/**\n * The plugin's SERVER declarations (AGL-2939): what the server must know\n * at boot, before any door of the plugin has been called. The two platform\n * events the plugin writes activity for — the AI add-on bought or dropped,\n * an `ai.*` permission moved — are raised by core billing and membership\n * routes that never load the plugin's API surface, so the subscriptions\n * have to be in place from the first request. So does the eraser: an\n * account erasure is a core route too, and a person's AI usage months sit\n * under each org, keyed by the uid, where no core delete reaches them. So\n * does the usage alert contributor (AGL-2984): the usage-alerts sweep is a\n * core cron, and it evaluates the plugin's staff alerts on provider spend for\n * every org it reads.\n *\n * Light at boot by construction: the writers behind the handlers are\n * imported when the first event arrives, and the alert rules when the sweep\n * first evaluates an org, not when the process starts, so the boot cost is\n * the registration and nothing that touches Firestore.\n */\nexport function registerAiServerDeclarations(): void {\n registerAiDeclarations()\n // Idempotent against each REGISTRY rather than a module flag, so a\n // registry reset (a spec) or a second module instance registers again.\n if (!listPluginEventHandlers('org.seatAddons.changed').includes(AI_PLUGIN_ID)) {\n registerPluginEventHandler(\n 'org.seatAddons.changed',\n async ({ orgId, actor, before, after }) => {\n const { logAiAddonChanged } = await import('./activity/ai-activity')\n await logAiAddonChanged(orgId, actor, { before, after })\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'org.permissions.changed',\n async ({ orgId, actor, subject, permission, granted }) => {\n if (!permission.startsWith('ai.')) return\n const { logAiPermissionChanged } = await import('./activity/ai-activity')\n await logAiPermissionChanged(orgId, actor, { subject, permission, granted })\n },\n { pluginId: AI_PLUGIN_ID },\n )\n }\n // THE PLATFORM'S BILLING EVENTS (AGL-3011). Subscribed at boot for the\n // same reason as the two above: they are raised by the billing webhook,\n // which never loads an AI door, and a workspace's overage standing moving\n // late is a workspace charged or refused against a fact we already had.\n //\n // The handler modules are imported when the first event arrives, so a\n // process that never receives one pays only for the registration.\n if (!listPluginEventHandlers('billing.invoice.paid').includes(AI_PLUGIN_ID)) {\n registerPluginEventHandler(\n 'billing.invoice.paid',\n async (payload) => {\n const { onAiBillingInvoicePaid } = await import('./billing/ai-overage-events')\n await onAiBillingInvoicePaid(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'billing.invoice.failed',\n async (payload) => {\n const { onAiBillingInvoiceFailed } = await import('./billing/ai-overage-events')\n await onAiBillingInvoiceFailed(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'billing.invoice.closed',\n async (payload) => {\n const { onAiBillingInvoiceClosed } = await import('./billing/ai-overage-events')\n await onAiBillingInvoiceClosed(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'billing.dispute.opened',\n async (payload) => {\n const { onAiBillingDisputeOpened } = await import('./billing/ai-overage-events')\n await onAiBillingDisputeOpened(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'billing.paymentMethod.changed',\n async (payload) => {\n const { onAiBillingPaymentMethodChanged } = await import(\n './billing/ai-overage-events'\n )\n await onAiBillingPaymentMethodChanged(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n }\n // The meter line the plugin bills for itself from the cutover month\n // (AGL-3011). Registered at boot because the monthly usage sweep is a core\n // cron; the claim reads its own cutover month on every call, so it is\n // inert until the month is configured.\n if (pluginMeteredLineOwner(AI_OVERAGE_METER_LINE_ID) !== AI_PLUGIN_ID) {\n registerAiOverageMeteredLine()\n }\n if (!listPluginUserErasers().includes(AI_PLUGIN_ID)) {\n registerPluginUserEraser(\n async (request) => {\n const { eraseAiUsageForUser } = await import('./usage/ai-usage-eraser')\n return eraseAiUsageForUser(request)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n }\n if (\n !listUsageAlertContributors().some(\n (contributor) =>\n contributor.pluginId === AI_PLUGIN_ID &&\n contributor.id === AI_USAGE_ALERTS_ID,\n )\n ) {\n registerUsageAlertContributor({\n pluginId: AI_PLUGIN_ID,\n id: AI_USAGE_ALERTS_ID,\n evaluate: async (context) => {\n const { evaluateAiUsageAlerts } = await import('./usage/ai-usage-alerts')\n await evaluateAiUsageAlerts(context)\n },\n })\n }\n}\n\nregisterAiServerDeclarations()\n"],"names":["listPluginEventHandlers","registerPluginEventHandler","listPluginUserErasers","registerPluginUserEraser","listUsageAlertContributors","registerUsageAlertContributor","pluginMeteredLineOwner","AI_PLUGIN_ID","registerAiDeclarations","AI_OVERAGE_METER_LINE_ID","registerAiOverageMeteredLine","AI_USAGE_ALERTS_ID","registerAiServerDeclarations","includes","orgId","actor","before","after","logAiAddonChanged","pluginId","subject","permission","granted","startsWith","logAiPermissionChanged","payload","onAiBillingInvoicePaid","onAiBillingInvoiceFailed","onAiBillingInvoiceClosed","onAiBillingDisputeOpened","onAiBillingPaymentMethodChanged","request","eraseAiUsageForUser","some","contributor","id","evaluate","context","evaluateAiUsageAlerts"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/declarations.server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Each registry from its own module, not the `@aglyn/aglyn/server` barrel:\n// boot needs three registries rather than the whole server surface, and a\n// suite that stubs that barrel wholesale still loads these declarations\n// against the real registries.\nimport {\n listPluginEventHandlers,\n registerPluginEventHandler,\n} from '@aglyn/aglyn/plugin-manager/plugin-events'\nimport {\n listPluginUserErasers,\n registerPluginUserEraser,\n} from '@aglyn/aglyn/plugin-manager/plugin-user-erasure'\nimport {\n listUsageAlertContributors,\n registerUsageAlertContributor,\n} from '@aglyn/aglyn/plugin-manager/usage-alert-contributors'\nimport { pluginMeteredLineOwner } from '@aglyn/aglyn/plugin-manager/plugin-metered-lines'\nimport { registerOperatorAlerts } from '@aglyn/aglyn/plugin-manager/operator-alerts'\nimport { AI_PLUGIN_ID } from './constants'\nimport { AI_PROVIDER_UNAVAILABLE } from './operator-alerts'\nimport { registerAiDeclarations } from './declarations'\nimport {\n AI_OVERAGE_METER_LINE_ID,\n registerAiOverageMeteredLine,\n} from './billing/ai-overage-cutover'\n\n/** The provider-spend staff alerts' contributor id under the plugin. */\nconst AI_USAGE_ALERTS_ID = 'provider-spend'\n\n/**\n * The plugin's SERVER declarations (AGL-2939): what the server must know\n * at boot, before any door of the plugin has been called. The two platform\n * events the plugin writes activity for — the AI add-on bought or dropped,\n * an `ai.*` permission moved — are raised by core billing and membership\n * routes that never load the plugin's API surface, so the subscriptions\n * have to be in place from the first request. So does the eraser: an\n * account erasure is a core route too, and a person's AI usage months sit\n * under each org, keyed by the uid, where no core delete reaches them. So\n * does the usage alert contributor (AGL-2984): the usage-alerts sweep is a\n * core cron, and it evaluates the plugin's staff alerts on provider spend for\n * every org it reads.\n *\n * Light at boot by construction: the writers behind the handlers are\n * imported when the first event arrives, and the alert rules when the sweep\n * first evaluates an org, not when the process starts, so the boot cost is\n * the registration and nothing that touches Firestore.\n */\nexport function registerAiServerDeclarations(): void {\n registerAiDeclarations()\n // Idempotent against each REGISTRY rather than a module flag, so a\n // registry reset (a spec) or a second module instance registers again.\n if (!listPluginEventHandlers('org.seatAddons.changed').includes(AI_PLUGIN_ID)) {\n registerPluginEventHandler(\n 'org.seatAddons.changed',\n async ({ orgId, actor, before, after }) => {\n const { logAiAddonChanged } = await import('./activity/ai-activity')\n await logAiAddonChanged(orgId, actor, { before, after })\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'org.permissions.changed',\n async ({ orgId, actor, subject, permission, granted }) => {\n if (!permission.startsWith('ai.')) return\n const { logAiPermissionChanged } = await import('./activity/ai-activity')\n await logAiPermissionChanged(orgId, actor, { subject, permission, granted })\n },\n { pluginId: AI_PLUGIN_ID },\n )\n }\n // THE PLATFORM'S BILLING EVENTS (AGL-3011). Subscribed at boot for the\n // same reason as the two above: they are raised by the billing webhook,\n // which never loads an AI door, and a workspace's overage standing moving\n // late is a workspace charged or refused against a fact we already had.\n //\n // The handler modules are imported when the first event arrives, so a\n // process that never receives one pays only for the registration.\n if (!listPluginEventHandlers('billing.invoice.paid').includes(AI_PLUGIN_ID)) {\n registerPluginEventHandler(\n 'billing.invoice.paid',\n async (payload) => {\n const { onAiBillingInvoicePaid } = await import('./billing/ai-overage-events')\n await onAiBillingInvoicePaid(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'billing.invoice.failed',\n async (payload) => {\n const { onAiBillingInvoiceFailed } = await import('./billing/ai-overage-events')\n await onAiBillingInvoiceFailed(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'billing.invoice.closed',\n async (payload) => {\n const { onAiBillingInvoiceClosed } = await import('./billing/ai-overage-events')\n await onAiBillingInvoiceClosed(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'billing.dispute.opened',\n async (payload) => {\n const { onAiBillingDisputeOpened } = await import('./billing/ai-overage-events')\n await onAiBillingDisputeOpened(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n registerPluginEventHandler(\n 'billing.paymentMethod.changed',\n async (payload) => {\n const { onAiBillingPaymentMethodChanged } = await import(\n './billing/ai-overage-events'\n )\n await onAiBillingPaymentMethodChanged(payload)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n }\n // The meter line the plugin bills for itself from the cutover month\n // (AGL-3011). Registered at boot because the monthly usage sweep is a core\n // cron; the claim reads its own cutover month on every call, so it is\n // inert until the month is configured.\n if (pluginMeteredLineOwner(AI_OVERAGE_METER_LINE_ID) !== AI_PLUGIN_ID) {\n registerAiOverageMeteredLine()\n }\n if (!listPluginUserErasers().includes(AI_PLUGIN_ID)) {\n registerPluginUserEraser(\n async (request) => {\n const { eraseAiUsageForUser } = await import('./usage/ai-usage-eraser')\n return eraseAiUsageForUser(request)\n },\n { pluginId: AI_PLUGIN_ID },\n )\n }\n // The platform provider account alert (AGL-3377), listed on Staff →\n // Operator alerts before the first one is ever raised.\n registerOperatorAlerts([AI_PROVIDER_UNAVAILABLE], { pluginId: AI_PLUGIN_ID })\n if (\n !listUsageAlertContributors().some(\n (contributor) =>\n contributor.pluginId === AI_PLUGIN_ID &&\n contributor.id === AI_USAGE_ALERTS_ID,\n )\n ) {\n registerUsageAlertContributor({\n pluginId: AI_PLUGIN_ID,\n id: AI_USAGE_ALERTS_ID,\n evaluate: async (context) => {\n const { evaluateAiUsageAlerts } = await import('./usage/ai-usage-alerts')\n await evaluateAiUsageAlerts(context)\n },\n })\n }\n}\n\nregisterAiServerDeclarations()\n"],"names":["listPluginEventHandlers","registerPluginEventHandler","listPluginUserErasers","registerPluginUserEraser","listUsageAlertContributors","registerUsageAlertContributor","pluginMeteredLineOwner","registerOperatorAlerts","AI_PLUGIN_ID","AI_PROVIDER_UNAVAILABLE","registerAiDeclarations","AI_OVERAGE_METER_LINE_ID","registerAiOverageMeteredLine","AI_USAGE_ALERTS_ID","registerAiServerDeclarations","includes","orgId","actor","before","after","logAiAddonChanged","pluginId","subject","permission","granted","startsWith","logAiPermissionChanged","payload","onAiBillingInvoicePaid","onAiBillingInvoiceFailed","onAiBillingInvoiceClosed","onAiBillingDisputeOpened","onAiBillingPaymentMethodChanged","request","eraseAiUsageForUser","some","contributor","id","evaluate","context","evaluateAiUsageAlerts"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { OperatorAlertDefinition } from '@aglyn/aglyn/app-utils/operator-alerts';
|
|
18
|
+
/**
|
|
19
|
+
* The AI plugin's operator alert (AGL-3377): the platform's provider account
|
|
20
|
+
* refused its key or ran out of credit, so every AI feature on the install
|
|
21
|
+
* fails. Provider-generic — each adapter classifies its own vendor's answer
|
|
22
|
+
* as an `accountProblem`, and the runtime raises this with the provider's
|
|
23
|
+
* label — so a self-hoster on any provider hears the same thing.
|
|
24
|
+
*/
|
|
25
|
+
export declare const AI_PROVIDER_UNAVAILABLE: OperatorAlertDefinition;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { AI_PLUGIN_ID } from "./constants.js";
|
|
17
|
+
/**
|
|
18
|
+
* The AI plugin's operator alert (AGL-3377): the platform's provider account
|
|
19
|
+
* refused its key or ran out of credit, so every AI feature on the install
|
|
20
|
+
* fails. Provider-generic — each adapter classifies its own vendor's answer
|
|
21
|
+
* as an `accountProblem`, and the runtime raises this with the provider's
|
|
22
|
+
* label — so a self-hoster on any provider hears the same thing.
|
|
23
|
+
*/ export const AI_PROVIDER_UNAVAILABLE = {
|
|
24
|
+
type: 'ai.providerUnavailable',
|
|
25
|
+
pluginId: AI_PLUGIN_ID,
|
|
26
|
+
label: 'Platform AI provider unavailable',
|
|
27
|
+
description: 'The platform’s AI provider refused its key or ran out of credit, so every AI feature on the install is failing until the key or the balance is fixed.',
|
|
28
|
+
tier: 'should',
|
|
29
|
+
category: 'ops',
|
|
30
|
+
title: '{{provider}} is refusing requests',
|
|
31
|
+
body: 'The platform’s {{provider}} account refused a request because {{reason}}. AI features fail until it is fixed ({{keyEnv}}).',
|
|
32
|
+
delivery: 'immediate',
|
|
33
|
+
dedupeWindowMinutes: 6 * 60,
|
|
34
|
+
defaultEnabled: true
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=operator-alerts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/ai/src/lib/operator-alerts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { OperatorAlertDefinition } from '@aglyn/aglyn/app-utils/operator-alerts'\nimport { AI_PLUGIN_ID } from './constants'\n\n/**\n * The AI plugin's operator alert (AGL-3377): the platform's provider account\n * refused its key or ran out of credit, so every AI feature on the install\n * fails. Provider-generic — each adapter classifies its own vendor's answer\n * as an `accountProblem`, and the runtime raises this with the provider's\n * label — so a self-hoster on any provider hears the same thing.\n */\nexport const AI_PROVIDER_UNAVAILABLE: OperatorAlertDefinition = {\n type: 'ai.providerUnavailable',\n pluginId: AI_PLUGIN_ID,\n label: 'Platform AI provider unavailable',\n description:\n 'The platform’s AI provider refused its key or ran out of credit, so every AI feature on the install is failing until the key or the balance is fixed.',\n tier: 'should',\n category: 'ops',\n title: '{{provider}} is refusing requests',\n body:\n 'The platform’s {{provider}} account refused a request because {{reason}}. AI features fail until it is fixed ({{keyEnv}}).',\n delivery: 'immediate',\n dedupeWindowMinutes: 6 * 60,\n defaultEnabled: true,\n}\n"],"names":["AI_PLUGIN_ID","AI_PROVIDER_UNAVAILABLE","type","pluginId","label","description","tier","category","title","body","delivery","dedupeWindowMinutes","defaultEnabled"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,YAAY,QAAQ,iBAAa;AAE1C;;;;;;CAMC,GACD,OAAO,MAAMC,0BAAmD;IAC9DC,MAAM;IACNC,UAAUH;IACVI,OAAO;IACPC,aACE;IACFC,MAAM;IACNC,UAAU;IACVC,OAAO;IACPC,MACE;IACFC,UAAU;IACVC,qBAAqB,IAAI;IACzBC,gBAAgB;AAClB,EAAC"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type AiProvider, type AiProviderRequest, type AiToolSchemaLimits, type AiUsage } from './contract';
|
|
17
|
+
import { type AiAccountProblem, type AiProvider, type AiProviderRequest, type AiToolSchemaLimits, type AiUsage } from './contract';
|
|
18
18
|
/**
|
|
19
19
|
* The Anthropic adapter (AGL-2903, behind the contract since AGL-2939):
|
|
20
20
|
* the Messages API over raw `fetch`. No provider SDK is installed, and the
|
|
@@ -69,6 +69,14 @@ export declare const ANTHROPIC_TOOL_SCHEMA_LIMITS: AiToolSchemaLimits;
|
|
|
69
69
|
* status to go by.
|
|
70
70
|
*/
|
|
71
71
|
export declare function anthropicFailureIsRetryable(status: number | null, errorType: string | undefined): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Whether a failure is about the platform's Anthropic account rather than the
|
|
74
|
+
* request (AGL-3377): a refused key, or a balance too low to answer.
|
|
75
|
+
*/
|
|
76
|
+
export declare function anthropicAccountProblem(status: number | null, error: {
|
|
77
|
+
type?: string;
|
|
78
|
+
message?: string;
|
|
79
|
+
} | null | undefined): AiAccountProblem | null;
|
|
72
80
|
/** Anthropic's `usage` object, in the meter's shape. Missing fields read 0. */
|
|
73
81
|
export declare function anthropicUsageFrom(usage: unknown): AiUsage;
|
|
74
82
|
/**
|
|
@@ -78,6 +78,20 @@ const ANTHROPIC_VERSION = '2023-06-01';
|
|
|
78
78
|
*/ export function anthropicFailureIsRetryable(status, errorType) {
|
|
79
79
|
return status === 429 || status === 529 || errorType === 'rate_limit_error' || errorType === 'overloaded_error';
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether a failure is about the platform's Anthropic account rather than the
|
|
83
|
+
* request (AGL-3377): a refused key, or a balance too low to answer.
|
|
84
|
+
*/ export function anthropicAccountProblem(status, error) {
|
|
85
|
+
var _ref;
|
|
86
|
+
if (status === 401 || status === 403) return 'credentials';
|
|
87
|
+
if ((error == null ? void 0 : error.type) === 'authentication_error' || (error == null ? void 0 : error.type) === 'permission_error') {
|
|
88
|
+
return 'credentials';
|
|
89
|
+
}
|
|
90
|
+
if ((error == null ? void 0 : error.type) === 'billing_error' || /credit balance/i.test((_ref = error == null ? void 0 : error.message) != null ? _ref : '')) {
|
|
91
|
+
return 'credit';
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
81
95
|
/** Anthropic's `usage` object, in the meter's shape. Missing fields read 0. */ export function anthropicUsageFrom(usage) {
|
|
82
96
|
const record = usage != null ? usage : {};
|
|
83
97
|
// Part of `output_tokens`, and carried only where the response breaks them
|
|
@@ -208,7 +222,7 @@ async function send(input, stream) {
|
|
|
208
222
|
model: input.model,
|
|
209
223
|
error: (_ref = payload == null ? void 0 : payload.error) != null ? _ref : null
|
|
210
224
|
});
|
|
211
|
-
throw new AiUpstreamError(response.status, anthropicFailureIsRetryable(response.status, payload == null ? void 0 : (_payload_error = payload.error) == null ? void 0 : _payload_error.type), requestId);
|
|
225
|
+
throw new AiUpstreamError(response.status, anthropicFailureIsRetryable(response.status, payload == null ? void 0 : (_payload_error = payload.error) == null ? void 0 : _payload_error.type), requestId, anthropicAccountProblem(response.status, payload == null ? void 0 : payload.error));
|
|
212
226
|
}
|
|
213
227
|
return response;
|
|
214
228
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/providers/anthropic.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 { aiModelIdsForProvider, estimateAiBilledUsd, aiCatalogEntry } from './catalog'\nimport {\n AiUpstreamError,\n aiRawOutputOf,\n aiStoppedAtCeiling,\n aiTokenCount,\n aiToolInputOf,\n type AiMessage,\n type AiModelDescriptor,\n type AiProvider,\n type AiProviderRequest,\n type AiResult,\n type AiStreamEvent,\n type AiToolSchemaLimits,\n type AiToolUse,\n type AiUsage,\n} from './contract'\n\n/**\n * The Anthropic adapter (AGL-2903, behind the contract since AGL-2939):\n * the Messages API over raw `fetch`. No provider SDK is installed, and the\n * meter prices by model id from the catalog, so nothing an SDK would add\n * is used here. This file and the catalog are the only places in the\n * plugin that name the vendor's URL, header or model ids.\n *\n * ── Errors (AGL-2815) ─────────────────────────────────────────────────────\n *\n * Provider error text describes OUR account with the vendor — a key, a rate\n * limit, a balance — and names the vendor to a white-label org's users. It\n * never leaves this module as a message: a non-2xx answer becomes an\n * `AiUpstreamError` carrying the status, whether the failure is retryable\n * and a fixed customer-safe sentence; the provider's body goes to\n * `console.error` beside the request id, where an operator can find it.\n * A `refusal` stop is not an error at all — the tokens were spent and the\n * model answered — so it comes back as a typed `AiRefusal` result.\n */\n\nexport const ANTHROPIC_PROVIDER_ID = 'anthropic'\nexport const ANTHROPIC_API_KEY_ENV = 'ANTHROPIC_API_KEY'\nconst ANTHROPIC_HOST = 'api.anthropic.com'\nconst ANTHROPIC_URL = `https://${ANTHROPIC_HOST}/v1/messages`\nconst ANTHROPIC_VERSION = '2023-06-01'\n\n/** The Messages API accepts at most this many `cache_control` markers. */\nexport const ANTHROPIC_MAX_CACHE_BREAKPOINTS = 4\n\n/**\n * What the Messages API compiles of one request's strict schemas, as its\n * structured-outputs documentation states the limits: 20 strict tools, 24\n * optional parameters and 16 parameters with union types, each a total over\n * every strict schema the request carries. Past any of them the request is\n * refused with a 400 before the model runs.\n *\n * `compiledSchemaBytes` is not one of those. The API also refuses a request\n * whose schemas compile to too large a grammar — \"Simplify your tool schemas\n * or reduce the number of strict tools\" — and states no size for it, so this\n * bound is measured rather than read (AGL-3096). Its evidence is two live\n * requests for the automation draft, the largest strict schema anything here\n * sends:\n *\n * - 8,757 bytes, refused for grammar size (`req_011CfC6Fif9iJvmKPTRgtLdp`,\n * 2026-09-19), with 1 strict tool, 0 optional parameters and 2 unions:\n * every published count was green;\n * - 3,807 bytes, served (`req_011CfELn62WVLeaNzwK4TWgb`, 2026-09-20), the\n * same tool with the guard lifted out of the steps' union and one variant\n * per set of fields.\n *\n * Everything else the plugin sends is smaller still, the next largest being\n * the theme tool at 2,410 bytes. The bound sits at what has been served, so a\n * schema that grows past it has to be proved live before the bound moves.\n */\nexport const ANTHROPIC_TOOL_SCHEMA_LIMITS: AiToolSchemaLimits = {\n strictTools: 20,\n optionalParameters: 24,\n unionParameters: 16,\n compiledSchemaBytes: 3_807,\n}\n\n/**\n * Whether a provider failure is the kind that clears on its own. Matched on\n * the status AND the error type, because the in-stream `error` event has no\n * status to go by.\n */\nexport function anthropicFailureIsRetryable(\n status: number | null,\n errorType: string | undefined,\n): boolean {\n return (\n status === 429 ||\n status === 529 ||\n errorType === 'rate_limit_error' ||\n errorType === 'overloaded_error'\n )\n}\n\n/** Anthropic's `usage` object, in the meter's shape. Missing fields read 0. */\nexport function anthropicUsageFrom(usage: unknown): AiUsage {\n const record = (usage ?? {}) as Record<string, unknown>\n // Part of `output_tokens`, and carried only where the response breaks them\n // down, so an answer that reports none says nothing rather than zero.\n const details = record['output_tokens_details']\n const thinking =\n details && typeof details === 'object'\n ? (details as Record<string, unknown>)['thinking_tokens']\n : undefined\n return {\n inputTokens: aiTokenCount(record['input_tokens']),\n outputTokens: aiTokenCount(record['output_tokens']),\n cacheReadTokens: aiTokenCount(record['cache_read_input_tokens']),\n cacheWriteTokens: aiTokenCount(record['cache_creation_input_tokens']),\n ...(thinking === undefined ? {} : { thinkingTokens: aiTokenCount(thinking) }),\n }\n}\n\n/**\n * The Messages API body for a request. Exported so a spec can assert the\n * wire shape without a network. The cache-prefix rule is enforced by the\n * runtime before the request reaches any adapter; this maps the surviving\n * breakpoints onto `cache_control` and refuses only the count the API\n * itself refuses.\n */\nexport function buildAnthropicRequestBody(\n input: Omit<AiProviderRequest, 'apiKey' | 'signal'> & { stream: boolean },\n): Record<string, unknown> {\n const breakpoints = input.system.filter((block) => block.cacheBreakpoint)\n if (breakpoints.length > ANTHROPIC_MAX_CACHE_BREAKPOINTS) {\n throw new Error(\n `${breakpoints.length} cache breakpoints; the Messages API allows ${ANTHROPIC_MAX_CACHE_BREAKPOINTS}`,\n )\n }\n const system = input.system.map((block) => ({\n type: 'text',\n text: block.text,\n ...(block.cacheBreakpoint ? { cache_control: { type: 'ephemeral' } } : {}),\n }))\n const tools = input.tools?.map((tool) => ({\n name: tool.name,\n description: tool.description,\n input_schema: { ...tool.inputSchema, additionalProperties: false },\n strict: true,\n }))\n // A model that rejects an explicit thinking setting gets none, whatever\n // the door asked for, and no effort either: effort rides the same rule\n // (see the contract), and the catalog says which models those are.\n const settable = aiCatalogEntry(input.model)?.capabilities.thinking !== false\n const thinking = settable ? input.thinking : undefined\n const effort = settable ? input.effort : undefined\n return {\n model: input.model,\n max_tokens: input.maxTokens,\n ...(input.stream ? { stream: true } : {}),\n ...(thinking === 'off'\n ? { thinking: { type: 'disabled' } }\n : thinking === 'adaptive'\n ? { thinking: { type: 'adaptive' } }\n : {}),\n ...(effort ? { output_config: { effort } } : {}),\n ...(system.length ? { system } : {}),\n messages: input.messages.map((message) => ({\n role: message.role,\n content: anthropicContentOf(message),\n })),\n ...(tools?.length ? { tools, tool_choice: { type: 'auto' } } : {}),\n }\n}\n\n/**\n * A turn's content in the Messages API's shape: a text-only turn as the\n * string it is, and a turn with pictures as content blocks in order, each\n * picture a base64 image block (AGL-2916).\n */\nfunction anthropicContentOf(message: AiMessage): string | Array<Record<string, unknown>> {\n if (typeof message.content === 'string') return message.content\n return message.content.map((part) =>\n part.type === 'image'\n ? { type: 'image', source: { type: 'base64', media_type: part.mediaType, data: part.data } }\n : { type: 'text', text: part.text },\n )\n}\n\n/** The provider's request id, when it sent one — the log's join key. */\nfunction requestIdOf(response: Response): string | null {\n return response.headers?.get?.('request-id') ?? null\n}\n\nasync function send(\n input: AiProviderRequest,\n stream: boolean,\n): Promise<Response> {\n const body = buildAnthropicRequestBody({ ...input, stream })\n const response = await fetch(ANTHROPIC_URL, {\n method: 'POST',\n headers: {\n 'x-api-key': input.apiKey,\n 'anthropic-version': ANTHROPIC_VERSION,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n ...(input.signal ? { signal: input.signal } : {}),\n })\n if (!response.ok || (stream && !response.body)) {\n const requestId = requestIdOf(response)\n const payload = (await response.json().catch(() => null)) as {\n error?: { type?: string; message?: string }\n } | null\n console.error('ai upstream error', {\n provider: ANTHROPIC_PROVIDER_ID,\n status: response.status,\n requestId,\n model: input.model,\n error: payload?.error ?? null,\n })\n throw new AiUpstreamError(\n response.status,\n anthropicFailureIsRetryable(response.status, payload?.error?.type),\n requestId,\n )\n }\n return response\n}\n\nasync function complete(input: AiProviderRequest): Promise<AiResult> {\n const response = await send(input, false)\n const payload = (await response.json()) as {\n content?: Array<Record<string, unknown>>\n stop_reason?: unknown\n usage?: unknown\n }\n const blocks = Array.isArray(payload?.content) ? payload.content : []\n const text = blocks\n .filter((block) => block?.['type'] === 'text')\n .map((block) => String(block['text'] ?? ''))\n .join('')\n const toolUse: AiToolUse[] = blocks\n .filter((block) => block?.['type'] === 'tool_use')\n .map((block) => ({\n name: String(block['name'] ?? ''),\n input: aiToolInputOf(block['input']),\n }))\n const usage = anthropicUsageFrom(payload?.usage)\n const estCostUsd = estimateAiBilledUsd(usage, input.model)\n const stopReason =\n typeof payload?.stop_reason === 'string' && payload.stop_reason\n ? payload.stop_reason\n : null\n if (stopReason === 'refusal') {\n return { kind: 'refusal', text, usage, estCostUsd, stopReason }\n }\n return {\n kind: 'completion',\n text,\n toolUse,\n usage,\n estCostUsd,\n stopReason,\n // The blocks as the provider answered them, for a call its ceiling cut\n // off (AGL-3143): `toolUse` holds only what still parsed out of them.\n ...(aiStoppedAtCeiling(stopReason) ? { rawOutput: aiRawOutputOf(blocks) } : {}),\n }\n}\n\nasync function stream(input: AiProviderRequest): Promise<AsyncIterable<AiStreamEvent>> {\n const response = await send(input, true)\n return streamEvents(\n response.body as ReadableStream<Uint8Array>,\n input.model,\n requestIdOf(response),\n )\n}\n\n/**\n * Anthropic's SSE stream as contract events. Usage is taken from\n * `message_start` (input and cache figures) and `message_delta` (the\n * output count, and any cumulative input figures a newer API sends); the\n * `done` event carries whatever was known when the body ended, so a stream\n * cut short still meters what it cost.\n */\nasync function* streamEvents(\n body: ReadableStream<Uint8Array>,\n model: string,\n requestId: string | null,\n): AsyncGenerator<AiStreamEvent> {\n const decoder = new TextDecoder()\n const reader = body.getReader()\n const usage: AiUsage = {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n }\n let stopReason: string | null = null\n /** Tool-use blocks in flight, by content index; input arrives in pieces. */\n const tools = new Map<number, { name: string; json: string }>()\n /**\n * What each tool call was actually written in, in the order its block\n * opened (AGL-3143). The `tool` events below carry only as much of these\n * as `aiToolInputOf` could still parse, so for a call the ceiling cut off\n * this is the one place the emitted bytes survive.\n */\n const written: string[] = []\n let buffer = ''\n let drained = false\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) {\n drained = true\n break\n }\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue\n let event: Record<string, unknown>\n try {\n event = JSON.parse(line.slice('data: '.length))\n } catch {\n continue\n }\n const index = Number(event['index'])\n switch (event['type']) {\n case 'message_start': {\n const start = anthropicUsageFrom(\n (event['message'] as { usage?: unknown } | undefined)?.usage,\n )\n usage.inputTokens = start.inputTokens\n usage.cacheReadTokens = start.cacheReadTokens\n usage.cacheWriteTokens = start.cacheWriteTokens\n break\n }\n case 'content_block_start': {\n const block = event['content_block'] as\n | { type?: string; name?: string }\n | undefined\n if (block?.type === 'tool_use') {\n tools.set(index, { name: String(block.name ?? ''), json: '' })\n }\n break\n }\n case 'content_block_delta': {\n const delta = event['delta'] as\n | { type?: string; text?: string; partial_json?: string }\n | undefined\n if (delta?.type === 'text_delta' && delta.text) {\n yield { type: 'delta', text: delta.text }\n } else if (delta?.type === 'input_json_delta') {\n const tool = tools.get(index)\n if (tool) tool.json += delta.partial_json ?? ''\n }\n break\n }\n case 'content_block_stop': {\n const tool = tools.get(index)\n if (tool) {\n tools.delete(index)\n written.push(tool.json)\n yield { type: 'tool', name: tool.name, input: aiToolInputOf(tool.json) }\n }\n break\n }\n case 'message_delta': {\n const delta = anthropicUsageFrom(event['usage'])\n usage.outputTokens = delta.outputTokens || usage.outputTokens\n usage.inputTokens = delta.inputTokens || usage.inputTokens\n usage.cacheReadTokens = delta.cacheReadTokens || usage.cacheReadTokens\n usage.cacheWriteTokens = delta.cacheWriteTokens || usage.cacheWriteTokens\n // Kept where the breakdown is reported and left absent where it\n // is not (AGL-3143), never set to a zero the provider did not\n // send. A stream needs it for the reason a completion does: it\n // is the figure that accounts for a spent ceiling first, and it\n // arrives in the same usage object as the four above.\n if (delta.thinkingTokens !== undefined) usage.thinkingTokens = delta.thinkingTokens\n const reason = (event['delta'] as { stop_reason?: unknown } | undefined)\n ?.stop_reason\n if (typeof reason === 'string' && reason) stopReason = reason\n break\n }\n case 'error': {\n const error = event['error'] as\n | { type?: string; message?: string }\n | undefined\n console.error('ai upstream stream error', {\n provider: ANTHROPIC_PROVIDER_ID,\n requestId,\n model,\n error: error ?? null,\n })\n yield { type: 'error', retryable: anthropicFailureIsRetryable(null, error?.type) }\n break\n }\n default:\n break\n }\n }\n }\n } finally {\n // A consumer that stops iterating early — a client that hung up — must\n // not leave the provider generating into a socket nobody reads: cancel\n // tells the upstream to stop, and the tokens it has already produced are\n // the only ones billed.\n if (!drained) await reader.cancel().catch(() => undefined)\n reader.releaseLock()\n }\n // A message the ceiling ends mid-block never reaches that block's stop\n // event, so what it had written is still held in flight.\n for (const tool of tools.values()) written.push(tool.json)\n yield {\n type: 'done',\n usage,\n estCostUsd: estimateAiBilledUsd(usage, model),\n stopReason,\n // What the model emitted into its tool calls, for a stream its ceiling\n // cut off (AGL-3143): the `tool` events carry only what parsed out.\n ...(aiStoppedAtCeiling(stopReason) ? { rawOutput: aiRawOutputOf(written.join('')) } : {}),\n }\n}\n\nexport const anthropicProvider: AiProvider = {\n id: ANTHROPIC_PROVIDER_ID,\n label: 'Anthropic',\n apiKeyEnv: ANTHROPIC_API_KEY_ENV,\n readApiKey: () => process.env.ANTHROPIC_API_KEY?.trim() || undefined,\n endpointHost: ANTHROPIC_HOST,\n toolSchemaLimits: ANTHROPIC_TOOL_SCHEMA_LIMITS,\n models(): readonly AiModelDescriptor[] {\n return aiModelIdsForProvider(ANTHROPIC_PROVIDER_ID)\n .map((id) => aiCatalogEntry(id))\n .filter((entry): entry is NonNullable<typeof entry> => Boolean(entry))\n },\n complete,\n stream,\n}\n"],"names":["aiModelIdsForProvider","estimateAiBilledUsd","aiCatalogEntry","AiUpstreamError","aiRawOutputOf","aiStoppedAtCeiling","aiTokenCount","aiToolInputOf","ANTHROPIC_PROVIDER_ID","ANTHROPIC_API_KEY_ENV","ANTHROPIC_HOST","ANTHROPIC_URL","ANTHROPIC_VERSION","ANTHROPIC_MAX_CACHE_BREAKPOINTS","ANTHROPIC_TOOL_SCHEMA_LIMITS","strictTools","optionalParameters","unionParameters","compiledSchemaBytes","anthropicFailureIsRetryable","status","errorType","anthropicUsageFrom","usage","record","details","thinking","undefined","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","thinkingTokens","buildAnthropicRequestBody","input","breakpoints","system","filter","block","cacheBreakpoint","length","Error","map","type","text","cache_control","tools","tool","name","description","input_schema","inputSchema","additionalProperties","strict","settable","model","capabilities","effort","max_tokens","maxTokens","stream","output_config","messages","message","role","content","anthropicContentOf","tool_choice","part","source","media_type","mediaType","data","requestIdOf","response","headers","get","send","body","fetch","method","apiKey","JSON","stringify","signal","ok","payload","requestId","json","catch","console","error","provider","complete","blocks","Array","isArray","String","join","toolUse","estCostUsd","stopReason","stop_reason","kind","rawOutput","streamEvents","decoder","TextDecoder","reader","getReader","Map","written","buffer","drained","lines","done","value","read","decode","split","pop","line","startsWith","event","parse","slice","index","Number","start","set","delta","partial_json","delete","push","reason","retryable","cancel","releaseLock","values","anthropicProvider","id","label","apiKeyEnv","readApiKey","process","env","ANTHROPIC_API_KEY","trim","endpointHost","toolSchemaLimits","models","entry","Boolean"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/providers/anthropic.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 { aiModelIdsForProvider, estimateAiBilledUsd, aiCatalogEntry } from './catalog'\nimport {\n AiUpstreamError,\n type AiAccountProblem,\n aiRawOutputOf,\n aiStoppedAtCeiling,\n aiTokenCount,\n aiToolInputOf,\n type AiMessage,\n type AiModelDescriptor,\n type AiProvider,\n type AiProviderRequest,\n type AiResult,\n type AiStreamEvent,\n type AiToolSchemaLimits,\n type AiToolUse,\n type AiUsage,\n} from './contract'\n\n/**\n * The Anthropic adapter (AGL-2903, behind the contract since AGL-2939):\n * the Messages API over raw `fetch`. No provider SDK is installed, and the\n * meter prices by model id from the catalog, so nothing an SDK would add\n * is used here. This file and the catalog are the only places in the\n * plugin that name the vendor's URL, header or model ids.\n *\n * ── Errors (AGL-2815) ─────────────────────────────────────────────────────\n *\n * Provider error text describes OUR account with the vendor — a key, a rate\n * limit, a balance — and names the vendor to a white-label org's users. It\n * never leaves this module as a message: a non-2xx answer becomes an\n * `AiUpstreamError` carrying the status, whether the failure is retryable\n * and a fixed customer-safe sentence; the provider's body goes to\n * `console.error` beside the request id, where an operator can find it.\n * A `refusal` stop is not an error at all — the tokens were spent and the\n * model answered — so it comes back as a typed `AiRefusal` result.\n */\n\nexport const ANTHROPIC_PROVIDER_ID = 'anthropic'\nexport const ANTHROPIC_API_KEY_ENV = 'ANTHROPIC_API_KEY'\nconst ANTHROPIC_HOST = 'api.anthropic.com'\nconst ANTHROPIC_URL = `https://${ANTHROPIC_HOST}/v1/messages`\nconst ANTHROPIC_VERSION = '2023-06-01'\n\n/** The Messages API accepts at most this many `cache_control` markers. */\nexport const ANTHROPIC_MAX_CACHE_BREAKPOINTS = 4\n\n/**\n * What the Messages API compiles of one request's strict schemas, as its\n * structured-outputs documentation states the limits: 20 strict tools, 24\n * optional parameters and 16 parameters with union types, each a total over\n * every strict schema the request carries. Past any of them the request is\n * refused with a 400 before the model runs.\n *\n * `compiledSchemaBytes` is not one of those. The API also refuses a request\n * whose schemas compile to too large a grammar — \"Simplify your tool schemas\n * or reduce the number of strict tools\" — and states no size for it, so this\n * bound is measured rather than read (AGL-3096). Its evidence is two live\n * requests for the automation draft, the largest strict schema anything here\n * sends:\n *\n * - 8,757 bytes, refused for grammar size (`req_011CfC6Fif9iJvmKPTRgtLdp`,\n * 2026-09-19), with 1 strict tool, 0 optional parameters and 2 unions:\n * every published count was green;\n * - 3,807 bytes, served (`req_011CfELn62WVLeaNzwK4TWgb`, 2026-09-20), the\n * same tool with the guard lifted out of the steps' union and one variant\n * per set of fields.\n *\n * Everything else the plugin sends is smaller still, the next largest being\n * the theme tool at 2,410 bytes. The bound sits at what has been served, so a\n * schema that grows past it has to be proved live before the bound moves.\n */\nexport const ANTHROPIC_TOOL_SCHEMA_LIMITS: AiToolSchemaLimits = {\n strictTools: 20,\n optionalParameters: 24,\n unionParameters: 16,\n compiledSchemaBytes: 3_807,\n}\n\n/**\n * Whether a provider failure is the kind that clears on its own. Matched on\n * the status AND the error type, because the in-stream `error` event has no\n * status to go by.\n */\nexport function anthropicFailureIsRetryable(\n status: number | null,\n errorType: string | undefined,\n): boolean {\n return (\n status === 429 ||\n status === 529 ||\n errorType === 'rate_limit_error' ||\n errorType === 'overloaded_error'\n )\n}\n\n/**\n * Whether a failure is about the platform's Anthropic account rather than the\n * request (AGL-3377): a refused key, or a balance too low to answer.\n */\nexport function anthropicAccountProblem(\n status: number | null,\n error: { type?: string; message?: string } | null | undefined,\n): AiAccountProblem | null {\n if (status === 401 || status === 403) return 'credentials'\n if (error?.type === 'authentication_error' || error?.type === 'permission_error') {\n return 'credentials'\n }\n if (error?.type === 'billing_error' || /credit balance/i.test(error?.message ?? '')) {\n return 'credit'\n }\n return null\n}\n\n/** Anthropic's `usage` object, in the meter's shape. Missing fields read 0. */\nexport function anthropicUsageFrom(usage: unknown): AiUsage {\n const record = (usage ?? {}) as Record<string, unknown>\n // Part of `output_tokens`, and carried only where the response breaks them\n // down, so an answer that reports none says nothing rather than zero.\n const details = record['output_tokens_details']\n const thinking =\n details && typeof details === 'object'\n ? (details as Record<string, unknown>)['thinking_tokens']\n : undefined\n return {\n inputTokens: aiTokenCount(record['input_tokens']),\n outputTokens: aiTokenCount(record['output_tokens']),\n cacheReadTokens: aiTokenCount(record['cache_read_input_tokens']),\n cacheWriteTokens: aiTokenCount(record['cache_creation_input_tokens']),\n ...(thinking === undefined ? {} : { thinkingTokens: aiTokenCount(thinking) }),\n }\n}\n\n/**\n * The Messages API body for a request. Exported so a spec can assert the\n * wire shape without a network. The cache-prefix rule is enforced by the\n * runtime before the request reaches any adapter; this maps the surviving\n * breakpoints onto `cache_control` and refuses only the count the API\n * itself refuses.\n */\nexport function buildAnthropicRequestBody(\n input: Omit<AiProviderRequest, 'apiKey' | 'signal'> & { stream: boolean },\n): Record<string, unknown> {\n const breakpoints = input.system.filter((block) => block.cacheBreakpoint)\n if (breakpoints.length > ANTHROPIC_MAX_CACHE_BREAKPOINTS) {\n throw new Error(\n `${breakpoints.length} cache breakpoints; the Messages API allows ${ANTHROPIC_MAX_CACHE_BREAKPOINTS}`,\n )\n }\n const system = input.system.map((block) => ({\n type: 'text',\n text: block.text,\n ...(block.cacheBreakpoint ? { cache_control: { type: 'ephemeral' } } : {}),\n }))\n const tools = input.tools?.map((tool) => ({\n name: tool.name,\n description: tool.description,\n input_schema: { ...tool.inputSchema, additionalProperties: false },\n strict: true,\n }))\n // A model that rejects an explicit thinking setting gets none, whatever\n // the door asked for, and no effort either: effort rides the same rule\n // (see the contract), and the catalog says which models those are.\n const settable = aiCatalogEntry(input.model)?.capabilities.thinking !== false\n const thinking = settable ? input.thinking : undefined\n const effort = settable ? input.effort : undefined\n return {\n model: input.model,\n max_tokens: input.maxTokens,\n ...(input.stream ? { stream: true } : {}),\n ...(thinking === 'off'\n ? { thinking: { type: 'disabled' } }\n : thinking === 'adaptive'\n ? { thinking: { type: 'adaptive' } }\n : {}),\n ...(effort ? { output_config: { effort } } : {}),\n ...(system.length ? { system } : {}),\n messages: input.messages.map((message) => ({\n role: message.role,\n content: anthropicContentOf(message),\n })),\n ...(tools?.length ? { tools, tool_choice: { type: 'auto' } } : {}),\n }\n}\n\n/**\n * A turn's content in the Messages API's shape: a text-only turn as the\n * string it is, and a turn with pictures as content blocks in order, each\n * picture a base64 image block (AGL-2916).\n */\nfunction anthropicContentOf(message: AiMessage): string | Array<Record<string, unknown>> {\n if (typeof message.content === 'string') return message.content\n return message.content.map((part) =>\n part.type === 'image'\n ? { type: 'image', source: { type: 'base64', media_type: part.mediaType, data: part.data } }\n : { type: 'text', text: part.text },\n )\n}\n\n/** The provider's request id, when it sent one — the log's join key. */\nfunction requestIdOf(response: Response): string | null {\n return response.headers?.get?.('request-id') ?? null\n}\n\nasync function send(\n input: AiProviderRequest,\n stream: boolean,\n): Promise<Response> {\n const body = buildAnthropicRequestBody({ ...input, stream })\n const response = await fetch(ANTHROPIC_URL, {\n method: 'POST',\n headers: {\n 'x-api-key': input.apiKey,\n 'anthropic-version': ANTHROPIC_VERSION,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n ...(input.signal ? { signal: input.signal } : {}),\n })\n if (!response.ok || (stream && !response.body)) {\n const requestId = requestIdOf(response)\n const payload = (await response.json().catch(() => null)) as {\n error?: { type?: string; message?: string }\n } | null\n console.error('ai upstream error', {\n provider: ANTHROPIC_PROVIDER_ID,\n status: response.status,\n requestId,\n model: input.model,\n error: payload?.error ?? null,\n })\n throw new AiUpstreamError(\n response.status,\n anthropicFailureIsRetryable(response.status, payload?.error?.type),\n requestId,\n anthropicAccountProblem(response.status, payload?.error),\n )\n }\n return response\n}\n\nasync function complete(input: AiProviderRequest): Promise<AiResult> {\n const response = await send(input, false)\n const payload = (await response.json()) as {\n content?: Array<Record<string, unknown>>\n stop_reason?: unknown\n usage?: unknown\n }\n const blocks = Array.isArray(payload?.content) ? payload.content : []\n const text = blocks\n .filter((block) => block?.['type'] === 'text')\n .map((block) => String(block['text'] ?? ''))\n .join('')\n const toolUse: AiToolUse[] = blocks\n .filter((block) => block?.['type'] === 'tool_use')\n .map((block) => ({\n name: String(block['name'] ?? ''),\n input: aiToolInputOf(block['input']),\n }))\n const usage = anthropicUsageFrom(payload?.usage)\n const estCostUsd = estimateAiBilledUsd(usage, input.model)\n const stopReason =\n typeof payload?.stop_reason === 'string' && payload.stop_reason\n ? payload.stop_reason\n : null\n if (stopReason === 'refusal') {\n return { kind: 'refusal', text, usage, estCostUsd, stopReason }\n }\n return {\n kind: 'completion',\n text,\n toolUse,\n usage,\n estCostUsd,\n stopReason,\n // The blocks as the provider answered them, for a call its ceiling cut\n // off (AGL-3143): `toolUse` holds only what still parsed out of them.\n ...(aiStoppedAtCeiling(stopReason) ? { rawOutput: aiRawOutputOf(blocks) } : {}),\n }\n}\n\nasync function stream(input: AiProviderRequest): Promise<AsyncIterable<AiStreamEvent>> {\n const response = await send(input, true)\n return streamEvents(\n response.body as ReadableStream<Uint8Array>,\n input.model,\n requestIdOf(response),\n )\n}\n\n/**\n * Anthropic's SSE stream as contract events. Usage is taken from\n * `message_start` (input and cache figures) and `message_delta` (the\n * output count, and any cumulative input figures a newer API sends); the\n * `done` event carries whatever was known when the body ended, so a stream\n * cut short still meters what it cost.\n */\nasync function* streamEvents(\n body: ReadableStream<Uint8Array>,\n model: string,\n requestId: string | null,\n): AsyncGenerator<AiStreamEvent> {\n const decoder = new TextDecoder()\n const reader = body.getReader()\n const usage: AiUsage = {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n }\n let stopReason: string | null = null\n /** Tool-use blocks in flight, by content index; input arrives in pieces. */\n const tools = new Map<number, { name: string; json: string }>()\n /**\n * What each tool call was actually written in, in the order its block\n * opened (AGL-3143). The `tool` events below carry only as much of these\n * as `aiToolInputOf` could still parse, so for a call the ceiling cut off\n * this is the one place the emitted bytes survive.\n */\n const written: string[] = []\n let buffer = ''\n let drained = false\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) {\n drained = true\n break\n }\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue\n let event: Record<string, unknown>\n try {\n event = JSON.parse(line.slice('data: '.length))\n } catch {\n continue\n }\n const index = Number(event['index'])\n switch (event['type']) {\n case 'message_start': {\n const start = anthropicUsageFrom(\n (event['message'] as { usage?: unknown } | undefined)?.usage,\n )\n usage.inputTokens = start.inputTokens\n usage.cacheReadTokens = start.cacheReadTokens\n usage.cacheWriteTokens = start.cacheWriteTokens\n break\n }\n case 'content_block_start': {\n const block = event['content_block'] as\n | { type?: string; name?: string }\n | undefined\n if (block?.type === 'tool_use') {\n tools.set(index, { name: String(block.name ?? ''), json: '' })\n }\n break\n }\n case 'content_block_delta': {\n const delta = event['delta'] as\n | { type?: string; text?: string; partial_json?: string }\n | undefined\n if (delta?.type === 'text_delta' && delta.text) {\n yield { type: 'delta', text: delta.text }\n } else if (delta?.type === 'input_json_delta') {\n const tool = tools.get(index)\n if (tool) tool.json += delta.partial_json ?? ''\n }\n break\n }\n case 'content_block_stop': {\n const tool = tools.get(index)\n if (tool) {\n tools.delete(index)\n written.push(tool.json)\n yield { type: 'tool', name: tool.name, input: aiToolInputOf(tool.json) }\n }\n break\n }\n case 'message_delta': {\n const delta = anthropicUsageFrom(event['usage'])\n usage.outputTokens = delta.outputTokens || usage.outputTokens\n usage.inputTokens = delta.inputTokens || usage.inputTokens\n usage.cacheReadTokens = delta.cacheReadTokens || usage.cacheReadTokens\n usage.cacheWriteTokens = delta.cacheWriteTokens || usage.cacheWriteTokens\n // Kept where the breakdown is reported and left absent where it\n // is not (AGL-3143), never set to a zero the provider did not\n // send. A stream needs it for the reason a completion does: it\n // is the figure that accounts for a spent ceiling first, and it\n // arrives in the same usage object as the four above.\n if (delta.thinkingTokens !== undefined) usage.thinkingTokens = delta.thinkingTokens\n const reason = (event['delta'] as { stop_reason?: unknown } | undefined)\n ?.stop_reason\n if (typeof reason === 'string' && reason) stopReason = reason\n break\n }\n case 'error': {\n const error = event['error'] as\n | { type?: string; message?: string }\n | undefined\n console.error('ai upstream stream error', {\n provider: ANTHROPIC_PROVIDER_ID,\n requestId,\n model,\n error: error ?? null,\n })\n yield { type: 'error', retryable: anthropicFailureIsRetryable(null, error?.type) }\n break\n }\n default:\n break\n }\n }\n }\n } finally {\n // A consumer that stops iterating early — a client that hung up — must\n // not leave the provider generating into a socket nobody reads: cancel\n // tells the upstream to stop, and the tokens it has already produced are\n // the only ones billed.\n if (!drained) await reader.cancel().catch(() => undefined)\n reader.releaseLock()\n }\n // A message the ceiling ends mid-block never reaches that block's stop\n // event, so what it had written is still held in flight.\n for (const tool of tools.values()) written.push(tool.json)\n yield {\n type: 'done',\n usage,\n estCostUsd: estimateAiBilledUsd(usage, model),\n stopReason,\n // What the model emitted into its tool calls, for a stream its ceiling\n // cut off (AGL-3143): the `tool` events carry only what parsed out.\n ...(aiStoppedAtCeiling(stopReason) ? { rawOutput: aiRawOutputOf(written.join('')) } : {}),\n }\n}\n\nexport const anthropicProvider: AiProvider = {\n id: ANTHROPIC_PROVIDER_ID,\n label: 'Anthropic',\n apiKeyEnv: ANTHROPIC_API_KEY_ENV,\n readApiKey: () => process.env.ANTHROPIC_API_KEY?.trim() || undefined,\n endpointHost: ANTHROPIC_HOST,\n toolSchemaLimits: ANTHROPIC_TOOL_SCHEMA_LIMITS,\n models(): readonly AiModelDescriptor[] {\n return aiModelIdsForProvider(ANTHROPIC_PROVIDER_ID)\n .map((id) => aiCatalogEntry(id))\n .filter((entry): entry is NonNullable<typeof entry> => Boolean(entry))\n },\n complete,\n stream,\n}\n"],"names":["aiModelIdsForProvider","estimateAiBilledUsd","aiCatalogEntry","AiUpstreamError","aiRawOutputOf","aiStoppedAtCeiling","aiTokenCount","aiToolInputOf","ANTHROPIC_PROVIDER_ID","ANTHROPIC_API_KEY_ENV","ANTHROPIC_HOST","ANTHROPIC_URL","ANTHROPIC_VERSION","ANTHROPIC_MAX_CACHE_BREAKPOINTS","ANTHROPIC_TOOL_SCHEMA_LIMITS","strictTools","optionalParameters","unionParameters","compiledSchemaBytes","anthropicFailureIsRetryable","status","errorType","anthropicAccountProblem","error","type","test","message","anthropicUsageFrom","usage","record","details","thinking","undefined","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","thinkingTokens","buildAnthropicRequestBody","input","breakpoints","system","filter","block","cacheBreakpoint","length","Error","map","text","cache_control","tools","tool","name","description","input_schema","inputSchema","additionalProperties","strict","settable","model","capabilities","effort","max_tokens","maxTokens","stream","output_config","messages","role","content","anthropicContentOf","tool_choice","part","source","media_type","mediaType","data","requestIdOf","response","headers","get","send","body","fetch","method","apiKey","JSON","stringify","signal","ok","payload","requestId","json","catch","console","provider","complete","blocks","Array","isArray","String","join","toolUse","estCostUsd","stopReason","stop_reason","kind","rawOutput","streamEvents","decoder","TextDecoder","reader","getReader","Map","written","buffer","drained","lines","done","value","read","decode","split","pop","line","startsWith","event","parse","slice","index","Number","start","set","delta","partial_json","delete","push","reason","retryable","cancel","releaseLock","values","anthropicProvider","id","label","apiKeyEnv","readApiKey","process","env","ANTHROPIC_API_KEY","trim","endpointHost","toolSchemaLimits","models","entry","Boolean"],"mappings":"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"}
|
|
@@ -400,8 +400,18 @@ export declare const AI_UPSTREAM_FAILURE_COPY = "The AI request failed \u2014 tr
|
|
|
400
400
|
*/
|
|
401
401
|
export declare class AiUpstreamError extends UpstreamServiceError {
|
|
402
402
|
readonly name = "AiUpstreamError";
|
|
403
|
-
|
|
403
|
+
/**
|
|
404
|
+
* A failure about the PLATFORM's account with the provider rather than the
|
|
405
|
+
* request (AGL-3377): the key was refused, or the account is out of
|
|
406
|
+
* credit. Every AI feature on the install fails until the operator fixes
|
|
407
|
+
* it, so the runtime tells them. Each adapter classifies its own vendor's
|
|
408
|
+
* answers; absent is an ordinary failure.
|
|
409
|
+
*/
|
|
410
|
+
readonly accountProblem: AiAccountProblem | null;
|
|
411
|
+
constructor(status: number | null, retryable: boolean, requestId: string | null, accountProblem?: AiAccountProblem | null);
|
|
404
412
|
}
|
|
413
|
+
/** See {@link AiUpstreamError.accountProblem}. */
|
|
414
|
+
export type AiAccountProblem = 'credentials' | 'credit';
|
|
405
415
|
/**
|
|
406
416
|
* A request the runtime refuses to send. Thrown synchronously before any
|
|
407
417
|
* network is touched, and it names the block at fault: this is a
|
|
@@ -226,12 +226,13 @@ function countSchema(node, root, counts, resolving) {
|
|
|
226
226
|
* A non-2xx answer from the provider. `message` is the fixed customer-safe
|
|
227
227
|
* sentence; the provider's payload is in the log under `requestId`.
|
|
228
228
|
*/ export class AiUpstreamError extends UpstreamServiceError {
|
|
229
|
-
constructor(status, retryable, requestId){
|
|
229
|
+
constructor(status, retryable, requestId, accountProblem = null){
|
|
230
230
|
super(AI_UPSTREAM_FAILURE_COPY, {
|
|
231
231
|
status,
|
|
232
232
|
retryable,
|
|
233
233
|
requestId
|
|
234
234
|
}), this.name = 'AiUpstreamError';
|
|
235
|
+
this.accountProblem = accountProblem;
|
|
235
236
|
}
|
|
236
237
|
}
|
|
237
238
|
/**
|
|
@@ -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 */\nexport interface AiSystemBlock {\n text: string\n cacheBreakpoint?: true\n volatile?: 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\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/**\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 `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 constructor(status: number | null, retryable: boolean, requestId: string | null) {\n super(AI_UPSTREAM_FAILURE_COPY, { status, retryable, requestId })\n }\n}\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\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","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","name","AiRequestShapeError","Error","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 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 */\nexport interface AiSystemBlock {\n text: string\n cacheBreakpoint?: true\n volatile?: 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\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/**\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 `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\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","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","aiTokenCount","Number","isFinite","aiToolInputOf","trim","parse"],"mappings":"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"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type AiProvider, type AiProviderRequest, type AiToolSchemaLimits, type AiUsage } from './contract';
|
|
17
|
+
import { type AiAccountProblem, type AiProvider, type AiProviderRequest, type AiToolSchemaLimits, type AiUsage } from './contract';
|
|
18
18
|
/**
|
|
19
19
|
* The OpenAI-compatible adapter (AGL-2939): `POST {base}/chat/completions`
|
|
20
20
|
* against whatever endpoint `AI_OPENAI_COMPAT_BASE_URL` names — a vendor,
|
|
@@ -55,4 +55,14 @@ export declare function openAiCompatibleUsageFrom(usage: unknown): AiUsage;
|
|
|
55
55
|
export declare function buildOpenAiCompatibleRequestBody(input: Omit<AiProviderRequest, 'apiKey' | 'signal'> & {
|
|
56
56
|
stream: boolean;
|
|
57
57
|
}): Record<string, unknown>;
|
|
58
|
+
/**
|
|
59
|
+
* Whether a failure is about the platform's account rather than the request
|
|
60
|
+
* (AGL-3377): a refused key, or a quota or balance that has run out. The
|
|
61
|
+
* OpenAI-shaped `insufficient_quota` code is what compatible vendors send
|
|
62
|
+
* for an empty balance, on a 429 that is otherwise a rate limit.
|
|
63
|
+
*/
|
|
64
|
+
export declare function openAiCompatibleAccountProblem(status: number | null, error: {
|
|
65
|
+
type?: string;
|
|
66
|
+
code?: string;
|
|
67
|
+
} | null | undefined): AiAccountProblem | null;
|
|
58
68
|
export declare const openAiCompatibleProvider: AiProvider;
|
|
@@ -175,10 +175,23 @@ async function send(input, stream) {
|
|
|
175
175
|
model: input.model,
|
|
176
176
|
error: (_ref = payload == null ? void 0 : payload.error) != null ? _ref : null
|
|
177
177
|
});
|
|
178
|
-
throw new AiUpstreamError(response.status, openAiCompatibleFailureIsRetryable(response.status), requestId);
|
|
178
|
+
throw new AiUpstreamError(response.status, openAiCompatibleFailureIsRetryable(response.status), requestId, openAiCompatibleAccountProblem(response.status, payload == null ? void 0 : payload.error));
|
|
179
179
|
}
|
|
180
180
|
return response;
|
|
181
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Whether a failure is about the platform's account rather than the request
|
|
184
|
+
* (AGL-3377): a refused key, or a quota or balance that has run out. The
|
|
185
|
+
* OpenAI-shaped `insufficient_quota` code is what compatible vendors send
|
|
186
|
+
* for an empty balance, on a 429 that is otherwise a rate limit.
|
|
187
|
+
*/ export function openAiCompatibleAccountProblem(status, error) {
|
|
188
|
+
if (status === 401 || status === 403) return 'credentials';
|
|
189
|
+
if (status === 402) return 'credit';
|
|
190
|
+
if ((error == null ? void 0 : error.code) === 'insufficient_quota' || (error == null ? void 0 : error.type) === 'insufficient_quota') {
|
|
191
|
+
return 'credit';
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
182
195
|
/** `finish_reason` → the contract's stop reason; a filter is a refusal. */ function stopReasonOf(finish) {
|
|
183
196
|
if (typeof finish !== 'string' || !finish) return null;
|
|
184
197
|
if (finish === 'content_filter') return 'refusal';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/providers/openai-compatible.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 { aiCatalogEntry, aiModelIdsForProvider, estimateAiBilledUsd } from './catalog'\nimport {\n AiUpstreamError,\n aiMessageText,\n aiRawOutputOf,\n aiStoppedAtCeiling,\n aiTokenCount,\n aiToolInputOf,\n type AiMessage,\n type AiModelDescriptor,\n type AiProvider,\n type AiProviderRequest,\n type AiResult,\n type AiStreamEvent,\n type AiToolSchemaLimits,\n type AiToolUse,\n type AiUsage,\n} from './contract'\n\n/**\n * The OpenAI-compatible adapter (AGL-2939): `POST {base}/chat/completions`\n * against whatever endpoint `AI_OPENAI_COMPAT_BASE_URL` names — a vendor,\n * a gateway, a self-hosted model server — with the key in\n * `AI_OPENAI_COMPAT_API_KEY`. The second adapter behind the contract, and\n * the proof that the runtime, the gate, the jobs and the meter never\n * depended on the first vendor's wire shape.\n *\n * What the mapping gives up, stated rather than hidden: the endpoint has\n * no explicit cache breakpoints, so the ordered system blocks become one\n * system message in order (the runtime's prefix rule still holds, so the\n * order is cache-friendly for an endpoint that caches prefixes on its own),\n * and `cacheReadTokens` is whatever `prompt_tokens_details.cached_tokens`\n * reports. A `content_filter` finish is the endpoint's decline and becomes\n * an `AiRefusal`, the way a refusal stop does on the other adapter.\n */\n\nexport const OPENAI_COMPATIBLE_PROVIDER_ID = 'openai-compatible'\nexport const OPENAI_COMPAT_BASE_URL_ENV = 'AI_OPENAI_COMPAT_BASE_URL'\nexport const OPENAI_COMPAT_API_KEY_ENV = 'AI_OPENAI_COMPAT_API_KEY'\n\n/** The configured base, with any trailing slash and `/chat/completions` stripped. */\nexport function openAiCompatibleBaseUrl(): string | null {\n const raw = process.env[OPENAI_COMPAT_BASE_URL_ENV]?.trim()\n if (!raw) return null\n return raw.replace(/\\/+$/, '').replace(/\\/chat\\/completions$/, '')\n}\n\n/** The host the endpoint is reached at, for the subprocessor inventory. */\nexport function openAiCompatibleHost(): string {\n const base = openAiCompatibleBaseUrl()\n if (!base) return ''\n try {\n return new URL(base).host\n } catch {\n return ''\n }\n}\n\n/**\n * What the strict function shape this adapter sends compiles: every property\n * of every object listed in `required`, so no optional parameter at all — an\n * optional field is a union with `null` instead. No other bound is stated:\n * the endpoint is whatever `AI_OPENAI_COMPAT_BASE_URL` names, and no one\n * count of tools or unions — nor any grammar size — holds for every endpoint\n * that speaks the shape. A bound nothing here has watched an endpoint hold to\n * would be a guess, and a guess in a guard is worse than a gap in one.\n */\nexport const OPENAI_COMPATIBLE_TOOL_SCHEMA_LIMITS: AiToolSchemaLimits = {\n optionalParameters: 0,\n}\n\nexport function openAiCompatibleFailureIsRetryable(status: number | null): boolean {\n return status === 429 || status === 503 || status === 529\n}\n\n/** The endpoint's `usage` object, in the meter's shape. Missing fields read 0. */\nexport function openAiCompatibleUsageFrom(usage: unknown): AiUsage {\n const record = (usage ?? {}) as Record<string, unknown>\n const details = (record['prompt_tokens_details'] ?? {}) as Record<string, unknown>\n const prompt = aiTokenCount(record['prompt_tokens'])\n const cached = Math.min(aiTokenCount(details['cached_tokens']), prompt)\n return {\n // The endpoint counts cached tokens inside `prompt_tokens`; the meter\n // prices them apart, so they are split out here.\n inputTokens: prompt - cached,\n outputTokens: aiTokenCount(record['completion_tokens']),\n cacheReadTokens: cached,\n cacheWriteTokens: 0,\n }\n}\n\n/** The chat-completions body. Exported so a spec can assert the wire shape. */\nexport function buildOpenAiCompatibleRequestBody(\n input: Omit<AiProviderRequest, 'apiKey' | 'signal'> & { stream: boolean },\n): Record<string, unknown> {\n const system = input.system.map((block) => block.text).join('\\n\\n')\n const tools = input.tools?.map((tool) => ({\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description,\n parameters: { ...tool.inputSchema, additionalProperties: false },\n strict: true,\n },\n }))\n return {\n model: input.model,\n max_tokens: input.maxTokens,\n ...(input.stream ? { stream: true, stream_options: { include_usage: true } } : {}),\n messages: [\n ...(system ? [{ role: 'system', content: system }] : []),\n ...input.messages.map((message) => ({\n role: message.role,\n content: openAiCompatibleContentOf(message),\n })),\n ],\n ...(tools?.length ? { tools, tool_choice: 'auto' } : {}),\n }\n}\n\n/**\n * A turn's content in the chat-completions shape: a string for a turn with\n * nothing but text, and content parts for a user turn with pictures, each\n * picture an `image_url` carrying its bytes as a `data:` URL, so the endpoint\n * fetches nothing (AGL-2916). An assistant turn is always a string: the\n * runtime admits no picture there, and not every endpoint takes parts on it.\n */\nfunction openAiCompatibleContentOf(message: AiMessage): string | Array<Record<string, unknown>> {\n if (typeof message.content === 'string') return message.content\n if (message.role === 'assistant') return aiMessageText(message)\n return message.content.map((part) =>\n part.type === 'image'\n ? { type: 'image_url', image_url: { url: `data:${part.mediaType};base64,${part.data}` } }\n : { type: 'text', text: part.text },\n )\n}\n\nfunction requestIdOf(response: Response): string | null {\n return response.headers?.get?.('x-request-id') ?? null\n}\n\nasync function send(input: AiProviderRequest, stream: boolean): Promise<Response> {\n const base = openAiCompatibleBaseUrl()\n if (!base) {\n // Every door gates on the provider being configured before it gets\n // here, so this is a wiring fault rather than a customer path.\n throw new Error(`${OPENAI_COMPAT_BASE_URL_ENV} is not set`)\n }\n const body = buildOpenAiCompatibleRequestBody({ ...input, stream })\n const response = await fetch(`${base}/chat/completions`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${input.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n ...(input.signal ? { signal: input.signal } : {}),\n })\n if (!response.ok || (stream && !response.body)) {\n const requestId = requestIdOf(response)\n const payload = (await response.json().catch(() => null)) as {\n error?: { type?: string; message?: string; code?: string }\n } | null\n console.error('ai upstream error', {\n provider: OPENAI_COMPATIBLE_PROVIDER_ID,\n status: response.status,\n requestId,\n model: input.model,\n error: payload?.error ?? null,\n })\n throw new AiUpstreamError(\n response.status,\n openAiCompatibleFailureIsRetryable(response.status),\n requestId,\n )\n }\n return response\n}\n\n/** `finish_reason` → the contract's stop reason; a filter is a refusal. */\nfunction stopReasonOf(finish: unknown): string | null {\n if (typeof finish !== 'string' || !finish) return null\n if (finish === 'content_filter') return 'refusal'\n if (finish === 'stop') return 'end_turn'\n if (finish === 'length') return 'max_tokens'\n if (finish === 'tool_calls') return 'tool_use'\n return finish\n}\n\nasync function complete(input: AiProviderRequest): Promise<AiResult> {\n const response = await send(input, false)\n const payload = (await response.json()) as {\n choices?: Array<{\n message?: {\n content?: unknown\n tool_calls?: Array<{ function?: { name?: unknown; arguments?: unknown } }>\n }\n finish_reason?: unknown\n }>\n usage?: unknown\n }\n const choice = payload?.choices?.[0]\n const text = typeof choice?.message?.content === 'string' ? choice.message.content : ''\n const toolUse: AiToolUse[] = (choice?.message?.tool_calls ?? []).map((call) => ({\n name: String(call?.function?.name ?? ''),\n input: aiToolInputOf(call?.function?.arguments),\n }))\n const usage = openAiCompatibleUsageFrom(payload?.usage)\n const estCostUsd = estimateAiBilledUsd(usage, input.model)\n const stopReason = stopReasonOf(choice?.finish_reason)\n if (stopReason === 'refusal') {\n return { kind: 'refusal', text, usage, estCostUsd, stopReason }\n }\n return {\n kind: 'completion',\n text,\n toolUse,\n usage,\n estCostUsd,\n stopReason,\n // The message as the endpoint answered it, for a call its ceiling cut off\n // (AGL-3143). A tool call's `arguments` is raw JSON text here, and this\n // is the only place the bytes it was cut in survive the parse.\n ...(aiStoppedAtCeiling(stopReason) ? { rawOutput: aiRawOutputOf(choice?.message) } : {}),\n }\n}\n\nasync function stream(input: AiProviderRequest): Promise<AsyncIterable<AiStreamEvent>> {\n const response = await send(input, true)\n return streamEvents(\n response.body as ReadableStream<Uint8Array>,\n input.model,\n requestIdOf(response),\n )\n}\n\n/**\n * The endpoint's SSE stream as contract events: `choices[0].delta.content`\n * is text; `delta.tool_calls[i].function.arguments` arrive in pieces and\n * are parsed once at the end of the stream; the final chunk carries\n * `usage` when `stream_options.include_usage` was sent, and `[DONE]` ends\n * the body.\n */\nasync function* streamEvents(\n body: ReadableStream<Uint8Array>,\n model: string,\n requestId: string | null,\n): AsyncGenerator<AiStreamEvent> {\n const decoder = new TextDecoder()\n const reader = body.getReader()\n let usage: AiUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }\n let stopReason: string | null = null\n const tools = new Map<number, { name: string; json: string }>()\n let buffer = ''\n let drained = false\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) {\n drained = true\n break\n }\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue\n const data = line.slice('data: '.length).trim()\n if (data === '[DONE]') continue\n let event: Record<string, unknown>\n try {\n event = JSON.parse(data)\n } catch {\n continue\n }\n if (event['error']) {\n const error = event['error'] as { type?: string; message?: string; code?: unknown }\n console.error('ai upstream stream error', {\n provider: OPENAI_COMPATIBLE_PROVIDER_ID,\n requestId,\n model,\n error,\n })\n yield {\n type: 'error',\n retryable: openAiCompatibleFailureIsRetryable(\n typeof error.code === 'number' ? error.code : null,\n ),\n }\n continue\n }\n if (event['usage']) usage = openAiCompatibleUsageFrom(event['usage'])\n const choice = (event['choices'] as Array<Record<string, unknown>> | undefined)?.[0]\n if (!choice) continue\n const delta = choice['delta'] as\n | { content?: unknown; tool_calls?: Array<Record<string, unknown>> }\n | undefined\n if (typeof delta?.content === 'string' && delta.content) {\n yield { type: 'delta', text: delta.content }\n }\n for (const call of delta?.tool_calls ?? []) {\n const index = Number(call['index'] ?? 0)\n const fn = (call['function'] ?? {}) as { name?: unknown; arguments?: unknown }\n const inFlight = tools.get(index) ?? { name: '', json: '' }\n if (typeof fn.name === 'string' && fn.name) inFlight.name = fn.name\n if (typeof fn.arguments === 'string') inFlight.json += fn.arguments\n tools.set(index, inFlight)\n }\n const finish = stopReasonOf(choice['finish_reason'])\n if (finish) stopReason = finish\n }\n }\n } finally {\n if (!drained) await reader.cancel().catch(() => undefined)\n reader.releaseLock()\n }\n const called = [...tools.entries()].sort((a, b) => a[0] - b[0]).map(([, tool]) => tool)\n for (const tool of called) {\n yield { type: 'tool', name: tool.name, input: aiToolInputOf(tool.json) }\n }\n yield {\n type: 'done',\n usage,\n estCostUsd: estimateAiBilledUsd(usage, model),\n stopReason,\n // The `arguments` text the endpoint sent in fragments, for a stream its\n // ceiling cut off (AGL-3143): the `tool` events above carry only what\n // still parsed out of it, which for a cut call is a prefix or nothing.\n ...(aiStoppedAtCeiling(stopReason)\n ? { rawOutput: aiRawOutputOf(called.map((tool) => tool.json).join('')) }\n : {}),\n }\n}\n\nexport const openAiCompatibleProvider: AiProvider = {\n id: OPENAI_COMPATIBLE_PROVIDER_ID,\n label: 'OpenAI-compatible endpoint',\n apiKeyEnv: OPENAI_COMPAT_API_KEY_ENV,\n // Ready only with both halves: a key with no endpoint has nowhere to go.\n readApiKey: () =>\n openAiCompatibleBaseUrl() ? process.env.AI_OPENAI_COMPAT_API_KEY?.trim() || undefined : undefined,\n get endpointHost() {\n return openAiCompatibleHost()\n },\n toolSchemaLimits: OPENAI_COMPATIBLE_TOOL_SCHEMA_LIMITS,\n models(): readonly AiModelDescriptor[] {\n return aiModelIdsForProvider(OPENAI_COMPATIBLE_PROVIDER_ID)\n .map((id) => aiCatalogEntry(id))\n .filter((entry): entry is NonNullable<typeof entry> => Boolean(entry))\n },\n complete,\n stream,\n}\n"],"names":["aiCatalogEntry","aiModelIdsForProvider","estimateAiBilledUsd","AiUpstreamError","aiMessageText","aiRawOutputOf","aiStoppedAtCeiling","aiTokenCount","aiToolInputOf","OPENAI_COMPATIBLE_PROVIDER_ID","OPENAI_COMPAT_BASE_URL_ENV","OPENAI_COMPAT_API_KEY_ENV","openAiCompatibleBaseUrl","process","raw","env","trim","replace","openAiCompatibleHost","base","URL","host","OPENAI_COMPATIBLE_TOOL_SCHEMA_LIMITS","optionalParameters","openAiCompatibleFailureIsRetryable","status","openAiCompatibleUsageFrom","usage","record","details","prompt","cached","Math","min","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","buildOpenAiCompatibleRequestBody","input","system","map","block","text","join","tools","tool","type","function","name","description","parameters","inputSchema","additionalProperties","strict","model","max_tokens","maxTokens","stream","stream_options","include_usage","messages","role","content","message","openAiCompatibleContentOf","length","tool_choice","part","image_url","url","mediaType","data","requestIdOf","response","headers","get","send","Error","body","fetch","method","Authorization","apiKey","JSON","stringify","signal","ok","requestId","payload","json","catch","console","error","provider","stopReasonOf","finish","complete","choice","choices","toolUse","tool_calls","call","String","arguments","estCostUsd","stopReason","finish_reason","kind","rawOutput","streamEvents","decoder","TextDecoder","reader","getReader","Map","buffer","drained","lines","done","value","read","decode","split","pop","line","event","startsWith","slice","parse","retryable","code","delta","index","Number","fn","inFlight","set","cancel","undefined","releaseLock","called","entries","sort","a","b","openAiCompatibleProvider","id","label","apiKeyEnv","readApiKey","AI_OPENAI_COMPAT_API_KEY","endpointHost","toolSchemaLimits","models","filter","entry","Boolean"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/providers/openai-compatible.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 { aiCatalogEntry, aiModelIdsForProvider, estimateAiBilledUsd } from './catalog'\nimport {\n AiUpstreamError,\n type AiAccountProblem,\n aiMessageText,\n aiRawOutputOf,\n aiStoppedAtCeiling,\n aiTokenCount,\n aiToolInputOf,\n type AiMessage,\n type AiModelDescriptor,\n type AiProvider,\n type AiProviderRequest,\n type AiResult,\n type AiStreamEvent,\n type AiToolSchemaLimits,\n type AiToolUse,\n type AiUsage,\n} from './contract'\n\n/**\n * The OpenAI-compatible adapter (AGL-2939): `POST {base}/chat/completions`\n * against whatever endpoint `AI_OPENAI_COMPAT_BASE_URL` names — a vendor,\n * a gateway, a self-hosted model server — with the key in\n * `AI_OPENAI_COMPAT_API_KEY`. The second adapter behind the contract, and\n * the proof that the runtime, the gate, the jobs and the meter never\n * depended on the first vendor's wire shape.\n *\n * What the mapping gives up, stated rather than hidden: the endpoint has\n * no explicit cache breakpoints, so the ordered system blocks become one\n * system message in order (the runtime's prefix rule still holds, so the\n * order is cache-friendly for an endpoint that caches prefixes on its own),\n * and `cacheReadTokens` is whatever `prompt_tokens_details.cached_tokens`\n * reports. A `content_filter` finish is the endpoint's decline and becomes\n * an `AiRefusal`, the way a refusal stop does on the other adapter.\n */\n\nexport const OPENAI_COMPATIBLE_PROVIDER_ID = 'openai-compatible'\nexport const OPENAI_COMPAT_BASE_URL_ENV = 'AI_OPENAI_COMPAT_BASE_URL'\nexport const OPENAI_COMPAT_API_KEY_ENV = 'AI_OPENAI_COMPAT_API_KEY'\n\n/** The configured base, with any trailing slash and `/chat/completions` stripped. */\nexport function openAiCompatibleBaseUrl(): string | null {\n const raw = process.env[OPENAI_COMPAT_BASE_URL_ENV]?.trim()\n if (!raw) return null\n return raw.replace(/\\/+$/, '').replace(/\\/chat\\/completions$/, '')\n}\n\n/** The host the endpoint is reached at, for the subprocessor inventory. */\nexport function openAiCompatibleHost(): string {\n const base = openAiCompatibleBaseUrl()\n if (!base) return ''\n try {\n return new URL(base).host\n } catch {\n return ''\n }\n}\n\n/**\n * What the strict function shape this adapter sends compiles: every property\n * of every object listed in `required`, so no optional parameter at all — an\n * optional field is a union with `null` instead. No other bound is stated:\n * the endpoint is whatever `AI_OPENAI_COMPAT_BASE_URL` names, and no one\n * count of tools or unions — nor any grammar size — holds for every endpoint\n * that speaks the shape. A bound nothing here has watched an endpoint hold to\n * would be a guess, and a guess in a guard is worse than a gap in one.\n */\nexport const OPENAI_COMPATIBLE_TOOL_SCHEMA_LIMITS: AiToolSchemaLimits = {\n optionalParameters: 0,\n}\n\nexport function openAiCompatibleFailureIsRetryable(status: number | null): boolean {\n return status === 429 || status === 503 || status === 529\n}\n\n/** The endpoint's `usage` object, in the meter's shape. Missing fields read 0. */\nexport function openAiCompatibleUsageFrom(usage: unknown): AiUsage {\n const record = (usage ?? {}) as Record<string, unknown>\n const details = (record['prompt_tokens_details'] ?? {}) as Record<string, unknown>\n const prompt = aiTokenCount(record['prompt_tokens'])\n const cached = Math.min(aiTokenCount(details['cached_tokens']), prompt)\n return {\n // The endpoint counts cached tokens inside `prompt_tokens`; the meter\n // prices them apart, so they are split out here.\n inputTokens: prompt - cached,\n outputTokens: aiTokenCount(record['completion_tokens']),\n cacheReadTokens: cached,\n cacheWriteTokens: 0,\n }\n}\n\n/** The chat-completions body. Exported so a spec can assert the wire shape. */\nexport function buildOpenAiCompatibleRequestBody(\n input: Omit<AiProviderRequest, 'apiKey' | 'signal'> & { stream: boolean },\n): Record<string, unknown> {\n const system = input.system.map((block) => block.text).join('\\n\\n')\n const tools = input.tools?.map((tool) => ({\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description,\n parameters: { ...tool.inputSchema, additionalProperties: false },\n strict: true,\n },\n }))\n return {\n model: input.model,\n max_tokens: input.maxTokens,\n ...(input.stream ? { stream: true, stream_options: { include_usage: true } } : {}),\n messages: [\n ...(system ? [{ role: 'system', content: system }] : []),\n ...input.messages.map((message) => ({\n role: message.role,\n content: openAiCompatibleContentOf(message),\n })),\n ],\n ...(tools?.length ? { tools, tool_choice: 'auto' } : {}),\n }\n}\n\n/**\n * A turn's content in the chat-completions shape: a string for a turn with\n * nothing but text, and content parts for a user turn with pictures, each\n * picture an `image_url` carrying its bytes as a `data:` URL, so the endpoint\n * fetches nothing (AGL-2916). An assistant turn is always a string: the\n * runtime admits no picture there, and not every endpoint takes parts on it.\n */\nfunction openAiCompatibleContentOf(message: AiMessage): string | Array<Record<string, unknown>> {\n if (typeof message.content === 'string') return message.content\n if (message.role === 'assistant') return aiMessageText(message)\n return message.content.map((part) =>\n part.type === 'image'\n ? { type: 'image_url', image_url: { url: `data:${part.mediaType};base64,${part.data}` } }\n : { type: 'text', text: part.text },\n )\n}\n\nfunction requestIdOf(response: Response): string | null {\n return response.headers?.get?.('x-request-id') ?? null\n}\n\nasync function send(input: AiProviderRequest, stream: boolean): Promise<Response> {\n const base = openAiCompatibleBaseUrl()\n if (!base) {\n // Every door gates on the provider being configured before it gets\n // here, so this is a wiring fault rather than a customer path.\n throw new Error(`${OPENAI_COMPAT_BASE_URL_ENV} is not set`)\n }\n const body = buildOpenAiCompatibleRequestBody({ ...input, stream })\n const response = await fetch(`${base}/chat/completions`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${input.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n ...(input.signal ? { signal: input.signal } : {}),\n })\n if (!response.ok || (stream && !response.body)) {\n const requestId = requestIdOf(response)\n const payload = (await response.json().catch(() => null)) as {\n error?: { type?: string; message?: string; code?: string }\n } | null\n console.error('ai upstream error', {\n provider: OPENAI_COMPATIBLE_PROVIDER_ID,\n status: response.status,\n requestId,\n model: input.model,\n error: payload?.error ?? null,\n })\n throw new AiUpstreamError(\n response.status,\n openAiCompatibleFailureIsRetryable(response.status),\n requestId,\n openAiCompatibleAccountProblem(response.status, payload?.error),\n )\n }\n return response\n}\n\n/**\n * Whether a failure is about the platform's account rather than the request\n * (AGL-3377): a refused key, or a quota or balance that has run out. The\n * OpenAI-shaped `insufficient_quota` code is what compatible vendors send\n * for an empty balance, on a 429 that is otherwise a rate limit.\n */\nexport function openAiCompatibleAccountProblem(\n status: number | null,\n error: { type?: string; code?: string } | null | undefined,\n): AiAccountProblem | null {\n if (status === 401 || status === 403) return 'credentials'\n if (status === 402) return 'credit'\n if (error?.code === 'insufficient_quota' || error?.type === 'insufficient_quota') {\n return 'credit'\n }\n return null\n}\n\n/** `finish_reason` → the contract's stop reason; a filter is a refusal. */\nfunction stopReasonOf(finish: unknown): string | null {\n if (typeof finish !== 'string' || !finish) return null\n if (finish === 'content_filter') return 'refusal'\n if (finish === 'stop') return 'end_turn'\n if (finish === 'length') return 'max_tokens'\n if (finish === 'tool_calls') return 'tool_use'\n return finish\n}\n\nasync function complete(input: AiProviderRequest): Promise<AiResult> {\n const response = await send(input, false)\n const payload = (await response.json()) as {\n choices?: Array<{\n message?: {\n content?: unknown\n tool_calls?: Array<{ function?: { name?: unknown; arguments?: unknown } }>\n }\n finish_reason?: unknown\n }>\n usage?: unknown\n }\n const choice = payload?.choices?.[0]\n const text = typeof choice?.message?.content === 'string' ? choice.message.content : ''\n const toolUse: AiToolUse[] = (choice?.message?.tool_calls ?? []).map((call) => ({\n name: String(call?.function?.name ?? ''),\n input: aiToolInputOf(call?.function?.arguments),\n }))\n const usage = openAiCompatibleUsageFrom(payload?.usage)\n const estCostUsd = estimateAiBilledUsd(usage, input.model)\n const stopReason = stopReasonOf(choice?.finish_reason)\n if (stopReason === 'refusal') {\n return { kind: 'refusal', text, usage, estCostUsd, stopReason }\n }\n return {\n kind: 'completion',\n text,\n toolUse,\n usage,\n estCostUsd,\n stopReason,\n // The message as the endpoint answered it, for a call its ceiling cut off\n // (AGL-3143). A tool call's `arguments` is raw JSON text here, and this\n // is the only place the bytes it was cut in survive the parse.\n ...(aiStoppedAtCeiling(stopReason) ? { rawOutput: aiRawOutputOf(choice?.message) } : {}),\n }\n}\n\nasync function stream(input: AiProviderRequest): Promise<AsyncIterable<AiStreamEvent>> {\n const response = await send(input, true)\n return streamEvents(\n response.body as ReadableStream<Uint8Array>,\n input.model,\n requestIdOf(response),\n )\n}\n\n/**\n * The endpoint's SSE stream as contract events: `choices[0].delta.content`\n * is text; `delta.tool_calls[i].function.arguments` arrive in pieces and\n * are parsed once at the end of the stream; the final chunk carries\n * `usage` when `stream_options.include_usage` was sent, and `[DONE]` ends\n * the body.\n */\nasync function* streamEvents(\n body: ReadableStream<Uint8Array>,\n model: string,\n requestId: string | null,\n): AsyncGenerator<AiStreamEvent> {\n const decoder = new TextDecoder()\n const reader = body.getReader()\n let usage: AiUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }\n let stopReason: string | null = null\n const tools = new Map<number, { name: string; json: string }>()\n let buffer = ''\n let drained = false\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) {\n drained = true\n break\n }\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue\n const data = line.slice('data: '.length).trim()\n if (data === '[DONE]') continue\n let event: Record<string, unknown>\n try {\n event = JSON.parse(data)\n } catch {\n continue\n }\n if (event['error']) {\n const error = event['error'] as { type?: string; message?: string; code?: unknown }\n console.error('ai upstream stream error', {\n provider: OPENAI_COMPATIBLE_PROVIDER_ID,\n requestId,\n model,\n error,\n })\n yield {\n type: 'error',\n retryable: openAiCompatibleFailureIsRetryable(\n typeof error.code === 'number' ? error.code : null,\n ),\n }\n continue\n }\n if (event['usage']) usage = openAiCompatibleUsageFrom(event['usage'])\n const choice = (event['choices'] as Array<Record<string, unknown>> | undefined)?.[0]\n if (!choice) continue\n const delta = choice['delta'] as\n | { content?: unknown; tool_calls?: Array<Record<string, unknown>> }\n | undefined\n if (typeof delta?.content === 'string' && delta.content) {\n yield { type: 'delta', text: delta.content }\n }\n for (const call of delta?.tool_calls ?? []) {\n const index = Number(call['index'] ?? 0)\n const fn = (call['function'] ?? {}) as { name?: unknown; arguments?: unknown }\n const inFlight = tools.get(index) ?? { name: '', json: '' }\n if (typeof fn.name === 'string' && fn.name) inFlight.name = fn.name\n if (typeof fn.arguments === 'string') inFlight.json += fn.arguments\n tools.set(index, inFlight)\n }\n const finish = stopReasonOf(choice['finish_reason'])\n if (finish) stopReason = finish\n }\n }\n } finally {\n if (!drained) await reader.cancel().catch(() => undefined)\n reader.releaseLock()\n }\n const called = [...tools.entries()].sort((a, b) => a[0] - b[0]).map(([, tool]) => tool)\n for (const tool of called) {\n yield { type: 'tool', name: tool.name, input: aiToolInputOf(tool.json) }\n }\n yield {\n type: 'done',\n usage,\n estCostUsd: estimateAiBilledUsd(usage, model),\n stopReason,\n // The `arguments` text the endpoint sent in fragments, for a stream its\n // ceiling cut off (AGL-3143): the `tool` events above carry only what\n // still parsed out of it, which for a cut call is a prefix or nothing.\n ...(aiStoppedAtCeiling(stopReason)\n ? { rawOutput: aiRawOutputOf(called.map((tool) => tool.json).join('')) }\n : {}),\n }\n}\n\nexport const openAiCompatibleProvider: AiProvider = {\n id: OPENAI_COMPATIBLE_PROVIDER_ID,\n label: 'OpenAI-compatible endpoint',\n apiKeyEnv: OPENAI_COMPAT_API_KEY_ENV,\n // Ready only with both halves: a key with no endpoint has nowhere to go.\n readApiKey: () =>\n openAiCompatibleBaseUrl() ? process.env.AI_OPENAI_COMPAT_API_KEY?.trim() || undefined : undefined,\n get endpointHost() {\n return openAiCompatibleHost()\n },\n toolSchemaLimits: OPENAI_COMPATIBLE_TOOL_SCHEMA_LIMITS,\n models(): readonly AiModelDescriptor[] {\n return aiModelIdsForProvider(OPENAI_COMPATIBLE_PROVIDER_ID)\n .map((id) => aiCatalogEntry(id))\n .filter((entry): entry is NonNullable<typeof entry> => Boolean(entry))\n },\n complete,\n stream,\n}\n"],"names":["aiCatalogEntry","aiModelIdsForProvider","estimateAiBilledUsd","AiUpstreamError","aiMessageText","aiRawOutputOf","aiStoppedAtCeiling","aiTokenCount","aiToolInputOf","OPENAI_COMPATIBLE_PROVIDER_ID","OPENAI_COMPAT_BASE_URL_ENV","OPENAI_COMPAT_API_KEY_ENV","openAiCompatibleBaseUrl","process","raw","env","trim","replace","openAiCompatibleHost","base","URL","host","OPENAI_COMPATIBLE_TOOL_SCHEMA_LIMITS","optionalParameters","openAiCompatibleFailureIsRetryable","status","openAiCompatibleUsageFrom","usage","record","details","prompt","cached","Math","min","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","buildOpenAiCompatibleRequestBody","input","system","map","block","text","join","tools","tool","type","function","name","description","parameters","inputSchema","additionalProperties","strict","model","max_tokens","maxTokens","stream","stream_options","include_usage","messages","role","content","message","openAiCompatibleContentOf","length","tool_choice","part","image_url","url","mediaType","data","requestIdOf","response","headers","get","send","Error","body","fetch","method","Authorization","apiKey","JSON","stringify","signal","ok","requestId","payload","json","catch","console","error","provider","openAiCompatibleAccountProblem","code","stopReasonOf","finish","complete","choice","choices","toolUse","tool_calls","call","String","arguments","estCostUsd","stopReason","finish_reason","kind","rawOutput","streamEvents","decoder","TextDecoder","reader","getReader","Map","buffer","drained","lines","done","value","read","decode","split","pop","line","event","startsWith","slice","parse","retryable","delta","index","Number","fn","inFlight","set","cancel","undefined","releaseLock","called","entries","sort","a","b","openAiCompatibleProvider","id","label","apiKeyEnv","readApiKey","AI_OPENAI_COMPAT_API_KEY","endpointHost","toolSchemaLimits","models","filter","entry","Boolean"],"mappings":"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"}
|
|
@@ -16,7 +16,7 @@ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_wi
|
|
|
16
16
|
* See the License for the specific language governing permissions and
|
|
17
17
|
* limitations under the License.
|
|
18
18
|
*/ import { aiModelCacheMinTokens } from "../providers/catalog.js";
|
|
19
|
-
import { AI_IMAGE_MAX_BYTES, AI_IMAGE_MEDIA_TYPES, AI_REQUEST_MAX_IMAGES, AiRequestShapeError, aiBase64Bytes, aiMessageImages } from "../providers/contract.js";
|
|
19
|
+
import { AI_IMAGE_MAX_BYTES, AI_IMAGE_MEDIA_TYPES, AI_REQUEST_MAX_IMAGES, AiRequestShapeError, AiUpstreamError, aiBase64Bytes, aiMessageImages } from "../providers/contract.js";
|
|
20
20
|
import { resolveAiProvider } from "../providers/routing.js";
|
|
21
21
|
export { AI_UPSTREAM_FAILURE_COPY, AiRequestShapeError, AiUpstreamError } from "../providers/contract.js";
|
|
22
22
|
/**
|
|
@@ -231,7 +231,37 @@ export async function runAiRequest(input) {
|
|
|
231
231
|
const providerRequest = _extends({}, request, {
|
|
232
232
|
apiKey
|
|
233
233
|
});
|
|
234
|
-
|
|
234
|
+
try {
|
|
235
|
+
return await (stream ? provider.stream(providerRequest) : provider.complete(providerRequest));
|
|
236
|
+
} catch (error) {
|
|
237
|
+
await reportAccountProblem(provider, error);
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Tells the operator when the failure is the platform's provider account —
|
|
243
|
+
* a refused key, no credit — rather than the request (AGL-3377). Once per
|
|
244
|
+
* provider and problem per window; loaded lazily so the runtime pulls no
|
|
245
|
+
* Admin SDK until an account is actually broken. Never throws.
|
|
246
|
+
*/ async function reportAccountProblem(provider, error) {
|
|
247
|
+
if (!(error instanceof AiUpstreamError) || !error.accountProblem) return;
|
|
248
|
+
try {
|
|
249
|
+
var _error_status;
|
|
250
|
+
const [{ raiseOperatorAlert }, { AI_PROVIDER_UNAVAILABLE }] = await Promise.all([
|
|
251
|
+
import("./operator-alerts-seam.js"),
|
|
252
|
+
import("../operator-alerts.js")
|
|
253
|
+
]);
|
|
254
|
+
await raiseOperatorAlert(AI_PROVIDER_UNAVAILABLE, {
|
|
255
|
+
dedupeKey: `${provider.id}:${error.accountProblem}`,
|
|
256
|
+
context: {
|
|
257
|
+
provider: provider.label,
|
|
258
|
+
keyEnv: provider.apiKeyEnv,
|
|
259
|
+
reason: error.accountProblem === 'credentials' ? `its API key was refused (HTTP ${(_error_status = error.status) != null ? _error_status : 'unknown'})` : 'the account is out of credit'
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
} catch (alertError) {
|
|
263
|
+
console.error('[ai] the provider account alert could not be raised', alertError);
|
|
264
|
+
}
|
|
235
265
|
}
|
|
236
266
|
|
|
237
267
|
//# sourceMappingURL=ai-runtime.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-runtime.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 { aiModelCacheMinTokens } from '../providers/catalog'\nimport {\n AI_IMAGE_MAX_BYTES,\n AI_IMAGE_MEDIA_TYPES,\n AI_REQUEST_MAX_IMAGES,\n AiRequestShapeError,\n aiBase64Bytes,\n aiMessageImages,\n type AiMessage,\n type AiProvider,\n type AiRequestBase,\n type AiResult,\n type AiStreamEvent,\n type AiSystemBlock,\n type AiTool,\n} from '../providers/contract'\nimport { resolveAiProvider, type AiPluginSettings } from '../providers/routing'\n\nexport {\n AI_UPSTREAM_FAILURE_COPY,\n AiRequestShapeError,\n AiUpstreamError,\n type AiCompletion,\n type AiEffort,\n type AiImagePart,\n type AiMessage,\n type AiMessagePart,\n type AiRefusal,\n type AiRequestBase,\n type AiResult,\n type AiStreamEvent,\n type AiSystemBlock,\n type AiThinking,\n type AiTool,\n type AiToolUse,\n type AiUsage,\n} from '../providers/contract'\n\n/**\n * The one runtime every AI door calls (AGL-2903), provider-generic since\n * AGL-2939.\n *\n * `/api/assist/chat` and `/api/ai/assist` each carried their own `fetch` to\n * one vendor, their own SSE parser and their own usage extraction, and a\n * third door would have been a third copy. This module owns the request\n * shape, the cache-prefix rule, the provider resolution and the error\n * boundary; an adapter owns one vendor's wire protocol; a door owns its\n * prompt, its gate ladder and its wire format.\n *\n * The cache-prefix rule (AGL-2352) is enforced HERE, before any adapter:\n * a block marked `volatile` may not sit at or before the last\n * `cacheBreakpoint`, and more breakpoints than any provider caches are\n * refused. A provider without a cache ignores the markers and keeps the\n * order, so a request that is right for the caching adapter is right for\n * every adapter.\n *\n * The picture rule (AGL-2916) is enforced here too, and for the same reason:\n * a picture on an assistant turn, a picture for a model whose provider does\n * not say it reads one, or a picture past the bounds is refused before any\n * adapter is reached, so no door can send one a provider would reject.\n */\n\n/** The most `cacheBreakpoint` markers a request may carry. */\nexport const AI_MAX_CACHE_BREAKPOINTS = 4\n\n/**\n * The acceptable-use rules every generation prompt carries (AGL-2925).\n *\n * ONE constant, appended to each door's STATIC system text — inside the\n * cached prefix on the console chat, ahead of the mode prompt on the copy\n * assistant — so the rules the model is held to are the same at every\n * door and carry no per-org byte. The Free taste is what makes them worth\n * stating: a generated page that nobody paid for is the cheapest phishing\n * kit there is, and the model declining it is the first line, ahead of the\n * abuse-report route and the lockdown key.\n *\n * The categories are the platform's own Acceptable Use Policy, restated in\n * the second person for the model. A decline from the model's safety layer\n * arrives as a refusal, which the meter counts against the account's daily\n * pause and never against its credits.\n */\nexport const AI_ACCEPTABLE_USE_BLOCK = `Acceptable use — decline these rather than write them, and say briefly why:\n- Pages, emails or copy that harvest credentials, payment details or personal data by deception (phishing, fake sign-in or checkout pages, fake \"verify your account\" notices).\n- Impersonating a real business, brand, public body or person you were not told the user represents — their name, logo, sign-in page or voice — or copy that claims an endorsement, certification or partnership that was not given to you as a fact.\n- Content that promotes or sells prohibited goods and services: weapons, illegal drugs, counterfeit goods, malware or deceptive downloads, unlicensed gambling, or adult content involving minors.\n- Scams and fraud: fake investment or crypto schemes, advance-fee offers, tech-support scams, fake charities, deceptive \"you have won\" pages, or copy built to defraud.\n- Harassment, threats, or hateful content targeting a person or a group.\n- Spam farms: bulk near-duplicate pages or copy whose only purpose is to game search engines or send unsolicited mail.\nWhen a brief is ordinary business content, write it well and do not lecture.`\n\n/** A door's request, with the org's settings when the door knows the org. */\ninterface AiRuntimeOptions {\n /** The org's resolved `pluginSettings/ai`, for its provider and model choices. */\n settings?: AiPluginSettings\n /** A provider chosen by the caller, ahead of every setting; specs use it. */\n provider?: AiProvider\n}\n\nexport interface AiRequest extends AiRequestBase, AiRuntimeOptions {\n stream: false\n}\n\nexport interface AiStreamRequest extends AiRequestBase, AiRuntimeOptions {\n stream: true\n}\n\n/**\n * The cache guard, applied to the system blocks before any adapter sees\n * them. Exported so a spec and a door can assert a prompt's shape without\n * a provider.\n */\nexport function validateAiSystemBlocks(system: readonly AiSystemBlock[]): void {\n const breakpoints = system.filter((block) => block.cacheBreakpoint)\n if (breakpoints.length > AI_MAX_CACHE_BREAKPOINTS) {\n throw new AiRequestShapeError(\n `${breakpoints.length} cache breakpoints; at most ${AI_MAX_CACHE_BREAKPOINTS} are cached`,\n )\n }\n // Caching is a prefix match, so the LAST breakpoint decides what is\n // cached, and every block at or before it is inside the cached prefix. A\n // volatile block anywhere in that span — including one that is itself\n // the breakpoint — puts a per-request byte into the prefix (AGL-2352).\n const lastBreakpoint = system.reduce(\n (last, block, index) => (block.cacheBreakpoint ? index : last),\n -1,\n )\n system.forEach((block, index) => {\n if (block.volatile && index <= lastBreakpoint) {\n throw new AiRequestShapeError(\n `system block ${index} is volatile but sits inside the cached prefix (last breakpoint at block ${lastBreakpoint})`,\n )\n }\n })\n}\n\n/**\n * Whether a provider's model reads a picture in a user turn (AGL-2916): what\n * the provider's own descriptor of the model says. A model the provider does\n * not describe reads none, so an adopter's provider is never sent a picture\n * it did not say it can take.\n */\nexport function aiModelReadsImages(provider: AiProvider, model: string): boolean {\n return provider\n .models()\n .some((descriptor) => descriptor.id === model && descriptor.capabilities.vision === true)\n}\n\n/**\n * Whether a model a door resolved reads pictures on the provider that would\n * serve the request: what a door asks before it attaches one, so a model that\n * does not is sent the text alone rather than refused.\n */\nexport function aiRouteReadsImages(model: string, settings?: AiPluginSettings): boolean {\n const provider = resolveAiProvider(settings)\n return provider ? aiModelReadsImages(provider, model) : false\n}\n\n/** A character standard base64 does not use, outside its padding. */\nconst NOT_BASE64 = /[^A-Za-z0-9+/]/\n\n/**\n * Whether `data` is standard base64 and nothing else: its alphabet, then at\n * most two `=` of padding; no `data:` prefix, no line breaks.\n *\n * Scanned for a character outside the alphabet rather than matched whole: a\n * picture is megabytes of base64, and a whole-string pattern with a greedy\n * loop can exhaust the regular expression engine's backtrack stack on input\n * that large, which throws a RangeError instead of answering.\n */\nfunction isBareBase64(data: string): boolean {\n const body = data.endsWith('==') ? data.slice(0, -2) : data.endsWith('=') ? data.slice(0, -1) : data\n return body.length > 0 && !NOT_BASE64.test(body)\n}\n\n/**\n * The picture guard (AGL-2916), applied to the messages before any adapter\n * sees them. A picture rides only a user turn, only to a model that reads\n * pictures, only in a format every such model takes, as bare base64 within\n * the size a provider accepts, and no more of them than a request may carry.\n * `readsImages` is asked only when a turn carries a picture. Exported so a\n * spec and a door can assert a request's shape without a provider.\n */\nexport function validateAiMessages(messages: readonly AiMessage[], readsImages: () => boolean): void {\n messages.forEach((message, index) => {\n if (typeof message.content === 'string') return\n if (!Array.isArray(message.content) || message.content.length === 0) {\n throw new AiRequestShapeError(`message ${index} has neither text nor parts`)\n }\n for (const part of message.content) {\n if (part.type === 'text') {\n if (typeof part.text !== 'string') {\n throw new AiRequestShapeError(`message ${index} has a text part with no text`)\n }\n continue\n }\n if (part.type !== 'image') {\n throw new AiRequestShapeError(`message ${index} has a part of an unknown type`)\n }\n if (message.role !== 'user') {\n throw new AiRequestShapeError(`message ${index} is an assistant turn and carries a picture`)\n }\n if (!(AI_IMAGE_MEDIA_TYPES as readonly string[]).includes(part.mediaType)) {\n throw new AiRequestShapeError(`message ${index} carries a picture of an unsupported type`)\n }\n if (typeof part.data !== 'string') {\n throw new AiRequestShapeError(`message ${index} carries a picture that is not bare base64`)\n }\n // The size first: it needs only the length, so an oversized picture is\n // refused without reading its bytes.\n if (aiBase64Bytes(part.data) > AI_IMAGE_MAX_BYTES) {\n throw new AiRequestShapeError(\n `message ${index} carries a picture over ${AI_IMAGE_MAX_BYTES} bytes`,\n )\n }\n if (!isBareBase64(part.data)) {\n throw new AiRequestShapeError(`message ${index} carries a picture that is not bare base64`)\n }\n }\n })\n const images = aiMessageImages(messages).length\n if (images > AI_REQUEST_MAX_IMAGES) {\n throw new AiRequestShapeError(`${images} pictures; a request carries at most ${AI_REQUEST_MAX_IMAGES}`)\n }\n if (images > 0 && !readsImages()) {\n throw new AiRequestShapeError('the request carries a picture and its model does not read pictures')\n }\n}\n\n/**\n * Characters to a token when a prompt is measured rather than tokenized\n * (AGL-2937). Four is the high end of English prose, so reading a prefix\n * through it errs LOW on the token count — and a prefix counts as cached\n * only when it clears its model's minimum even at that reading. Erring the\n * other way would let a prompt claim a cache it does not get.\n */\nexport const AI_CACHE_PREFIX_TOKEN_CHARS = 4\n\n/**\n * The characters inside the cached span of a request: the tools, which a\n * provider renders ahead of the system blocks and which therefore sit in\n * every prefix, plus every system block up to and including the last\n * breakpoint. Zero when the request marks no breakpoint at all.\n */\nexport function aiCachedPrefixChars(\n system: readonly AiSystemBlock[],\n tools: readonly AiTool[] = [],\n): number {\n const lastBreakpoint = system.reduce(\n (last, block, index) => (block.cacheBreakpoint ? index : last),\n -1,\n )\n if (lastBreakpoint < 0) return 0\n const toolChars = tools.reduce((total, tool) => total + JSON.stringify(tool).length, 0)\n return system\n .slice(0, lastBreakpoint + 1)\n .reduce((total, block) => total + block.text.length, toolChars)\n}\n\n/**\n * Whether this request's cached span is long enough for the model to cache\n * it at all (AGL-2937). A prefix under the model's `cacheMinTokens` is\n * served with the breakpoints honored and nothing cached — no error, and a\n * usage report that reads as a permanent miss — so a door designed around a\n * cache has to answer this question before it believes its own markers, and\n * `runtime/ai-prompt-cache.spec.ts` answers it for every door at once.\n */\nexport function aiCachedPrefixCaches(\n model: string,\n system: readonly AiSystemBlock[],\n tools: readonly AiTool[] = [],\n): boolean {\n const chars = aiCachedPrefixChars(system, tools)\n return chars / AI_CACHE_PREFIX_TOKEN_CHARS >= aiModelCacheMinTokens(model)\n}\n\n/**\n * Whether a provider is configured to answer: registered, and its key\n * present. Every door asks this before it spends a reservation, and\n * answers 501 (or the docs fallback) when it is false.\n */\nexport function aiProviderReady(settings?: AiPluginSettings): boolean {\n return Boolean(resolveAiProvider(settings)?.readApiKey())\n}\n\n/** The provider that would answer, for a door that names it in its logs or its inventory. */\nexport function aiActiveProvider(settings?: AiPluginSettings): AiProvider | undefined {\n return resolveAiProvider(settings)\n}\n\nfunction providerFor(input: AiRuntimeOptions): { provider: AiProvider; apiKey: string } {\n const provider = input.provider ?? resolveAiProvider(input.settings)\n if (!provider) throw new Error('no AI provider is registered')\n const apiKey = provider.readApiKey()\n if (!apiKey) {\n // Every door gates on `aiProviderReady` before it gets here, so this is\n // a wiring fault rather than a customer path.\n throw new Error(`${provider.apiKeyEnv} is not set`)\n }\n return { provider, apiKey }\n}\n\n/**\n * Send one request through the resolved provider.\n *\n * Streaming: resolves once the provider has ACCEPTED the request — after\n * the headers, before the first token — with an iterator of simplified\n * events the caller wraps in its own wire format. A non-2xx answer rejects\n * with `AiUpstreamError` before the iterator exists, so a door can still\n * send a plain error status and hand back its reservation.\n *\n * Non-streaming: resolves with the parsed result, a decline typed as\n * `AiRefusal` rather than thrown.\n */\nexport async function runAiRequest(\n input: AiStreamRequest,\n): Promise<AsyncIterable<AiStreamEvent>>\nexport async function runAiRequest(input: AiRequest): Promise<AiResult>\nexport async function runAiRequest(\n input: AiRequest | AiStreamRequest,\n): Promise<AsyncIterable<AiStreamEvent> | AiResult> {\n validateAiSystemBlocks(input.system)\n const { provider, apiKey } = providerFor(input)\n validateAiMessages(input.messages, () => aiModelReadsImages(provider, input.model))\n const { stream, settings: _settings, provider: _provider, ...request } = input\n const providerRequest = { ...request, apiKey }\n return stream ? provider.stream(providerRequest) : provider.complete(providerRequest)\n}\n"],"names":["aiModelCacheMinTokens","AI_IMAGE_MAX_BYTES","AI_IMAGE_MEDIA_TYPES","AI_REQUEST_MAX_IMAGES","AiRequestShapeError","aiBase64Bytes","aiMessageImages","resolveAiProvider","AI_UPSTREAM_FAILURE_COPY","AiUpstreamError","AI_MAX_CACHE_BREAKPOINTS","AI_ACCEPTABLE_USE_BLOCK","validateAiSystemBlocks","system","breakpoints","filter","block","cacheBreakpoint","length","lastBreakpoint","reduce","last","index","forEach","volatile","aiModelReadsImages","provider","model","models","some","descriptor","id","capabilities","vision","aiRouteReadsImages","settings","NOT_BASE64","isBareBase64","data","body","endsWith","slice","test","validateAiMessages","messages","readsImages","message","content","Array","isArray","part","type","text","role","includes","mediaType","images","AI_CACHE_PREFIX_TOKEN_CHARS","aiCachedPrefixChars","tools","toolChars","total","tool","JSON","stringify","aiCachedPrefixCaches","chars","aiProviderReady","Boolean","readApiKey","aiActiveProvider","providerFor","input","Error","apiKey","apiKeyEnv","runAiRequest","stream","_settings","_provider","request","providerRequest","complete"],"mappings":";;AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,qBAAqB,QAAQ,0BAAsB;AAC5D,SACEC,kBAAkB,EAClBC,oBAAoB,EACpBC,qBAAqB,EACrBC,mBAAmB,EACnBC,aAAa,EACbC,eAAe,QAQV,2BAAuB;AAC9B,SAASC,iBAAiB,QAA+B,0BAAsB;AAE/E,SACEC,wBAAwB,EACxBJ,mBAAmB,EACnBK,eAAe,QAeV,2BAAuB;AAE9B;;;;;;;;;;;;;;;;;;;;;;CAsBC,GAED,4DAA4D,GAC5D,OAAO,MAAMC,2BAA2B,EAAC;AAEzC;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAMC,0BAA0B,CAAC;;;;;;;4EAOoC,CAAC,CAAA;AAkB7E;;;;CAIC,GACD,OAAO,SAASC,uBAAuBC,MAAgC;IACrE,MAAMC,cAAcD,OAAOE,MAAM,CAAC,CAACC,QAAUA,MAAMC,eAAe;IAClE,IAAIH,YAAYI,MAAM,GAAGR,0BAA0B;QACjD,MAAM,IAAIN,oBACR,GAAGU,YAAYI,MAAM,CAAC,4BAA4B,EAAER,yBAAyB,WAAW,CAAC;IAE7F;IACA,oEAAoE;IACpE,yEAAyE;IACzE,sEAAsE;IACtE,uEAAuE;IACvE,MAAMS,iBAAiBN,OAAOO,MAAM,CAClC,CAACC,MAAML,OAAOM,QAAWN,MAAMC,eAAe,GAAGK,QAAQD,MACzD,CAAC;IAEHR,OAAOU,OAAO,CAAC,CAACP,OAAOM;QACrB,IAAIN,MAAMQ,QAAQ,IAAIF,SAASH,gBAAgB;YAC7C,MAAM,IAAIf,oBACR,CAAC,aAAa,EAAEkB,MAAM,yEAAyE,EAAEH,eAAe,CAAC,CAAC;QAEtH;IACF;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASM,mBAAmBC,QAAoB,EAAEC,KAAa;IACpE,OAAOD,SACJE,MAAM,GACNC,IAAI,CAAC,CAACC,aAAeA,WAAWC,EAAE,KAAKJ,SAASG,WAAWE,YAAY,CAACC,MAAM,KAAK;AACxF;AAEA;;;;CAIC,GACD,OAAO,SAASC,mBAAmBP,KAAa,EAAEQ,QAA2B;IAC3E,MAAMT,WAAWnB,kBAAkB4B;IACnC,OAAOT,WAAWD,mBAAmBC,UAAUC,SAAS;AAC1D;AAEA,mEAAmE,GACnE,MAAMS,aAAa;AAEnB;;;;;;;;CAQC,GACD,SAASC,aAAaC,IAAY;IAChC,MAAMC,OAAOD,KAAKE,QAAQ,CAAC,QAAQF,KAAKG,KAAK,CAAC,GAAG,CAAC,KAAKH,KAAKE,QAAQ,CAAC,OAAOF,KAAKG,KAAK,CAAC,GAAG,CAAC,KAAKH;IAChG,OAAOC,KAAKrB,MAAM,GAAG,KAAK,CAACkB,WAAWM,IAAI,CAACH;AAC7C;AAEA;;;;;;;CAOC,GACD,OAAO,SAASI,mBAAmBC,QAA8B,EAAEC,WAA0B;IAC3FD,SAASrB,OAAO,CAAC,CAACuB,SAASxB;QACzB,IAAI,OAAOwB,QAAQC,OAAO,KAAK,UAAU;QACzC,IAAI,CAACC,MAAMC,OAAO,CAACH,QAAQC,OAAO,KAAKD,QAAQC,OAAO,CAAC7B,MAAM,KAAK,GAAG;YACnE,MAAM,IAAId,oBAAoB,CAAC,QAAQ,EAAEkB,MAAM,2BAA2B,CAAC;QAC7E;QACA,KAAK,MAAM4B,QAAQJ,QAAQC,OAAO,CAAE;YAClC,IAAIG,KAAKC,IAAI,KAAK,QAAQ;gBACxB,IAAI,OAAOD,KAAKE,IAAI,KAAK,UAAU;oBACjC,MAAM,IAAIhD,oBAAoB,CAAC,QAAQ,EAAEkB,MAAM,6BAA6B,CAAC;gBAC/E;gBACA;YACF;YACA,IAAI4B,KAAKC,IAAI,KAAK,SAAS;gBACzB,MAAM,IAAI/C,oBAAoB,CAAC,QAAQ,EAAEkB,MAAM,8BAA8B,CAAC;YAChF;YACA,IAAIwB,QAAQO,IAAI,KAAK,QAAQ;gBAC3B,MAAM,IAAIjD,oBAAoB,CAAC,QAAQ,EAAEkB,MAAM,2CAA2C,CAAC;YAC7F;YACA,IAAI,CAAC,AAACpB,qBAA2CoD,QAAQ,CAACJ,KAAKK,SAAS,GAAG;gBACzE,MAAM,IAAInD,oBAAoB,CAAC,QAAQ,EAAEkB,MAAM,yCAAyC,CAAC;YAC3F;YACA,IAAI,OAAO4B,KAAKZ,IAAI,KAAK,UAAU;gBACjC,MAAM,IAAIlC,oBAAoB,CAAC,QAAQ,EAAEkB,MAAM,0CAA0C,CAAC;YAC5F;YACA,uEAAuE;YACvE,qCAAqC;YACrC,IAAIjB,cAAc6C,KAAKZ,IAAI,IAAIrC,oBAAoB;gBACjD,MAAM,IAAIG,oBACR,CAAC,QAAQ,EAAEkB,MAAM,wBAAwB,EAAErB,mBAAmB,MAAM,CAAC;YAEzE;YACA,IAAI,CAACoC,aAAaa,KAAKZ,IAAI,GAAG;gBAC5B,MAAM,IAAIlC,oBAAoB,CAAC,QAAQ,EAAEkB,MAAM,0CAA0C,CAAC;YAC5F;QACF;IACF;IACA,MAAMkC,SAASlD,gBAAgBsC,UAAU1B,MAAM;IAC/C,IAAIsC,SAASrD,uBAAuB;QAClC,MAAM,IAAIC,oBAAoB,GAAGoD,OAAO,qCAAqC,EAAErD,uBAAuB;IACxG;IACA,IAAIqD,SAAS,KAAK,CAACX,eAAe;QAChC,MAAM,IAAIzC,oBAAoB;IAChC;AACF;AAEA;;;;;;CAMC,GACD,OAAO,MAAMqD,8BAA8B,EAAC;AAE5C;;;;;CAKC,GACD,OAAO,SAASC,oBACd7C,MAAgC,EAChC8C,QAA2B,EAAE;IAE7B,MAAMxC,iBAAiBN,OAAOO,MAAM,CAClC,CAACC,MAAML,OAAOM,QAAWN,MAAMC,eAAe,GAAGK,QAAQD,MACzD,CAAC;IAEH,IAAIF,iBAAiB,GAAG,OAAO;IAC/B,MAAMyC,YAAYD,MAAMvC,MAAM,CAAC,CAACyC,OAAOC,OAASD,QAAQE,KAAKC,SAAS,CAACF,MAAM5C,MAAM,EAAE;IACrF,OAAOL,OACJ4B,KAAK,CAAC,GAAGtB,iBAAiB,GAC1BC,MAAM,CAAC,CAACyC,OAAO7C,QAAU6C,QAAQ7C,MAAMoC,IAAI,CAAClC,MAAM,EAAE0C;AACzD;AAEA;;;;;;;CAOC,GACD,OAAO,SAASK,qBACdtC,KAAa,EACbd,MAAgC,EAChC8C,QAA2B,EAAE;IAE7B,MAAMO,QAAQR,oBAAoB7C,QAAQ8C;IAC1C,OAAOO,QAAQT,+BAA+BzD,sBAAsB2B;AACtE;AAEA;;;;CAIC,GACD,OAAO,SAASwC,gBAAgBhC,QAA2B;QAC1C5B;IAAf,OAAO6D,SAAQ7D,qBAAAA,kBAAkB4B,8BAAlB5B,mBAA6B8D,UAAU;AACxD;AAEA,2FAA2F,GAC3F,OAAO,SAASC,iBAAiBnC,QAA2B;IAC1D,OAAO5B,kBAAkB4B;AAC3B;AAEA,SAASoC,YAAYC,KAAuB;QACzBA;IAAjB,MAAM9C,YAAW8C,kBAAAA,MAAM9C,QAAQ,YAAd8C,kBAAkBjE,kBAAkBiE,MAAMrC,QAAQ;IACnE,IAAI,CAACT,UAAU,MAAM,IAAI+C,MAAM;IAC/B,MAAMC,SAAShD,SAAS2C,UAAU;IAClC,IAAI,CAACK,QAAQ;QACX,wEAAwE;QACxE,8CAA8C;QAC9C,MAAM,IAAID,MAAM,GAAG/C,SAASiD,SAAS,CAAC,WAAW,CAAC;IACpD;IACA,OAAO;QAAEjD;QAAUgD;IAAO;AAC5B;AAkBA,OAAO,eAAeE,aACpBJ,KAAkC;IAElC5D,uBAAuB4D,MAAM3D,MAAM;IACnC,MAAM,EAAEa,QAAQ,EAAEgD,MAAM,EAAE,GAAGH,YAAYC;IACzC7B,mBAAmB6B,MAAM5B,QAAQ,EAAE,IAAMnB,mBAAmBC,UAAU8C,MAAM7C,KAAK;IACjF,MAAM,EAAEkD,MAAM,EAAE1C,UAAU2C,SAAS,EAAEpD,UAAUqD,SAAS,EAAc,GAAGP,OAAZQ,2CAAYR;;;;;IACzE,MAAMS,kBAAkB,aAAKD;QAASN;;IACtC,OAAOG,SAASnD,SAASmD,MAAM,CAACI,mBAAmBvD,SAASwD,QAAQ,CAACD;AACvE"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-runtime.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 { aiModelCacheMinTokens } from '../providers/catalog'\nimport {\n AI_IMAGE_MAX_BYTES,\n AI_IMAGE_MEDIA_TYPES,\n AI_REQUEST_MAX_IMAGES,\n AiRequestShapeError,\n AiUpstreamError,\n aiBase64Bytes,\n aiMessageImages,\n type AiMessage,\n type AiProvider,\n type AiRequestBase,\n type AiResult,\n type AiStreamEvent,\n type AiSystemBlock,\n type AiTool,\n} from '../providers/contract'\nimport { resolveAiProvider, type AiPluginSettings } from '../providers/routing'\n\nexport {\n AI_UPSTREAM_FAILURE_COPY,\n AiRequestShapeError,\n AiUpstreamError,\n type AiCompletion,\n type AiEffort,\n type AiImagePart,\n type AiMessage,\n type AiMessagePart,\n type AiRefusal,\n type AiRequestBase,\n type AiResult,\n type AiStreamEvent,\n type AiSystemBlock,\n type AiThinking,\n type AiTool,\n type AiToolUse,\n type AiUsage,\n} from '../providers/contract'\n\n/**\n * The one runtime every AI door calls (AGL-2903), provider-generic since\n * AGL-2939.\n *\n * `/api/assist/chat` and `/api/ai/assist` each carried their own `fetch` to\n * one vendor, their own SSE parser and their own usage extraction, and a\n * third door would have been a third copy. This module owns the request\n * shape, the cache-prefix rule, the provider resolution and the error\n * boundary; an adapter owns one vendor's wire protocol; a door owns its\n * prompt, its gate ladder and its wire format.\n *\n * The cache-prefix rule (AGL-2352) is enforced HERE, before any adapter:\n * a block marked `volatile` may not sit at or before the last\n * `cacheBreakpoint`, and more breakpoints than any provider caches are\n * refused. A provider without a cache ignores the markers and keeps the\n * order, so a request that is right for the caching adapter is right for\n * every adapter.\n *\n * The picture rule (AGL-2916) is enforced here too, and for the same reason:\n * a picture on an assistant turn, a picture for a model whose provider does\n * not say it reads one, or a picture past the bounds is refused before any\n * adapter is reached, so no door can send one a provider would reject.\n */\n\n/** The most `cacheBreakpoint` markers a request may carry. */\nexport const AI_MAX_CACHE_BREAKPOINTS = 4\n\n/**\n * The acceptable-use rules every generation prompt carries (AGL-2925).\n *\n * ONE constant, appended to each door's STATIC system text — inside the\n * cached prefix on the console chat, ahead of the mode prompt on the copy\n * assistant — so the rules the model is held to are the same at every\n * door and carry no per-org byte. The Free taste is what makes them worth\n * stating: a generated page that nobody paid for is the cheapest phishing\n * kit there is, and the model declining it is the first line, ahead of the\n * abuse-report route and the lockdown key.\n *\n * The categories are the platform's own Acceptable Use Policy, restated in\n * the second person for the model. A decline from the model's safety layer\n * arrives as a refusal, which the meter counts against the account's daily\n * pause and never against its credits.\n */\nexport const AI_ACCEPTABLE_USE_BLOCK = `Acceptable use — decline these rather than write them, and say briefly why:\n- Pages, emails or copy that harvest credentials, payment details or personal data by deception (phishing, fake sign-in or checkout pages, fake \"verify your account\" notices).\n- Impersonating a real business, brand, public body or person you were not told the user represents — their name, logo, sign-in page or voice — or copy that claims an endorsement, certification or partnership that was not given to you as a fact.\n- Content that promotes or sells prohibited goods and services: weapons, illegal drugs, counterfeit goods, malware or deceptive downloads, unlicensed gambling, or adult content involving minors.\n- Scams and fraud: fake investment or crypto schemes, advance-fee offers, tech-support scams, fake charities, deceptive \"you have won\" pages, or copy built to defraud.\n- Harassment, threats, or hateful content targeting a person or a group.\n- Spam farms: bulk near-duplicate pages or copy whose only purpose is to game search engines or send unsolicited mail.\nWhen a brief is ordinary business content, write it well and do not lecture.`\n\n/** A door's request, with the org's settings when the door knows the org. */\ninterface AiRuntimeOptions {\n /** The org's resolved `pluginSettings/ai`, for its provider and model choices. */\n settings?: AiPluginSettings\n /** A provider chosen by the caller, ahead of every setting; specs use it. */\n provider?: AiProvider\n}\n\nexport interface AiRequest extends AiRequestBase, AiRuntimeOptions {\n stream: false\n}\n\nexport interface AiStreamRequest extends AiRequestBase, AiRuntimeOptions {\n stream: true\n}\n\n/**\n * The cache guard, applied to the system blocks before any adapter sees\n * them. Exported so a spec and a door can assert a prompt's shape without\n * a provider.\n */\nexport function validateAiSystemBlocks(system: readonly AiSystemBlock[]): void {\n const breakpoints = system.filter((block) => block.cacheBreakpoint)\n if (breakpoints.length > AI_MAX_CACHE_BREAKPOINTS) {\n throw new AiRequestShapeError(\n `${breakpoints.length} cache breakpoints; at most ${AI_MAX_CACHE_BREAKPOINTS} are cached`,\n )\n }\n // Caching is a prefix match, so the LAST breakpoint decides what is\n // cached, and every block at or before it is inside the cached prefix. A\n // volatile block anywhere in that span — including one that is itself\n // the breakpoint — puts a per-request byte into the prefix (AGL-2352).\n const lastBreakpoint = system.reduce(\n (last, block, index) => (block.cacheBreakpoint ? index : last),\n -1,\n )\n system.forEach((block, index) => {\n if (block.volatile && index <= lastBreakpoint) {\n throw new AiRequestShapeError(\n `system block ${index} is volatile but sits inside the cached prefix (last breakpoint at block ${lastBreakpoint})`,\n )\n }\n })\n}\n\n/**\n * Whether a provider's model reads a picture in a user turn (AGL-2916): what\n * the provider's own descriptor of the model says. A model the provider does\n * not describe reads none, so an adopter's provider is never sent a picture\n * it did not say it can take.\n */\nexport function aiModelReadsImages(provider: AiProvider, model: string): boolean {\n return provider\n .models()\n .some((descriptor) => descriptor.id === model && descriptor.capabilities.vision === true)\n}\n\n/**\n * Whether a model a door resolved reads pictures on the provider that would\n * serve the request: what a door asks before it attaches one, so a model that\n * does not is sent the text alone rather than refused.\n */\nexport function aiRouteReadsImages(model: string, settings?: AiPluginSettings): boolean {\n const provider = resolveAiProvider(settings)\n return provider ? aiModelReadsImages(provider, model) : false\n}\n\n/** A character standard base64 does not use, outside its padding. */\nconst NOT_BASE64 = /[^A-Za-z0-9+/]/\n\n/**\n * Whether `data` is standard base64 and nothing else: its alphabet, then at\n * most two `=` of padding; no `data:` prefix, no line breaks.\n *\n * Scanned for a character outside the alphabet rather than matched whole: a\n * picture is megabytes of base64, and a whole-string pattern with a greedy\n * loop can exhaust the regular expression engine's backtrack stack on input\n * that large, which throws a RangeError instead of answering.\n */\nfunction isBareBase64(data: string): boolean {\n const body = data.endsWith('==') ? data.slice(0, -2) : data.endsWith('=') ? data.slice(0, -1) : data\n return body.length > 0 && !NOT_BASE64.test(body)\n}\n\n/**\n * The picture guard (AGL-2916), applied to the messages before any adapter\n * sees them. A picture rides only a user turn, only to a model that reads\n * pictures, only in a format every such model takes, as bare base64 within\n * the size a provider accepts, and no more of them than a request may carry.\n * `readsImages` is asked only when a turn carries a picture. Exported so a\n * spec and a door can assert a request's shape without a provider.\n */\nexport function validateAiMessages(messages: readonly AiMessage[], readsImages: () => boolean): void {\n messages.forEach((message, index) => {\n if (typeof message.content === 'string') return\n if (!Array.isArray(message.content) || message.content.length === 0) {\n throw new AiRequestShapeError(`message ${index} has neither text nor parts`)\n }\n for (const part of message.content) {\n if (part.type === 'text') {\n if (typeof part.text !== 'string') {\n throw new AiRequestShapeError(`message ${index} has a text part with no text`)\n }\n continue\n }\n if (part.type !== 'image') {\n throw new AiRequestShapeError(`message ${index} has a part of an unknown type`)\n }\n if (message.role !== 'user') {\n throw new AiRequestShapeError(`message ${index} is an assistant turn and carries a picture`)\n }\n if (!(AI_IMAGE_MEDIA_TYPES as readonly string[]).includes(part.mediaType)) {\n throw new AiRequestShapeError(`message ${index} carries a picture of an unsupported type`)\n }\n if (typeof part.data !== 'string') {\n throw new AiRequestShapeError(`message ${index} carries a picture that is not bare base64`)\n }\n // The size first: it needs only the length, so an oversized picture is\n // refused without reading its bytes.\n if (aiBase64Bytes(part.data) > AI_IMAGE_MAX_BYTES) {\n throw new AiRequestShapeError(\n `message ${index} carries a picture over ${AI_IMAGE_MAX_BYTES} bytes`,\n )\n }\n if (!isBareBase64(part.data)) {\n throw new AiRequestShapeError(`message ${index} carries a picture that is not bare base64`)\n }\n }\n })\n const images = aiMessageImages(messages).length\n if (images > AI_REQUEST_MAX_IMAGES) {\n throw new AiRequestShapeError(`${images} pictures; a request carries at most ${AI_REQUEST_MAX_IMAGES}`)\n }\n if (images > 0 && !readsImages()) {\n throw new AiRequestShapeError('the request carries a picture and its model does not read pictures')\n }\n}\n\n/**\n * Characters to a token when a prompt is measured rather than tokenized\n * (AGL-2937). Four is the high end of English prose, so reading a prefix\n * through it errs LOW on the token count — and a prefix counts as cached\n * only when it clears its model's minimum even at that reading. Erring the\n * other way would let a prompt claim a cache it does not get.\n */\nexport const AI_CACHE_PREFIX_TOKEN_CHARS = 4\n\n/**\n * The characters inside the cached span of a request: the tools, which a\n * provider renders ahead of the system blocks and which therefore sit in\n * every prefix, plus every system block up to and including the last\n * breakpoint. Zero when the request marks no breakpoint at all.\n */\nexport function aiCachedPrefixChars(\n system: readonly AiSystemBlock[],\n tools: readonly AiTool[] = [],\n): number {\n const lastBreakpoint = system.reduce(\n (last, block, index) => (block.cacheBreakpoint ? index : last),\n -1,\n )\n if (lastBreakpoint < 0) return 0\n const toolChars = tools.reduce((total, tool) => total + JSON.stringify(tool).length, 0)\n return system\n .slice(0, lastBreakpoint + 1)\n .reduce((total, block) => total + block.text.length, toolChars)\n}\n\n/**\n * Whether this request's cached span is long enough for the model to cache\n * it at all (AGL-2937). A prefix under the model's `cacheMinTokens` is\n * served with the breakpoints honored and nothing cached — no error, and a\n * usage report that reads as a permanent miss — so a door designed around a\n * cache has to answer this question before it believes its own markers, and\n * `runtime/ai-prompt-cache.spec.ts` answers it for every door at once.\n */\nexport function aiCachedPrefixCaches(\n model: string,\n system: readonly AiSystemBlock[],\n tools: readonly AiTool[] = [],\n): boolean {\n const chars = aiCachedPrefixChars(system, tools)\n return chars / AI_CACHE_PREFIX_TOKEN_CHARS >= aiModelCacheMinTokens(model)\n}\n\n/**\n * Whether a provider is configured to answer: registered, and its key\n * present. Every door asks this before it spends a reservation, and\n * answers 501 (or the docs fallback) when it is false.\n */\nexport function aiProviderReady(settings?: AiPluginSettings): boolean {\n return Boolean(resolveAiProvider(settings)?.readApiKey())\n}\n\n/** The provider that would answer, for a door that names it in its logs or its inventory. */\nexport function aiActiveProvider(settings?: AiPluginSettings): AiProvider | undefined {\n return resolveAiProvider(settings)\n}\n\nfunction providerFor(input: AiRuntimeOptions): { provider: AiProvider; apiKey: string } {\n const provider = input.provider ?? resolveAiProvider(input.settings)\n if (!provider) throw new Error('no AI provider is registered')\n const apiKey = provider.readApiKey()\n if (!apiKey) {\n // Every door gates on `aiProviderReady` before it gets here, so this is\n // a wiring fault rather than a customer path.\n throw new Error(`${provider.apiKeyEnv} is not set`)\n }\n return { provider, apiKey }\n}\n\n/**\n * Send one request through the resolved provider.\n *\n * Streaming: resolves once the provider has ACCEPTED the request — after\n * the headers, before the first token — with an iterator of simplified\n * events the caller wraps in its own wire format. A non-2xx answer rejects\n * with `AiUpstreamError` before the iterator exists, so a door can still\n * send a plain error status and hand back its reservation.\n *\n * Non-streaming: resolves with the parsed result, a decline typed as\n * `AiRefusal` rather than thrown.\n */\nexport async function runAiRequest(\n input: AiStreamRequest,\n): Promise<AsyncIterable<AiStreamEvent>>\nexport async function runAiRequest(input: AiRequest): Promise<AiResult>\nexport async function runAiRequest(\n input: AiRequest | AiStreamRequest,\n): Promise<AsyncIterable<AiStreamEvent> | AiResult> {\n validateAiSystemBlocks(input.system)\n const { provider, apiKey } = providerFor(input)\n validateAiMessages(input.messages, () => aiModelReadsImages(provider, input.model))\n const { stream, settings: _settings, provider: _provider, ...request } = input\n const providerRequest = { ...request, apiKey }\n try {\n return await (stream ? provider.stream(providerRequest) : provider.complete(providerRequest))\n } catch (error) {\n await reportAccountProblem(provider, error)\n throw error\n }\n}\n\n/**\n * Tells the operator when the failure is the platform's provider account —\n * a refused key, no credit — rather than the request (AGL-3377). Once per\n * provider and problem per window; loaded lazily so the runtime pulls no\n * Admin SDK until an account is actually broken. Never throws.\n */\nasync function reportAccountProblem(provider: AiProvider, error: unknown): Promise<void> {\n if (!(error instanceof AiUpstreamError) || !error.accountProblem) return\n try {\n const [{ raiseOperatorAlert }, { AI_PROVIDER_UNAVAILABLE }] = await Promise.all([\n import('./operator-alerts-seam'),\n import('../operator-alerts'),\n ])\n await raiseOperatorAlert(AI_PROVIDER_UNAVAILABLE, {\n dedupeKey: `${provider.id}:${error.accountProblem}`,\n context: {\n provider: provider.label,\n keyEnv: provider.apiKeyEnv,\n reason:\n error.accountProblem === 'credentials'\n ? `its API key was refused (HTTP ${error.status ?? 'unknown'})`\n : 'the account is out of credit',\n },\n })\n } catch (alertError) {\n console.error('[ai] the provider account alert could not be raised', alertError)\n }\n}\n"],"names":["aiModelCacheMinTokens","AI_IMAGE_MAX_BYTES","AI_IMAGE_MEDIA_TYPES","AI_REQUEST_MAX_IMAGES","AiRequestShapeError","AiUpstreamError","aiBase64Bytes","aiMessageImages","resolveAiProvider","AI_UPSTREAM_FAILURE_COPY","AI_MAX_CACHE_BREAKPOINTS","AI_ACCEPTABLE_USE_BLOCK","validateAiSystemBlocks","system","breakpoints","filter","block","cacheBreakpoint","length","lastBreakpoint","reduce","last","index","forEach","volatile","aiModelReadsImages","provider","model","models","some","descriptor","id","capabilities","vision","aiRouteReadsImages","settings","NOT_BASE64","isBareBase64","data","body","endsWith","slice","test","validateAiMessages","messages","readsImages","message","content","Array","isArray","part","type","text","role","includes","mediaType","images","AI_CACHE_PREFIX_TOKEN_CHARS","aiCachedPrefixChars","tools","toolChars","total","tool","JSON","stringify","aiCachedPrefixCaches","chars","aiProviderReady","Boolean","readApiKey","aiActiveProvider","providerFor","input","Error","apiKey","apiKeyEnv","runAiRequest","stream","_settings","_provider","request","providerRequest","complete","error","reportAccountProblem","accountProblem","raiseOperatorAlert","AI_PROVIDER_UNAVAILABLE","Promise","all","dedupeKey","context","label","keyEnv","reason","status","alertError","console"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The static edge to the operator-alert pipeline that `ai-runtime.ts` defers
|
|
19
|
+
* by RELATIVE path (AGL-3377).
|
|
20
|
+
*
|
|
21
|
+
* nx treats a workspace lib that is ever `import()`ed by its specifier as
|
|
22
|
+
* lazy-loaded everywhere, and `@nx/enforce-module-boundaries` then forbids
|
|
23
|
+
* every static import of `@aglyn/tenant-data-admin` in every project. A
|
|
24
|
+
* relative specifier crosses no project boundary, so deferring THIS file keeps
|
|
25
|
+
* the Admin SDK out of the runtime until a provider account is actually
|
|
26
|
+
* broken, without recording a lazy edge.
|
|
27
|
+
*/
|
|
28
|
+
export { raiseOperatorAlert } from '@aglyn/tenant-data-admin/server/operator-alerts';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* The static edge to the operator-alert pipeline that `ai-runtime.ts` defers
|
|
18
|
+
* by RELATIVE path (AGL-3377).
|
|
19
|
+
*
|
|
20
|
+
* nx treats a workspace lib that is ever `import()`ed by its specifier as
|
|
21
|
+
* lazy-loaded everywhere, and `@nx/enforce-module-boundaries` then forbids
|
|
22
|
+
* every static import of `@aglyn/tenant-data-admin` in every project. A
|
|
23
|
+
* relative specifier crosses no project boundary, so deferring THIS file keeps
|
|
24
|
+
* the Admin SDK out of the runtime until a provider account is actually
|
|
25
|
+
* broken, without recording a lazy edge.
|
|
26
|
+
*/ export { raiseOperatorAlert } from "@aglyn/tenant-data-admin/server/operator-alerts";
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=operator-alerts-seam.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/operator-alerts-seam.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The static edge to the operator-alert pipeline that `ai-runtime.ts` defers\n * by RELATIVE path (AGL-3377).\n *\n * nx treats a workspace lib that is ever `import()`ed by its specifier as\n * lazy-loaded everywhere, and `@nx/enforce-module-boundaries` then forbids\n * every static import of `@aglyn/tenant-data-admin` in every project. A\n * relative specifier crosses no project boundary, so deferring THIS file keeps\n * the Admin SDK out of the runtime until a provider account is actually\n * broken, without recording a lazy edge.\n */\nexport { raiseOperatorAlert } from '@aglyn/tenant-data-admin/server/operator-alerts'\n"],"names":["raiseOperatorAlert"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,GACD,SAASA,kBAAkB,QAAQ,kDAAiD"}
|