@x12i/ai-gateway 9.3.0 β†’ 9.3.4

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/README.md CHANGED
@@ -63,6 +63,7 @@ npm install @x12i/ai-gateway
63
63
  **πŸ“š Documentation**: After installation, documentation is available in:
64
64
  - `node_modules/@x12i/ai-gateway/CONTENT_RESOLVER_UPSTREAM_GUIDE.md` - **Content resolver (nx-content)**: config, keys, local/git, upstream checklist
65
65
  - `node_modules/@x12i/ai-gateway/docs/IDENTITY_OBJECT_CONTRACT.md` - **Identity contract** for Activix (`sessionId` + `instance`)
66
+ - `node_modules/@x12i/ai-gateway/docs/AI_GATEWAY_INVOKE_EXECUTION_METADATA.md` - **Invoke metadata**, cost/billing (G8), output contract (G6), Activix completion fields
66
67
  - `node_modules/@x12i/ai-gateway/docs/LOGGER_INITIALIZATION.md` - **Required reading**: How to properly initialize logger
67
68
  - `node_modules/@x12i/ai-gateway/TROUBLESHOOTING.md` - Troubleshooting guide
68
69
  - `node_modules/@x12i/ai-gateway/TROUBLESHOOTING_TOOLBOX.md` - Diagnostic tools
@@ -309,7 +310,7 @@ The gateway reads **Mongo connection** settings from the environment, but **coll
309
310
  `ActivityManager` drives **`@x12i/activix` v7** with a **two-phase** API:
310
311
 
311
312
  1. **`startRecord`** β€” Inserts a new document with **`status: 'started'`**, **`startTime`**, **`runContext`** (same object as **`request.identity`**), root **`request`** / **`config`** snapshots, gateway metadata (e.g. **`activityType`**, **`aiRequestId`**), and the initial **`outer`** fragment (see below). Activix returns **`activityId`** (prefix **`act-`**, configured as the collection **`primaryKey`**); that id is used for all later updates β€” **not** `jobId`.
312
- 2. **`completeRecord`** or **`failRecord`** β€” Patches the **same** document by **`activityId`**. Success adds **`response`**, **`endTime`**, **`duration`**, **`cost`**, refreshed **`status`**, and sets **`outer.output`** to the completion payload. Failure adds error details (and may attach **`outer.output`** for certain failure modes such as response parsing).
313
+ 2. **`completeRecord`** or **`failRecord`** β€” Patches the **same** document by **`activityId`**. On success, adds **`response`**, **`endTime`**, **`duration`**, root **`cost`** / **`costUsd`** / **`costStatus`**, sets **`outer.output`** to the completion payload, merges billing into **`outer.metadata`**, and when priced or unpriced with usage, sets Activix **`outer.cost`** (`usd`, `tokens`, `provider`, `model`, optional `details`). Failure adds error details (and may attach **`outer.output`** for certain failure modes such as response parsing).
313
314
 
314
315
  **How a document is shaped (reading `ai-actions` in Mongo)**
315
316
 
@@ -317,7 +318,7 @@ The gateway reads **Mongo connection** settings from the environment, but **coll
317
318
  - **Root-level copies** of common identity fields may appear beside **`runContext`** for convenient indexing; treat **`runContext`** as the full envelope when in doubt.
318
319
  - **`request`**: Structured snapshot only β€” **`raw`** / **`parsed`** instructions, context, prompt; **`messages`**; **`workingMemory`** (template/user payload). There is **no** separate legacy **`input`** field on this object; use **`workingMemory`**.
319
320
  - **`config`**: `model`, `provider`, `temperature`, `maxTokens`, **`rawConfig`** (exact router config).
320
- - **`outer`**: Activix v7 **validated I/O** at the document root. At **start**, **`outer.input`** contains **`activityType`** and the same **`request`** snapshot as root **`request`** when a body exists (`{ activityType, request }`). At **success**, **`outer.output`** matches the **`response`** object written on completion. Root **`request`** / **`response`** support querying and older tooling; **`outer`** satisfies Activix’s envelope β€” so the same logical request snapshot can appear both at **`request`** and under **`outer.input.request`** by design. Large provider blobs (**`response.content.fullResponse`**) and size limits are described in [Activities outer duplication & payload controls](./docs/ACTIVITIES_OUTER_DUPLICATION.md).
321
+ - **`outer`**: Activix v7 **validated I/O** at the document root. At **start**, **`outer.input`** contains **`activityType`** and the same **`request`** snapshot as root **`request`** when a body exists (`{ activityType, request }`). At **success**, **`outer.output`** matches the **`response`** object written on completion; **`outer.metadata`** mirrors routing and billing from **`response.metadata`** (`modelUsed`, `provider`, `cost`, `costUsd`, `costStatus`, optional `costBreakdown`); **`outer.cost`** holds the canonical Activix cost object when usage or price is known (see [Cost reporting](#cost-reporting-invoke-response--activix-run-analysis-g8) below). Root **`request`** / **`response`** support querying and older tooling; **`outer`** satisfies Activix’s envelope β€” so the same logical request snapshot can appear both at **`request`** and under **`outer.input.request`** by design. Large provider blobs (**`response.content.fullResponse`**) and size limits are described in [Activities outer duplication & payload controls](./docs/ACTIVITIES_OUTER_DUPLICATION.md).
321
322
 
322
323
  **Environment variable priority (Activix / Mongo β€” implemented in `@x12i/activix`, not in `activity-tracking-config.ts`):**
323
324
  - **Mongo URI**: `MONGO_LOGS_URI` if set, otherwise **`MONGO_URI`**. If neither is set, Activix cannot use the database.
@@ -401,6 +402,24 @@ const gateway = new AIGateway({
401
402
  });
402
403
  ```
403
404
 
405
+ #### Cost reporting (invoke response + Activix, Run Analysis G8)
406
+
407
+ Billing is resolved once per successful **`invoke()`** / **`invokeChat()`** via **`resolveCostCompletionWithAiTools`** (see [`docs/AI_GATEWAY_INVOKE_EXECUTION_METADATA.md`](./docs/AI_GATEWAY_INVOKE_EXECUTION_METADATA.md)):
408
+
409
+ | Layer | Fields |
410
+ |--------|--------|
411
+ | **Router** (`@x12i/ai-providers-router`) | Preferred source: **`metadata.costStatus`** (`priced` \| `unpriced`), **`metadata.costUsd`** / **`metadata.cost`** when priced |
412
+ | **Gateway response** | Same slice on **`response.metadata`**: **`costStatus`**, **`costUsd`**, **`cost`**, optional **`costBreakdown`** (when **`aiTools.calculateCost`** and catalog pricing apply and the router left cost unpriced) |
413
+ | **Activix activity (on `logSuccess`)** | Root **`cost`**, **`costUsd`**, **`costStatus`**; **`outer.metadata`** mirror; **`outer.cost`** (`usd`, `tokens` with `input`/`output`/`total`, `provider`, `model`, `details.costStatus`, optional `details.costBreakdown`) |
414
+
415
+ **`costStatus` semantics:**
416
+
417
+ - **`priced`** β€” **`costUsd`** / **`cost`** is a finite USD amount for this call (from the router or from **`@x12i/ai-tools`** catalog **`CostCalculator`** when the router did not price).
418
+ - **`unpriced`** β€” Token usage was recorded but no authoritative USD price was available (explicit router **`unpriced`** is never overridden by catalog).
419
+ - Omitted β€” No non-zero token usage (no billing signal).
420
+
421
+ Requires **`enableActivityTracking: true`** (default when Mongo/env is configured) for Activix persistence; invoke metadata is always set on the gateway response regardless.
422
+
404
423
  **Tests before release:**
405
424
 
406
425
  ```bash
@@ -472,7 +491,7 @@ When the gateway constructs Activix internally, each collection uses **`primaryK
472
491
  - **Config data**: Stored in **`config`** (model, provider, temperature, maxTokens, **`rawConfig`**)
473
492
  - **Response data**: Stored in **`response`** on completion (content, metadata, optional **`fullResponse`** per diagnostics)
474
493
  - **Activix I/O**: Root **`outer`** β€” **`outer.input`** at start, **`outer.output`** on success (and some failure paths)
475
- - **Cost**: Calculated and stored per activity on success
494
+ - **Cost / billing**: On success, root **`cost`**, **`costUsd`**, **`costStatus`**, plus **`outer.metadata`** and **`outer.cost`** (same values as **`response.metadata`** from the invoke path β€” router passthrough or catalog pricing via **`@x12i/ai-tools`**)
476
495
 
477
496
  **Best Practices for Type IDs:**
478
497
  - **`jobTypeId`**: Use MD5 hash of your job type string (e.g., `MD5('data-processing-job')`) for consistent job-level aggregation
@@ -1119,7 +1138,7 @@ The gateway uses **`@x12i/activix` v7** (xronox-activitix) for full lifecycle lo
1119
1138
  - Sends **`runContext`**, **`request`**, **`config`**, **`startTime`**, **`status: 'started'`**, plus Activix **`outer.input`** (wraps **`activityType`** and the same **`request`** snapshot when present β€” see section 2).
1120
1139
  - Returns **`activityId`** (and record payload) for phase 2.
1121
1140
  - **Phase 2 (complete / fail)**: Updates the SAME document by **`activityId`**
1122
- - Success: **`response`**, **`cost`**, **`endTime`**, **`duration`**, **`status`**, and **`outer.output`** set to the completion **`response`** payload (request/config are **not** re-sent).
1141
+ - Success: **`response`**, root **`cost`** / **`costUsd`** / **`costStatus`**, **`endTime`**, **`duration`**, **`status`**, **`outer.output`** (completion payload), **`outer.metadata`** (routing + billing mirror), and **`outer.cost`** when usage or price is known (see [Cost reporting](#cost-reporting-invoke-response--activix-run-analysis-g8)).
1123
1142
  - Failure: error payload and timing; optional **`response`** / **`outer.output`** only for specific failure kinds.
1124
1143
 
1125
1144
  4. **Structured fields vs Activix `outer` (v2.6.0+):**
@@ -1264,8 +1283,22 @@ Example shape for a completed row in **`ai-actions`** (`activityType: 'gateway-i
1264
1283
  // completeRecord: outer.output ← same object as root `response` on success
1265
1284
  outer: {
1266
1285
  input: { activityType: 'gateway-invocation', request: { /* same snapshot as root request */ } },
1267
- output: { /* success: normalized gateway response object */ },
1268
- metadata: { /* tier metadata / aiRequestId routing β€” see @x12i/activix */ }
1286
+ output: { /* success: gateway activity response (content, parsed, metadata, usage) */ },
1287
+ metadata: {
1288
+ modelUsed: 'openai/gpt-5-nano-2025-08-07',
1289
+ provider: 'openrouter',
1290
+ cost: 0.0000348,
1291
+ costUsd: 0.0000348,
1292
+ costStatus: 'priced'
1293
+ },
1294
+ cost: {
1295
+ usd: 0.0000348,
1296
+ unit: 'USD',
1297
+ tokens: { input: 16, output: 85, total: 101 },
1298
+ provider: 'openrouter',
1299
+ model: 'openai/gpt-5-nano-2025-08-07',
1300
+ details: { costStatus: 'priced' /* optional costBreakdown when aiTools.costIncludeBreakdown */ }
1301
+ }
1269
1302
  },
1270
1303
  // inner: optional step array for multi-step flows (see @x12i/activix docs)
1271
1304
 
@@ -1306,8 +1339,10 @@ Example shape for a completed row in **`ai-actions`** (`activityType: 'gateway-i
1306
1339
  metadata: {...}
1307
1340
  },
1308
1341
 
1309
- // Cost (from logSuccess)
1310
- cost: 0.002,
1342
+ // Billing (from logSuccess β€” mirrors response.metadata from invoke)
1343
+ cost: 0.0000348,
1344
+ costUsd: 0.0000348,
1345
+ costStatus: 'priced',
1311
1346
 
1312
1347
  // Metadata
1313
1348
  createdAt: Date,
@@ -1319,7 +1354,7 @@ Example shape for a completed row in **`ai-actions`** (`activityType: 'gateway-i
1319
1354
  - βœ… Each activity = separate Mongo document (**`_id`**) with stable **`activityId`** (`act-…`) for Activix APIs
1320
1355
  - βœ… **`aiRequestId`** = per-request correlation (required on invoke)
1321
1356
  - βœ… **`runContext.jobId`** / **`runContext.taskId`** = upstream identity (required on invoke since v9+)
1322
- - βœ… Request/config sent at **start**; response/timing/cost at **complete**
1357
+ - βœ… Request/config sent at **start**; response/timing/billing (`cost`, `costUsd`, `costStatus`, `outer.cost`) at **complete**
1323
1358
  - βœ… Updates target **`activityId`** from **`startRecord`**, not **`jobId`**
1324
1359
 
1325
1360
  #### Retry Tracking (@x12i/activix v7)
@@ -1455,8 +1490,16 @@ const response = await gateway.invoke({
1455
1490
  cacheTotalTokens?: number
1456
1491
  },
1457
1492
  model?: string, // Model ID used (e.g., 'gpt-4o', 'claude-sonnet-4')
1493
+ modelUsed?: string, // Resolved/served model id (when distinct from request model)
1458
1494
  provider?: string, // Provider used (e.g., 'openai', 'anthropic')
1459
- cost?: number, // Cost in USD (if available)
1495
+ costStatus?: 'priced' | 'unpriced', // Billing state (Run Analysis G8)
1496
+ costUsd?: number, // USD when costStatus === 'priced' (preferred field)
1497
+ cost?: number, // USD mirror of costUsd when priced
1498
+ costBreakdown?: { // Optional when aiTools catalog pricing runs (calculateCost + breakdown)
1499
+ promptCostUsd?: number;
1500
+ completionCostUsd?: number;
1501
+ // ...other breakdown keys from @x12i/ai-tools
1502
+ },
1460
1503
 
1461
1504
  // ============================================
1462
1505
  // Inference Output Parsing (if inferenceType provided)
@@ -1503,8 +1546,10 @@ const response = await gateway.invoke({
1503
1546
  - `metadata.jobId` - Job ID for correlation
1504
1547
  - `metadata.latencyMs` - Request duration in milliseconds
1505
1548
  - `metadata.tokens` - Token breakdown (prompt, completion, total, cache tokens)
1506
- - `metadata.cost` - Cost in USD
1507
- - `metadata.model` - Model ID used
1549
+ - `metadata.costStatus` - `priced` | `unpriced` (see [Cost reporting](#cost-reporting-invoke-response--activix-run-analysis-g8))
1550
+ - `metadata.costUsd` / `metadata.cost` - USD when priced
1551
+ - `metadata.costBreakdown` - Optional catalog breakdown when `aiTools.calculateCost` applies
1552
+ - `metadata.model` / `metadata.modelUsed` - Model id used
1508
1553
  - `metadata.provider` - Provider used
1509
1554
 
1510
1555
  #### Example: Full Response
@@ -1554,8 +1599,10 @@ const response = await gateway.invoke({
1554
1599
  completion: 50,
1555
1600
  total: 150
1556
1601
  },
1557
- model: 'gpt-5-mini',
1602
+ modelUsed: 'gpt-5-mini',
1558
1603
  provider: 'openai',
1604
+ costStatus: 'priced',
1605
+ costUsd: 0.002,
1559
1606
  cost: 0.002,
1560
1607
 
1561
1608
  // Inference output (parsed)
@@ -121,6 +121,7 @@ export declare class ActivityManager {
121
121
  logSuccess(activity: ActivityMetadata | undefined, details: {
122
122
  cost?: number;
123
123
  costStatus?: 'priced' | 'unpriced';
124
+ costBreakdown?: Record<string, unknown>;
124
125
  response: any;
125
126
  endTime: number;
126
127
  duration: number;
@@ -133,34 +133,120 @@ function logUpstreamIdentityWarnings(logger, incomingIdentity, merged) {
133
133
  }));
134
134
  }
135
135
  }
136
- /** Routing / generation facts from gateway response metadata for Activix `outer.metadata` on completion. */
137
- function pickActivixCompletionRoutingMetadata(response) {
136
+ /** Token counts for Activix `outer.cost.tokens` (maps gateway prompt/completion β†’ input/output). */
137
+ function pickActivixUsageTokens(response) {
138
138
  if (response == null || typeof response !== 'object')
139
- return {};
140
- const meta = response.metadata;
141
- if (meta == null || typeof meta !== 'object')
142
- return {};
143
- const m = meta;
139
+ return undefined;
140
+ const r = response;
141
+ const raw = (r.usage != null && typeof r.usage === 'object' ? r.usage : undefined) ??
142
+ (r.metadata != null && typeof r.metadata === 'object'
143
+ ? r.metadata.tokens
144
+ : undefined);
145
+ if (raw == null || typeof raw !== 'object')
146
+ return undefined;
147
+ const t = raw;
148
+ const input = typeof t.prompt === 'number'
149
+ ? t.prompt
150
+ : typeof t.input === 'number'
151
+ ? t.input
152
+ : undefined;
153
+ const output = typeof t.completion === 'number'
154
+ ? t.completion
155
+ : typeof t.output === 'number'
156
+ ? t.output
157
+ : undefined;
158
+ const total = typeof t.total === 'number' ? t.total : undefined;
159
+ if (input === undefined && output === undefined && total === undefined)
160
+ return undefined;
161
+ return {
162
+ ...(input !== undefined ? { input } : {}),
163
+ ...(output !== undefined ? { output } : {}),
164
+ ...(total !== undefined ? { total } : {})
165
+ };
166
+ }
167
+ /**
168
+ * Activix v6+ `outer.cost` from gateway billing + routing metadata (Run Analysis G8).
169
+ */
170
+ function buildActivixOuterCost(routingMeta, billing, response) {
171
+ const usd = typeof billing.cost === 'number' && Number.isFinite(billing.cost)
172
+ ? billing.cost
173
+ : typeof routingMeta.costUsd === 'number' && Number.isFinite(routingMeta.costUsd)
174
+ ? routingMeta.costUsd
175
+ : typeof routingMeta.cost === 'number' && Number.isFinite(routingMeta.cost)
176
+ ? routingMeta.cost
177
+ : undefined;
178
+ const tokens = pickActivixUsageTokens(response);
179
+ const provider = typeof routingMeta.provider === 'string' ? routingMeta.provider : undefined;
180
+ const model = typeof routingMeta.modelUsed === 'string'
181
+ ? routingMeta.modelUsed
182
+ : typeof routingMeta.model === 'string'
183
+ ? routingMeta.model
184
+ : undefined;
185
+ const details = {};
186
+ if (billing.costStatus === 'priced' || billing.costStatus === 'unpriced') {
187
+ details.costStatus = billing.costStatus;
188
+ }
189
+ if (billing.costBreakdown != null && typeof billing.costBreakdown === 'object') {
190
+ details.costBreakdown = billing.costBreakdown;
191
+ }
192
+ const hasDetails = Object.keys(details).length > 0;
193
+ if (usd === undefined && !tokens && !provider && !model && !hasDetails) {
194
+ return undefined;
195
+ }
196
+ return {
197
+ ...(usd !== undefined ? { usd, unit: 'USD' } : {}),
198
+ ...(tokens ? { tokens } : {}),
199
+ ...(provider ? { provider } : {}),
200
+ ...(model ? { model } : {}),
201
+ ...(hasDetails ? { details } : {})
202
+ };
203
+ }
204
+ /** Routing / generation facts for Activix `outer.metadata` on completion (includes billing mirror). */
205
+ function pickActivixCompletionRoutingMetadata(response, billing) {
144
206
  const out = {};
145
- if (typeof m.modelUsed === 'string')
146
- out.modelUsed = m.modelUsed;
147
- if (typeof m.model === 'string')
148
- out.model = m.model;
149
- if (typeof m.provider === 'string')
150
- out.provider = m.provider;
151
- if (typeof m.maxTokensRequested === 'number')
152
- out.maxTokensRequested = m.maxTokensRequested;
153
- if (typeof m.region === 'string')
154
- out.region = m.region;
155
- if (m.effectiveModelConfig != null && typeof m.effectiveModelConfig === 'object') {
156
- out.effectiveModelConfig = m.effectiveModelConfig;
207
+ if (response != null && typeof response === 'object') {
208
+ const meta = response.metadata;
209
+ if (meta != null && typeof meta === 'object') {
210
+ const m = meta;
211
+ if (typeof m.modelUsed === 'string')
212
+ out.modelUsed = m.modelUsed;
213
+ if (typeof m.model === 'string')
214
+ out.model = m.model;
215
+ if (typeof m.provider === 'string')
216
+ out.provider = m.provider;
217
+ if (typeof m.maxTokensRequested === 'number')
218
+ out.maxTokensRequested = m.maxTokensRequested;
219
+ if (typeof m.region === 'string')
220
+ out.region = m.region;
221
+ if (m.effectiveModelConfig != null && typeof m.effectiveModelConfig === 'object') {
222
+ out.effectiveModelConfig = m.effectiveModelConfig;
223
+ }
224
+ if (typeof m.cost === 'number' && Number.isFinite(m.cost))
225
+ out.cost = m.cost;
226
+ if (typeof m.costUsd === 'number' && Number.isFinite(m.costUsd))
227
+ out.costUsd = m.costUsd;
228
+ if (m.costStatus === 'priced' || m.costStatus === 'unpriced')
229
+ out.costStatus = m.costStatus;
230
+ if (m.costBreakdown != null && typeof m.costBreakdown === 'object') {
231
+ out.costBreakdown = m.costBreakdown;
232
+ }
233
+ }
234
+ }
235
+ if (billing) {
236
+ if ((out.costStatus !== 'priced' && out.costStatus !== 'unpriced') &&
237
+ (billing.costStatus === 'priced' || billing.costStatus === 'unpriced')) {
238
+ out.costStatus = billing.costStatus;
239
+ }
240
+ if (typeof billing.cost === 'number' && Number.isFinite(billing.cost)) {
241
+ if (out.cost === undefined)
242
+ out.cost = billing.cost;
243
+ if (out.costUsd === undefined)
244
+ out.costUsd = billing.cost;
245
+ }
246
+ if (out.costBreakdown === undefined && billing.costBreakdown != null) {
247
+ out.costBreakdown = billing.costBreakdown;
248
+ }
157
249
  }
158
- if (typeof m.cost === 'number' && Number.isFinite(m.cost))
159
- out.cost = m.cost;
160
- if (typeof m.costUsd === 'number' && Number.isFinite(m.costUsd))
161
- out.costUsd = m.costUsd;
162
- if (m.costStatus === 'priced' || m.costStatus === 'unpriced')
163
- out.costStatus = m.costStatus;
164
250
  return out;
165
251
  }
166
252
  function mergeGatewayActivityIdentity(request, aiRequestId, extras) {
@@ -848,13 +934,24 @@ export class ActivityManager {
848
934
  });
849
935
  return;
850
936
  }
937
+ const billingSlice = {
938
+ cost: details.cost,
939
+ costStatus: details.costStatus,
940
+ costBreakdown: details.costBreakdown
941
+ };
942
+ const outerMetadata = pickActivixCompletionRoutingMetadata(details.response, billingSlice);
943
+ const outerCost = buildActivixOuterCost(outerMetadata, billingSlice, details.response);
851
944
  await this.activix.completeRecord(activity.activityId, {
852
945
  cost: details.cost,
946
+ ...(typeof details.cost === 'number' && Number.isFinite(details.cost)
947
+ ? { costUsd: details.cost }
948
+ : {}),
853
949
  ...(details.costStatus ? { costStatus: details.costStatus } : {}),
854
950
  response: details.response,
855
951
  outer: {
856
952
  output: details.response,
857
- metadata: pickActivixCompletionRoutingMetadata(details.response)
953
+ metadata: outerMetadata,
954
+ ...(outerCost ? { cost: outerCost } : {})
858
955
  },
859
956
  endTime: details.endTime,
860
957
  duration: details.duration
package/dist/gateway.js CHANGED
@@ -142,7 +142,10 @@ export class AIGateway {
142
142
  : { cost: costCompletionChat.cost })
143
143
  }
144
144
  : {}),
145
- ...(costCompletionChat.costStatus ? { costStatus: costCompletionChat.costStatus } : {})
145
+ ...(costCompletionChat.costStatus ? { costStatus: costCompletionChat.costStatus } : {}),
146
+ ...(costCompletionChat.costBreakdown
147
+ ? { costBreakdown: costCompletionChat.costBreakdown }
148
+ : {})
146
149
  }
147
150
  };
148
151
  // Track activity success if activity was started
@@ -587,6 +590,7 @@ export class AIGateway {
587
590
  }
588
591
  : {}),
589
592
  ...(costCompletion.costStatus ? { costStatus: costCompletion.costStatus } : {}),
593
+ ...(costCompletion.costBreakdown ? { costBreakdown: costCompletion.costBreakdown } : {}),
590
594
  ...(traceEnabled
591
595
  ? {
592
596
  requestIds: traceRequestIds,
@@ -133,34 +133,120 @@ function logUpstreamIdentityWarnings(logger, incomingIdentity, merged) {
133
133
  }));
134
134
  }
135
135
  }
136
- /** Routing / generation facts from gateway response metadata for Activix `outer.metadata` on completion. */
137
- function pickActivixCompletionRoutingMetadata(response) {
136
+ /** Token counts for Activix `outer.cost.tokens` (maps gateway prompt/completion β†’ input/output). */
137
+ function pickActivixUsageTokens(response) {
138
138
  if (response == null || typeof response !== 'object')
139
- return {};
140
- const meta = response.metadata;
141
- if (meta == null || typeof meta !== 'object')
142
- return {};
143
- const m = meta;
139
+ return undefined;
140
+ const r = response;
141
+ const raw = (r.usage != null && typeof r.usage === 'object' ? r.usage : undefined) ??
142
+ (r.metadata != null && typeof r.metadata === 'object'
143
+ ? r.metadata.tokens
144
+ : undefined);
145
+ if (raw == null || typeof raw !== 'object')
146
+ return undefined;
147
+ const t = raw;
148
+ const input = typeof t.prompt === 'number'
149
+ ? t.prompt
150
+ : typeof t.input === 'number'
151
+ ? t.input
152
+ : undefined;
153
+ const output = typeof t.completion === 'number'
154
+ ? t.completion
155
+ : typeof t.output === 'number'
156
+ ? t.output
157
+ : undefined;
158
+ const total = typeof t.total === 'number' ? t.total : undefined;
159
+ if (input === undefined && output === undefined && total === undefined)
160
+ return undefined;
161
+ return {
162
+ ...(input !== undefined ? { input } : {}),
163
+ ...(output !== undefined ? { output } : {}),
164
+ ...(total !== undefined ? { total } : {})
165
+ };
166
+ }
167
+ /**
168
+ * Activix v6+ `outer.cost` from gateway billing + routing metadata (Run Analysis G8).
169
+ */
170
+ function buildActivixOuterCost(routingMeta, billing, response) {
171
+ const usd = typeof billing.cost === 'number' && Number.isFinite(billing.cost)
172
+ ? billing.cost
173
+ : typeof routingMeta.costUsd === 'number' && Number.isFinite(routingMeta.costUsd)
174
+ ? routingMeta.costUsd
175
+ : typeof routingMeta.cost === 'number' && Number.isFinite(routingMeta.cost)
176
+ ? routingMeta.cost
177
+ : undefined;
178
+ const tokens = pickActivixUsageTokens(response);
179
+ const provider = typeof routingMeta.provider === 'string' ? routingMeta.provider : undefined;
180
+ const model = typeof routingMeta.modelUsed === 'string'
181
+ ? routingMeta.modelUsed
182
+ : typeof routingMeta.model === 'string'
183
+ ? routingMeta.model
184
+ : undefined;
185
+ const details = {};
186
+ if (billing.costStatus === 'priced' || billing.costStatus === 'unpriced') {
187
+ details.costStatus = billing.costStatus;
188
+ }
189
+ if (billing.costBreakdown != null && typeof billing.costBreakdown === 'object') {
190
+ details.costBreakdown = billing.costBreakdown;
191
+ }
192
+ const hasDetails = Object.keys(details).length > 0;
193
+ if (usd === undefined && !tokens && !provider && !model && !hasDetails) {
194
+ return undefined;
195
+ }
196
+ return {
197
+ ...(usd !== undefined ? { usd, unit: 'USD' } : {}),
198
+ ...(tokens ? { tokens } : {}),
199
+ ...(provider ? { provider } : {}),
200
+ ...(model ? { model } : {}),
201
+ ...(hasDetails ? { details } : {})
202
+ };
203
+ }
204
+ /** Routing / generation facts for Activix `outer.metadata` on completion (includes billing mirror). */
205
+ function pickActivixCompletionRoutingMetadata(response, billing) {
144
206
  const out = {};
145
- if (typeof m.modelUsed === 'string')
146
- out.modelUsed = m.modelUsed;
147
- if (typeof m.model === 'string')
148
- out.model = m.model;
149
- if (typeof m.provider === 'string')
150
- out.provider = m.provider;
151
- if (typeof m.maxTokensRequested === 'number')
152
- out.maxTokensRequested = m.maxTokensRequested;
153
- if (typeof m.region === 'string')
154
- out.region = m.region;
155
- if (m.effectiveModelConfig != null && typeof m.effectiveModelConfig === 'object') {
156
- out.effectiveModelConfig = m.effectiveModelConfig;
207
+ if (response != null && typeof response === 'object') {
208
+ const meta = response.metadata;
209
+ if (meta != null && typeof meta === 'object') {
210
+ const m = meta;
211
+ if (typeof m.modelUsed === 'string')
212
+ out.modelUsed = m.modelUsed;
213
+ if (typeof m.model === 'string')
214
+ out.model = m.model;
215
+ if (typeof m.provider === 'string')
216
+ out.provider = m.provider;
217
+ if (typeof m.maxTokensRequested === 'number')
218
+ out.maxTokensRequested = m.maxTokensRequested;
219
+ if (typeof m.region === 'string')
220
+ out.region = m.region;
221
+ if (m.effectiveModelConfig != null && typeof m.effectiveModelConfig === 'object') {
222
+ out.effectiveModelConfig = m.effectiveModelConfig;
223
+ }
224
+ if (typeof m.cost === 'number' && Number.isFinite(m.cost))
225
+ out.cost = m.cost;
226
+ if (typeof m.costUsd === 'number' && Number.isFinite(m.costUsd))
227
+ out.costUsd = m.costUsd;
228
+ if (m.costStatus === 'priced' || m.costStatus === 'unpriced')
229
+ out.costStatus = m.costStatus;
230
+ if (m.costBreakdown != null && typeof m.costBreakdown === 'object') {
231
+ out.costBreakdown = m.costBreakdown;
232
+ }
233
+ }
234
+ }
235
+ if (billing) {
236
+ if ((out.costStatus !== 'priced' && out.costStatus !== 'unpriced') &&
237
+ (billing.costStatus === 'priced' || billing.costStatus === 'unpriced')) {
238
+ out.costStatus = billing.costStatus;
239
+ }
240
+ if (typeof billing.cost === 'number' && Number.isFinite(billing.cost)) {
241
+ if (out.cost === undefined)
242
+ out.cost = billing.cost;
243
+ if (out.costUsd === undefined)
244
+ out.costUsd = billing.cost;
245
+ }
246
+ if (out.costBreakdown === undefined && billing.costBreakdown != null) {
247
+ out.costBreakdown = billing.costBreakdown;
248
+ }
157
249
  }
158
- if (typeof m.cost === 'number' && Number.isFinite(m.cost))
159
- out.cost = m.cost;
160
- if (typeof m.costUsd === 'number' && Number.isFinite(m.costUsd))
161
- out.costUsd = m.costUsd;
162
- if (m.costStatus === 'priced' || m.costStatus === 'unpriced')
163
- out.costStatus = m.costStatus;
164
250
  return out;
165
251
  }
166
252
  function mergeGatewayActivityIdentity(request, aiRequestId, extras) {
@@ -848,13 +934,24 @@ export class ActivityManager {
848
934
  });
849
935
  return;
850
936
  }
937
+ const billingSlice = {
938
+ cost: details.cost,
939
+ costStatus: details.costStatus,
940
+ costBreakdown: details.costBreakdown
941
+ };
942
+ const outerMetadata = pickActivixCompletionRoutingMetadata(details.response, billingSlice);
943
+ const outerCost = buildActivixOuterCost(outerMetadata, billingSlice, details.response);
851
944
  await this.activix.completeRecord(activity.activityId, {
852
945
  cost: details.cost,
946
+ ...(typeof details.cost === 'number' && Number.isFinite(details.cost)
947
+ ? { costUsd: details.cost }
948
+ : {}),
853
949
  ...(details.costStatus ? { costStatus: details.costStatus } : {}),
854
950
  response: details.response,
855
951
  outer: {
856
952
  output: details.response,
857
- metadata: pickActivixCompletionRoutingMetadata(details.response)
953
+ metadata: outerMetadata,
954
+ ...(outerCost ? { cost: outerCost } : {})
858
955
  },
859
956
  endTime: details.endTime,
860
957
  duration: details.duration
@@ -121,6 +121,7 @@ export declare class ActivityManager {
121
121
  logSuccess(activity: ActivityMetadata | undefined, details: {
122
122
  cost?: number;
123
123
  costStatus?: 'priced' | 'unpriced';
124
+ costBreakdown?: Record<string, unknown>;
124
125
  response: any;
125
126
  endTime: number;
126
127
  duration: number;
@@ -142,7 +142,10 @@ export class AIGateway {
142
142
  : { cost: costCompletionChat.cost })
143
143
  }
144
144
  : {}),
145
- ...(costCompletionChat.costStatus ? { costStatus: costCompletionChat.costStatus } : {})
145
+ ...(costCompletionChat.costStatus ? { costStatus: costCompletionChat.costStatus } : {}),
146
+ ...(costCompletionChat.costBreakdown
147
+ ? { costBreakdown: costCompletionChat.costBreakdown }
148
+ : {})
146
149
  }
147
150
  };
148
151
  // Track activity success if activity was started
@@ -587,6 +590,7 @@ export class AIGateway {
587
590
  }
588
591
  : {}),
589
592
  ...(costCompletion.costStatus ? { costStatus: costCompletion.costStatus } : {}),
593
+ ...(costCompletion.costBreakdown ? { costBreakdown: costCompletion.costBreakdown } : {}),
590
594
  ...(traceEnabled
591
595
  ? {
592
596
  requestIds: traceRequestIds,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x12i/ai-gateway",
3
- "version": "9.3.0",
3
+ "version": "9.3.4",
4
4
  "description": "AI Gateway - Unified interface for LLM provider routing and management",
5
5
  "type": "module",
6
6
  "exports": {