@x12i/ai-gateway 9.1.6 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -120,6 +120,7 @@ export declare class ActivityManager {
120
120
  */
121
121
  logSuccess(activity: ActivityMetadata | undefined, details: {
122
122
  cost?: number;
123
+ costStatus?: 'priced' | 'unpriced';
123
124
  response: any;
124
125
  endTime: number;
125
126
  duration: number;
@@ -155,6 +155,12 @@ function pickActivixCompletionRoutingMetadata(response) {
155
155
  if (m.effectiveModelConfig != null && typeof m.effectiveModelConfig === 'object') {
156
156
  out.effectiveModelConfig = m.effectiveModelConfig;
157
157
  }
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;
158
164
  return out;
159
165
  }
160
166
  function mergeGatewayActivityIdentity(request, aiRequestId, extras) {
@@ -844,6 +850,7 @@ export class ActivityManager {
844
850
  }
845
851
  await this.activix.completeRecord(activity.activityId, {
846
852
  cost: details.cost,
853
+ ...(details.costStatus ? { costStatus: details.costStatus } : {}),
847
854
  response: details.response,
848
855
  outer: {
849
856
  output: details.response,
@@ -43,6 +43,11 @@ export declare function extractJsonFromFlexMd(content: string, logger?: Logxer):
43
43
  json: any;
44
44
  method: string;
45
45
  } | null>;
46
+ /**
47
+ * Section-based markdown → camelCase field map (e.g. `### Short Answer` → `shortAnswer`).
48
+ * Used when output contracts need structured `parsed` fields but flex-md returned only `rawText`.
49
+ */
50
+ export declare function parseMarkdownSectionsFromContent(content: string, logger?: Logxer): Record<string, unknown>;
46
51
  /**
47
52
  * Check if flex-md module is available
48
53
  */
@@ -503,6 +503,22 @@ function fallbackMarkdownParser(content, logger) {
503
503
  method: 'fallback-raw-text'
504
504
  };
505
505
  }
506
+ /**
507
+ * Section-based markdown → camelCase field map (e.g. `### Short Answer` → `shortAnswer`).
508
+ * Used when output contracts need structured `parsed` fields but flex-md returned only `rawText`.
509
+ */
510
+ export function parseMarkdownSectionsFromContent(content, logger) {
511
+ const parsed = fallbackMarkdownParser(content, logger);
512
+ const json = parsed.json;
513
+ if (json != null && typeof json === 'object' && !Array.isArray(json)) {
514
+ const keys = Object.keys(json);
515
+ if (keys.length === 1 && keys[0] === 'rawText') {
516
+ return {};
517
+ }
518
+ return json;
519
+ }
520
+ return {};
521
+ }
506
522
  /**
507
523
  * Fallback JSON extraction when flex-md is not available
508
524
  */
@@ -43,6 +43,35 @@ export declare function extractTokenUsageFromRouterResponse(routerResponse: unkn
43
43
  * Does not compute cost from tokens — adapters must populate normalized fields or raw usage.cost-style keys.
44
44
  */
45
45
  export declare function extractCostUsdFromRouterResponse(routerResponse: unknown): number | undefined;
46
+ /** Activity billing state when token usage is recorded (Run Analysis G8). */
47
+ export type ActivityCostStatus = 'priced' | 'unpriced';
48
+ export type ResolvedActivityCost = {
49
+ cost?: number;
50
+ costStatus?: ActivityCostStatus;
51
+ };
52
+ export declare function hasNonZeroTokenUsage(tokens: {
53
+ prompt: number;
54
+ completion: number;
55
+ total: number;
56
+ }): boolean;
57
+ /**
58
+ * Gateway fallback when the router does not set `metadata.costStatus`.
59
+ * Prefer {@link resolveCostCompletionForActivity} at invoke boundaries.
60
+ */
61
+ export declare function resolveActivityCostCompletion(tokens: {
62
+ prompt: number;
63
+ completion: number;
64
+ total: number;
65
+ }, costUsd: number | undefined): ResolvedActivityCost;
66
+ /**
67
+ * Activity cost slice for Activix: router `metadata.costStatus` / cost wins when present;
68
+ * otherwise gateway applies the G8 fallback (usage + no price → `unpriced`).
69
+ */
70
+ export declare function resolveCostCompletionForActivity(routerResponse: unknown, tokens: {
71
+ prompt: number;
72
+ completion: number;
73
+ total: number;
74
+ }): ResolvedActivityCost;
46
75
  /**
47
76
  * Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
48
77
  * Matches trace-mode resolution; intended for every successful invoke(), not only diagnostics.trace.
@@ -315,6 +315,50 @@ export function extractCostUsdFromRouterResponse(routerResponse) {
315
315
  }
316
316
  return undefined;
317
317
  }
318
+ export function hasNonZeroTokenUsage(tokens) {
319
+ return !!(tokens.prompt || tokens.completion || tokens.total);
320
+ }
321
+ function pickRouterCostStatus(routerResponse) {
322
+ if (routerResponse == null || typeof routerResponse !== 'object')
323
+ return undefined;
324
+ const r = routerResponse;
325
+ const meta = r.metadata != null && typeof r.metadata === 'object'
326
+ ? r.metadata
327
+ : undefined;
328
+ const status = meta?.costStatus ?? r.costStatus;
329
+ return status === 'priced' || status === 'unpriced' ? status : undefined;
330
+ }
331
+ /**
332
+ * Gateway fallback when the router does not set `metadata.costStatus`.
333
+ * Prefer {@link resolveCostCompletionForActivity} at invoke boundaries.
334
+ */
335
+ export function resolveActivityCostCompletion(tokens, costUsd) {
336
+ if (typeof costUsd === 'number' && Number.isFinite(costUsd)) {
337
+ return { cost: costUsd, costStatus: 'priced' };
338
+ }
339
+ if (hasNonZeroTokenUsage(tokens)) {
340
+ return { costStatus: 'unpriced' };
341
+ }
342
+ return {};
343
+ }
344
+ /**
345
+ * Activity cost slice for Activix: router `metadata.costStatus` / cost wins when present;
346
+ * otherwise gateway applies the G8 fallback (usage + no price → `unpriced`).
347
+ */
348
+ export function resolveCostCompletionForActivity(routerResponse, tokens) {
349
+ const routerStatus = pickRouterCostStatus(routerResponse);
350
+ const costUsd = extractCostUsdFromRouterResponse(routerResponse);
351
+ if (routerStatus === 'priced') {
352
+ return {
353
+ ...(typeof costUsd === 'number' && Number.isFinite(costUsd) ? { cost: costUsd } : {}),
354
+ costStatus: 'priced'
355
+ };
356
+ }
357
+ if (routerStatus === 'unpriced') {
358
+ return { costStatus: 'unpriced' };
359
+ }
360
+ return resolveActivityCostCompletion(tokens, costUsd);
361
+ }
318
362
  /**
319
363
  * Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
320
364
  * Matches trace-mode resolution; intended for every successful invoke(), not only diagnostics.trace.
package/dist/gateway.js CHANGED
@@ -8,7 +8,8 @@ import { ensureGatewayRequestIdentity } from './activity-manager.js';
8
8
  import { initializeGatewayComponents } from './gateway-config.js';
9
9
  import { buildMessages } from './message-builder.js';
10
10
  import { extractJsonFromFlexMd } from './flex-md-loader.js';
11
- import { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, capActivityFullResponsePayload, DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, extractCostUsdFromRouterResponse, extractTokenUsageFromRouterResponse, mergeConfig, pickEffectiveModelConfigForMetadata, pickInvokeRoutingMetadataSlice, pickTraceMergedRouterConfig, tryExtractRouterLikePayloadFromErrorChain } from './gateway-utils.js';
11
+ import { enrichParsedContentForOutputContract, resolveOutputContractFieldKeys } from './output-contract-normalizer.js';
12
+ import { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, capActivityFullResponsePayload, DEFAULT_ACTIVITY_FULL_RESPONSE_MAX_CHARS, extractCostUsdFromRouterResponse, extractTokenUsageFromRouterResponse, mergeConfig, pickEffectiveModelConfigForMetadata, pickInvokeRoutingMetadataSlice, pickTraceMergedRouterConfig, resolveCostCompletionForActivity, tryExtractRouterLikePayloadFromErrorChain } from './gateway-utils.js';
12
13
  import { autoRegisterProviders } from './gateway-provider-auto-register.js';
13
14
  import { setGatewayLastJobId, setGatewayRuntimeClients } from './runtime-objects.js';
14
15
  import { gatewayLogDebug, withActivityIdentity } from './gateway-log-meta.js';
@@ -108,8 +109,9 @@ export class AIGateway {
108
109
  },
109
110
  mode: 'sync'
110
111
  });
111
- const costUsdChat = extractCostUsdFromRouterResponse(response);
112
112
  const metaChat = response?.metadata || {};
113
+ const tokensChat = extractTokenUsageFromRouterResponse(response);
114
+ const costCompletionChat = resolveCostCompletionForActivity(response, tokensChat);
113
115
  // Create enhanced response
114
116
  const enhancedResponse = {
115
117
  content: response.content || '',
@@ -117,22 +119,25 @@ export class AIGateway {
117
119
  aiRequestId: request.aiRequestId,
118
120
  identity: request.identity,
119
121
  latencyMs: Date.now() - startTime,
120
- tokens: extractTokenUsageFromRouterResponse(response),
122
+ tokens: tokensChat,
121
123
  taskTypeId,
122
124
  agentType: 'chat',
123
- ...(typeof costUsdChat === 'number'
125
+ ...(costCompletionChat.costStatus === 'priced'
124
126
  ? {
125
- costUsd: costUsdChat,
126
- ...(typeof metaChat.cost === 'number' ? { cost: metaChat.cost } : { cost: costUsdChat })
127
+ costUsd: costCompletionChat.cost,
128
+ ...(typeof metaChat.cost === 'number'
129
+ ? { cost: metaChat.cost }
130
+ : { cost: costCompletionChat.cost })
127
131
  }
128
- : {})
132
+ : {}),
133
+ ...(costCompletionChat.costStatus ? { costStatus: costCompletionChat.costStatus } : {})
129
134
  }
130
135
  };
131
136
  // Track activity success if activity was started
132
137
  if (activity) {
133
138
  try {
134
139
  await this.activityManager.logSuccess(activity, {
135
- ...(typeof costUsdChat === 'number' ? { cost: costUsdChat } : {}),
140
+ ...costCompletionChat,
136
141
  response: enhancedResponse,
137
142
  endTime: Date.now(),
138
143
  duration: Date.now() - startTime
@@ -523,6 +528,8 @@ export class AIGateway {
523
528
  }
524
529
  contentType = 'structured';
525
530
  parsingMethod = 'flex-md';
531
+ const outputContractKeys = resolveOutputContractFieldKeys(request);
532
+ parsedContent = await enrichParsedContentForOutputContract(parsedContent, content, outputContractKeys, this.logger);
526
533
  let tokens = extractTokenUsageFromRouterResponse(routerResponse);
527
534
  if (!(tokens.prompt || tokens.completion || tokens.total)) {
528
535
  const alt = routerResponse?.rawResponse ?? routerResponse?.raw;
@@ -532,7 +539,7 @@ export class AIGateway {
532
539
  tokens = second;
533
540
  }
534
541
  }
535
- const resolvedCostUsd = extractCostUsdFromRouterResponse(routerResponse);
542
+ const costCompletion = resolveCostCompletionForActivity(routerResponse, tokens);
536
543
  const routerMetaForCost = routerResponse?.metadata || {};
537
544
  const routingMetadataSlice = pickInvokeRoutingMetadataSlice(routerResponse, mergedConfig);
538
545
  const effectiveModelConfig = pickEffectiveModelConfigForMetadata(mergedConfig);
@@ -551,14 +558,15 @@ export class AIGateway {
551
558
  parsingMethod,
552
559
  ...routingMetadataSlice,
553
560
  ...(effectiveModelConfig !== undefined ? { effectiveModelConfig } : {}),
554
- ...(typeof resolvedCostUsd === 'number'
561
+ ...(costCompletion.costStatus === 'priced'
555
562
  ? {
556
- costUsd: resolvedCostUsd,
563
+ costUsd: costCompletion.cost,
557
564
  ...(typeof routerMetaForCost.cost === 'number'
558
565
  ? { cost: routerMetaForCost.cost }
559
- : { cost: resolvedCostUsd })
566
+ : { cost: costCompletion.cost })
560
567
  }
561
568
  : {}),
569
+ ...(costCompletion.costStatus ? { costStatus: costCompletion.costStatus } : {}),
562
570
  ...(traceEnabled
563
571
  ? {
564
572
  requestIds: traceRequestIds,
@@ -597,7 +605,7 @@ export class AIGateway {
597
605
  usage: tokens
598
606
  };
599
607
  await this.activityManager.logSuccess(activity, {
600
- ...(typeof resolvedCostUsd === 'number' ? { cost: resolvedCostUsd } : {}),
608
+ ...costCompletion,
601
609
  response: activityResponse,
602
610
  endTime: Date.now(),
603
611
  duration: Date.now() - startTime
package/dist/index.d.ts CHANGED
@@ -17,7 +17,10 @@ export { AIGateway } from './gateway.js';
17
17
  export { InstructionNotFoundError, InstructionBackendError } from './instruction-errors.js';
18
18
  export { autoRegisterProviders } from './gateway-provider-auto-register.js';
19
19
  export type { GatewayConfig, ProviderModelRef, ModelConfig, RetryConfig, ChatRequest, AIInvokeRequest, AIRequest, GatewayActionType, GatewayInvokeRejectionMetadata, GatewayTraceRequestIds, GatewayTraceMergedConfig, EnhancedLLMResponse, InstructionMetadata, ValidationRule, TemplateRenderOptions, SmartInputConfig, SmartInputRenderOptions } from './types.js';
20
- export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike } from './gateway-utils.js';
20
+ export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, hasNonZeroTokenUsage } from './gateway-utils.js';
21
+ export type { ActivityCostStatus, ResolvedActivityCost } from './gateway-utils.js';
22
+ export { contractSpecToFieldKeys, enrichParsedContentForOutputContract, resolveOutputContractFieldKeys } from './output-contract-normalizer.js';
23
+ export type { OutputContractSpec } from './output-contract-normalizer.js';
21
24
  export { mergeGatewayAndRequestTemplateRenderOptions, mergeTemplateRenderOptions } from './template-render-merge.js';
22
25
  export type { GatewayTemplateRenderRequestSlice } from './template-render-merge.js';
23
26
  export { GATEWAY_DUAL_MEMORY_ROOTS, buildMemoryResolutionRootFromWorkingMemory, coalesceMergedInputBucket, extractCallerInputsBag, mapSmartInputPathsInputsToInput, parseLooseJsonObject, prepareWorkingMemoryForTemplateRender, resolveGatewayMemoryPathValue } from './memory-path-resolution.js';
package/dist/index.js CHANGED
@@ -17,7 +17,8 @@ export * from '@x12i/ai-providers-router';
17
17
  export { AIGateway } from './gateway.js';
18
18
  export { InstructionNotFoundError, InstructionBackendError } from './instruction-errors.js';
19
19
  export { autoRegisterProviders } from './gateway-provider-auto-register.js';
20
- export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike } from './gateway-utils.js';
20
+ export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, hasNonZeroTokenUsage } from './gateway-utils.js';
21
+ export { contractSpecToFieldKeys, enrichParsedContentForOutputContract, resolveOutputContractFieldKeys } from './output-contract-normalizer.js';
21
22
  export { mergeGatewayAndRequestTemplateRenderOptions, mergeTemplateRenderOptions } from './template-render-merge.js';
22
23
  export { GATEWAY_DUAL_MEMORY_ROOTS, buildMemoryResolutionRootFromWorkingMemory, coalesceMergedInputBucket, extractCallerInputsBag, mapSmartInputPathsInputsToInput, parseLooseJsonObject, prepareWorkingMemoryForTemplateRender, resolveGatewayMemoryPathValue } from './memory-path-resolution.js';
23
24
  // Usage tracking: UsageTracker class methods are available but consumption calculation is disabled
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Normalizes invoke responses into `output.parsed` when an explicit output contract is forwarded.
3
+ * Does not infer contracts from other request fields — that is upstream (graph-engine / ai-tasks).
4
+ */
5
+ import type { Logxer } from '@x12i/logxer';
6
+ /** Explicit contract: field names or JSON-schema-style `properties` only. */
7
+ export type OutputContractSpec = string[] | {
8
+ properties: Record<string, unknown>;
9
+ };
10
+ /**
11
+ * Maps an explicit contract spec to field keys. No inference from descriptions or other request fields.
12
+ */
13
+ export declare function contractSpecToFieldKeys(contract: unknown): string[];
14
+ /**
15
+ * Resolves field keys only from explicit `outputContract` on the request or graph-forwarded inputs.
16
+ */
17
+ export declare function resolveOutputContractFieldKeys(request: unknown): string[] | undefined;
18
+ /**
19
+ * Fills missing contract keys from markdown sections after flex-md. Only runs when explicit contract keys were supplied.
20
+ */
21
+ export declare function enrichParsedContentForOutputContract(parsed: unknown, rawContent: string, contractKeys: string[] | undefined, logger?: Logxer): Promise<Record<string, unknown>>;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Normalizes invoke responses into `output.parsed` when an explicit output contract is forwarded.
3
+ * Does not infer contracts from other request fields — that is upstream (graph-engine / ai-tasks).
4
+ */
5
+ import { parseMarkdownSectionsFromContent } from './flex-md-loader.js';
6
+ import { coalesceMergedInputBucket, extractCallerInputsBag, parseLooseJsonObject } from './memory-path-resolution.js';
7
+ function isPlainObject(value) {
8
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
9
+ }
10
+ function hasMeaningfulContractValue(value) {
11
+ if (value === undefined || value === null)
12
+ return false;
13
+ if (typeof value === 'string')
14
+ return value.trim().length > 0;
15
+ if (Array.isArray(value))
16
+ return value.length > 0;
17
+ if (typeof value === 'object')
18
+ return Object.keys(value).length > 0;
19
+ return true;
20
+ }
21
+ /**
22
+ * Maps an explicit contract spec to field keys. No inference from descriptions or other request fields.
23
+ */
24
+ export function contractSpecToFieldKeys(contract) {
25
+ if (contract == null)
26
+ return [];
27
+ if (Array.isArray(contract)) {
28
+ return contract.filter((k) => typeof k === 'string' && k.trim().length > 0);
29
+ }
30
+ if (!isPlainObject(contract))
31
+ return [];
32
+ const properties = contract.properties;
33
+ if (isPlainObject(properties)) {
34
+ return Object.keys(properties);
35
+ }
36
+ return [];
37
+ }
38
+ /** Graph-engine path: `workingMemory.inputs.outputContract` or merged `input.outputContract`. */
39
+ function readExplicitOutputContractFromWorkingMemory(workingMemory) {
40
+ if (!isPlainObject(workingMemory))
41
+ return undefined;
42
+ const inputs = extractCallerInputsBag(workingMemory);
43
+ if (inputs?.outputContract !== undefined)
44
+ return inputs.outputContract;
45
+ const input = coalesceMergedInputBucket(workingMemory);
46
+ if (isPlainObject(input) && input.outputContract !== undefined) {
47
+ return input.outputContract;
48
+ }
49
+ const loose = parseLooseJsonObject(workingMemory.input);
50
+ if (loose?.outputContract !== undefined)
51
+ return loose.outputContract;
52
+ return undefined;
53
+ }
54
+ /**
55
+ * Resolves field keys only from explicit `outputContract` on the request or graph-forwarded inputs.
56
+ */
57
+ export function resolveOutputContractFieldKeys(request) {
58
+ if (request == null || typeof request !== 'object')
59
+ return undefined;
60
+ const r = request;
61
+ const candidates = [
62
+ r.outputContract,
63
+ readExplicitOutputContractFromWorkingMemory(r.workingMemory)
64
+ ];
65
+ for (const candidate of candidates) {
66
+ const keys = contractSpecToFieldKeys(candidate);
67
+ if (keys.length > 0)
68
+ return keys;
69
+ }
70
+ return undefined;
71
+ }
72
+ function asParsedRecord(parsed) {
73
+ if (!isPlainObject(parsed))
74
+ return {};
75
+ return { ...parsed };
76
+ }
77
+ function pickAliasValue(source, key) {
78
+ if (hasMeaningfulContractValue(source[key]))
79
+ return source[key];
80
+ const spaced = key.replace(/([A-Z])/g, ' $1').replace(/^./, (c) => c.toUpperCase());
81
+ const title = spaced.charAt(0).toUpperCase() + spaced.slice(1);
82
+ const aliases = [
83
+ key,
84
+ key.toLowerCase(),
85
+ title,
86
+ title.replace(/\s+/g, ' '),
87
+ key.replace(/([A-Z])/g, '_$1').toLowerCase()
88
+ ];
89
+ for (const alias of aliases) {
90
+ if (hasMeaningfulContractValue(source[alias]))
91
+ return source[alias];
92
+ }
93
+ return undefined;
94
+ }
95
+ /**
96
+ * Fills missing contract keys from markdown sections after flex-md. Only runs when explicit contract keys were supplied.
97
+ */
98
+ export async function enrichParsedContentForOutputContract(parsed, rawContent, contractKeys, logger) {
99
+ const base = asParsedRecord(parsed);
100
+ if (!contractKeys?.length)
101
+ return base;
102
+ const missing = contractKeys.filter((k) => !hasMeaningfulContractValue(base[k]));
103
+ if (missing.length === 0)
104
+ return base;
105
+ const content = typeof rawContent === 'string' && rawContent.trim().length > 0
106
+ ? rawContent
107
+ : typeof base.rawText === 'string'
108
+ ? base.rawText
109
+ : '';
110
+ if (!content.trim())
111
+ return base;
112
+ const fromMarkdown = parseMarkdownSectionsFromContent(content, logger);
113
+ const merged = { ...base };
114
+ for (const key of missing) {
115
+ const value = pickAliasValue(fromMarkdown, key);
116
+ if (hasMeaningfulContractValue(value)) {
117
+ merged[key] = value;
118
+ }
119
+ }
120
+ return merged;
121
+ }
package/dist/types.d.ts CHANGED
@@ -701,6 +701,14 @@ interface BaseLLMRequest extends Omit<LLMRequest, 'messages' | 'input' | 'reques
701
701
  * attach heavy diagnostic objects or raw provider payloads.
702
702
  */
703
703
  diagnostics?: DiagnosticsOptions;
704
+ /**
705
+ * Explicit output contract from graph-engine / ai-tasks: field names or `{ properties }`.
706
+ * Also accepted on `workingMemory.inputs.outputContract`. When absent, the gateway does not
707
+ * infer contract fields from other request shapes (`expectedSchema`, config, etc.).
708
+ */
709
+ outputContract?: string[] | {
710
+ properties: Record<string, unknown>;
711
+ };
704
712
  }
705
713
  /**
706
714
  * Chat request for conversational use cases
@@ -932,6 +940,12 @@ export interface EnhancedLLMResponse<TContent = unknown> extends Omit<AIResponse
932
940
  * Cost in USD (if available)
933
941
  */
934
942
  cost?: number;
943
+ /**
944
+ * Billing state for Run Analysis / Activix when usage is recorded.
945
+ * - `priced`: {@link cost} / {@link costUsd} is a finite number from the router
946
+ * - `unpriced`: usage exists but no price table / adapter cost was returned
947
+ */
948
+ costStatus?: 'priced' | 'unpriced';
935
949
  /**
936
950
  * Cost in USD (preferred stable key when the router exposes it).
937
951
  * When both are present, costUsd should mirror cost.
@@ -159,6 +159,12 @@ function pickActivixCompletionRoutingMetadata(response) {
159
159
  if (m.effectiveModelConfig != null && typeof m.effectiveModelConfig === 'object') {
160
160
  out.effectiveModelConfig = m.effectiveModelConfig;
161
161
  }
162
+ if (typeof m.cost === 'number' && Number.isFinite(m.cost))
163
+ out.cost = m.cost;
164
+ if (typeof m.costUsd === 'number' && Number.isFinite(m.costUsd))
165
+ out.costUsd = m.costUsd;
166
+ if (m.costStatus === 'priced' || m.costStatus === 'unpriced')
167
+ out.costStatus = m.costStatus;
162
168
  return out;
163
169
  }
164
170
  function mergeGatewayActivityIdentity(request, aiRequestId, extras) {
@@ -848,6 +854,7 @@ class ActivityManager {
848
854
  }
849
855
  await this.activix.completeRecord(activity.activityId, {
850
856
  cost: details.cost,
857
+ ...(details.costStatus ? { costStatus: details.costStatus } : {}),
851
858
  response: details.response,
852
859
  outer: {
853
860
  output: details.response,
@@ -120,6 +120,7 @@ export declare class ActivityManager {
120
120
  */
121
121
  logSuccess(activity: ActivityMetadata | undefined, details: {
122
122
  cost?: number;
123
+ costStatus?: 'priced' | 'unpriced';
123
124
  response: any;
124
125
  endTime: number;
125
126
  duration: number;
@@ -50,6 +50,7 @@ var __importStar = (this && this.__importStar) || (function () {
50
50
  Object.defineProperty(exports, "__esModule", { value: true });
51
51
  exports.loadFlexMd = loadFlexMd;
52
52
  exports.extractJsonFromFlexMd = extractJsonFromFlexMd;
53
+ exports.parseMarkdownSectionsFromContent = parseMarkdownSectionsFromContent;
53
54
  exports.isFlexMdAvailable = isFlexMdAvailable;
54
55
  exports.getModelMaxTokensFromFlexMd = getModelMaxTokensFromFlexMd;
55
56
  exports.getFlexMdModule = getFlexMdModule;
@@ -549,6 +550,22 @@ function fallbackMarkdownParser(content, logger) {
549
550
  method: 'fallback-raw-text'
550
551
  };
551
552
  }
553
+ /**
554
+ * Section-based markdown → camelCase field map (e.g. `### Short Answer` → `shortAnswer`).
555
+ * Used when output contracts need structured `parsed` fields but flex-md returned only `rawText`.
556
+ */
557
+ function parseMarkdownSectionsFromContent(content, logger) {
558
+ const parsed = fallbackMarkdownParser(content, logger);
559
+ const json = parsed.json;
560
+ if (json != null && typeof json === 'object' && !Array.isArray(json)) {
561
+ const keys = Object.keys(json);
562
+ if (keys.length === 1 && keys[0] === 'rawText') {
563
+ return {};
564
+ }
565
+ return json;
566
+ }
567
+ return {};
568
+ }
552
569
  /**
553
570
  * Fallback JSON extraction when flex-md is not available
554
571
  */
@@ -43,6 +43,11 @@ export declare function extractJsonFromFlexMd(content: string, logger?: Logxer):
43
43
  json: any;
44
44
  method: string;
45
45
  } | null>;
46
+ /**
47
+ * Section-based markdown → camelCase field map (e.g. `### Short Answer` → `shortAnswer`).
48
+ * Used when output contracts need structured `parsed` fields but flex-md returned only `rawText`.
49
+ */
50
+ export declare function parseMarkdownSectionsFromContent(content: string, logger?: Logxer): Record<string, unknown>;
46
51
  /**
47
52
  * Check if flex-md module is available
48
53
  */
@@ -44,6 +44,9 @@ exports.mergeConfig = mergeConfig;
44
44
  exports.normalizeRouterUsageTokens = normalizeRouterUsageTokens;
45
45
  exports.extractTokenUsageFromRouterResponse = extractTokenUsageFromRouterResponse;
46
46
  exports.extractCostUsdFromRouterResponse = extractCostUsdFromRouterResponse;
47
+ exports.hasNonZeroTokenUsage = hasNonZeroTokenUsage;
48
+ exports.resolveActivityCostCompletion = resolveActivityCostCompletion;
49
+ exports.resolveCostCompletionForActivity = resolveCostCompletionForActivity;
47
50
  exports.pickInvokeRoutingMetadataSlice = pickInvokeRoutingMetadataSlice;
48
51
  exports.pickEffectiveModelConfigForMetadata = pickEffectiveModelConfigForMetadata;
49
52
  exports.pickTraceMergedRouterConfig = pickTraceMergedRouterConfig;
@@ -366,6 +369,50 @@ function extractCostUsdFromRouterResponse(routerResponse) {
366
369
  }
367
370
  return undefined;
368
371
  }
372
+ function hasNonZeroTokenUsage(tokens) {
373
+ return !!(tokens.prompt || tokens.completion || tokens.total);
374
+ }
375
+ function pickRouterCostStatus(routerResponse) {
376
+ if (routerResponse == null || typeof routerResponse !== 'object')
377
+ return undefined;
378
+ const r = routerResponse;
379
+ const meta = r.metadata != null && typeof r.metadata === 'object'
380
+ ? r.metadata
381
+ : undefined;
382
+ const status = meta?.costStatus ?? r.costStatus;
383
+ return status === 'priced' || status === 'unpriced' ? status : undefined;
384
+ }
385
+ /**
386
+ * Gateway fallback when the router does not set `metadata.costStatus`.
387
+ * Prefer {@link resolveCostCompletionForActivity} at invoke boundaries.
388
+ */
389
+ function resolveActivityCostCompletion(tokens, costUsd) {
390
+ if (typeof costUsd === 'number' && Number.isFinite(costUsd)) {
391
+ return { cost: costUsd, costStatus: 'priced' };
392
+ }
393
+ if (hasNonZeroTokenUsage(tokens)) {
394
+ return { costStatus: 'unpriced' };
395
+ }
396
+ return {};
397
+ }
398
+ /**
399
+ * Activity cost slice for Activix: router `metadata.costStatus` / cost wins when present;
400
+ * otherwise gateway applies the G8 fallback (usage + no price → `unpriced`).
401
+ */
402
+ function resolveCostCompletionForActivity(routerResponse, tokens) {
403
+ const routerStatus = pickRouterCostStatus(routerResponse);
404
+ const costUsd = extractCostUsdFromRouterResponse(routerResponse);
405
+ if (routerStatus === 'priced') {
406
+ return {
407
+ ...(typeof costUsd === 'number' && Number.isFinite(costUsd) ? { cost: costUsd } : {}),
408
+ costStatus: 'priced'
409
+ };
410
+ }
411
+ if (routerStatus === 'unpriced') {
412
+ return { costStatus: 'unpriced' };
413
+ }
414
+ return resolveActivityCostCompletion(tokens, costUsd);
415
+ }
369
416
  /**
370
417
  * Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
371
418
  * Matches trace-mode resolution; intended for every successful invoke(), not only diagnostics.trace.
@@ -43,6 +43,35 @@ export declare function extractTokenUsageFromRouterResponse(routerResponse: unkn
43
43
  * Does not compute cost from tokens — adapters must populate normalized fields or raw usage.cost-style keys.
44
44
  */
45
45
  export declare function extractCostUsdFromRouterResponse(routerResponse: unknown): number | undefined;
46
+ /** Activity billing state when token usage is recorded (Run Analysis G8). */
47
+ export type ActivityCostStatus = 'priced' | 'unpriced';
48
+ export type ResolvedActivityCost = {
49
+ cost?: number;
50
+ costStatus?: ActivityCostStatus;
51
+ };
52
+ export declare function hasNonZeroTokenUsage(tokens: {
53
+ prompt: number;
54
+ completion: number;
55
+ total: number;
56
+ }): boolean;
57
+ /**
58
+ * Gateway fallback when the router does not set `metadata.costStatus`.
59
+ * Prefer {@link resolveCostCompletionForActivity} at invoke boundaries.
60
+ */
61
+ export declare function resolveActivityCostCompletion(tokens: {
62
+ prompt: number;
63
+ completion: number;
64
+ total: number;
65
+ }, costUsd: number | undefined): ResolvedActivityCost;
66
+ /**
67
+ * Activity cost slice for Activix: router `metadata.costStatus` / cost wins when present;
68
+ * otherwise gateway applies the G8 fallback (usage + no price → `unpriced`).
69
+ */
70
+ export declare function resolveCostCompletionForActivity(routerResponse: unknown, tokens: {
71
+ prompt: number;
72
+ completion: number;
73
+ total: number;
74
+ }): ResolvedActivityCost;
46
75
  /**
47
76
  * Stable routing facts for gateway response metadata (router metadata + merged config fallbacks).
48
77
  * Matches trace-mode resolution; intended for every successful invoke(), not only diagnostics.trace.
@@ -11,6 +11,7 @@ const activity_manager_js_1 = require("./activity-manager.cjs");
11
11
  const gateway_config_js_1 = require("./gateway-config.cjs");
12
12
  const message_builder_js_1 = require("./message-builder.cjs");
13
13
  const flex_md_loader_js_1 = require("./flex-md-loader.cjs");
14
+ const output_contract_normalizer_js_1 = require("./output-contract-normalizer.cjs");
14
15
  const gateway_utils_js_1 = require("./gateway-utils.cjs");
15
16
  const gateway_provider_auto_register_js_1 = require("./gateway-provider-auto-register.cjs");
16
17
  const runtime_objects_js_1 = require("./runtime-objects.cjs");
@@ -111,8 +112,9 @@ class AIGateway {
111
112
  },
112
113
  mode: 'sync'
113
114
  });
114
- const costUsdChat = (0, gateway_utils_js_1.extractCostUsdFromRouterResponse)(response);
115
115
  const metaChat = response?.metadata || {};
116
+ const tokensChat = (0, gateway_utils_js_1.extractTokenUsageFromRouterResponse)(response);
117
+ const costCompletionChat = (0, gateway_utils_js_1.resolveCostCompletionForActivity)(response, tokensChat);
116
118
  // Create enhanced response
117
119
  const enhancedResponse = {
118
120
  content: response.content || '',
@@ -120,22 +122,25 @@ class AIGateway {
120
122
  aiRequestId: request.aiRequestId,
121
123
  identity: request.identity,
122
124
  latencyMs: Date.now() - startTime,
123
- tokens: (0, gateway_utils_js_1.extractTokenUsageFromRouterResponse)(response),
125
+ tokens: tokensChat,
124
126
  taskTypeId,
125
127
  agentType: 'chat',
126
- ...(typeof costUsdChat === 'number'
128
+ ...(costCompletionChat.costStatus === 'priced'
127
129
  ? {
128
- costUsd: costUsdChat,
129
- ...(typeof metaChat.cost === 'number' ? { cost: metaChat.cost } : { cost: costUsdChat })
130
+ costUsd: costCompletionChat.cost,
131
+ ...(typeof metaChat.cost === 'number'
132
+ ? { cost: metaChat.cost }
133
+ : { cost: costCompletionChat.cost })
130
134
  }
131
- : {})
135
+ : {}),
136
+ ...(costCompletionChat.costStatus ? { costStatus: costCompletionChat.costStatus } : {})
132
137
  }
133
138
  };
134
139
  // Track activity success if activity was started
135
140
  if (activity) {
136
141
  try {
137
142
  await this.activityManager.logSuccess(activity, {
138
- ...(typeof costUsdChat === 'number' ? { cost: costUsdChat } : {}),
143
+ ...costCompletionChat,
139
144
  response: enhancedResponse,
140
145
  endTime: Date.now(),
141
146
  duration: Date.now() - startTime
@@ -526,6 +531,8 @@ class AIGateway {
526
531
  }
527
532
  contentType = 'structured';
528
533
  parsingMethod = 'flex-md';
534
+ const outputContractKeys = (0, output_contract_normalizer_js_1.resolveOutputContractFieldKeys)(request);
535
+ parsedContent = await (0, output_contract_normalizer_js_1.enrichParsedContentForOutputContract)(parsedContent, content, outputContractKeys, this.logger);
529
536
  let tokens = (0, gateway_utils_js_1.extractTokenUsageFromRouterResponse)(routerResponse);
530
537
  if (!(tokens.prompt || tokens.completion || tokens.total)) {
531
538
  const alt = routerResponse?.rawResponse ?? routerResponse?.raw;
@@ -535,7 +542,7 @@ class AIGateway {
535
542
  tokens = second;
536
543
  }
537
544
  }
538
- const resolvedCostUsd = (0, gateway_utils_js_1.extractCostUsdFromRouterResponse)(routerResponse);
545
+ const costCompletion = (0, gateway_utils_js_1.resolveCostCompletionForActivity)(routerResponse, tokens);
539
546
  const routerMetaForCost = routerResponse?.metadata || {};
540
547
  const routingMetadataSlice = (0, gateway_utils_js_1.pickInvokeRoutingMetadataSlice)(routerResponse, mergedConfig);
541
548
  const effectiveModelConfig = (0, gateway_utils_js_1.pickEffectiveModelConfigForMetadata)(mergedConfig);
@@ -554,14 +561,15 @@ class AIGateway {
554
561
  parsingMethod,
555
562
  ...routingMetadataSlice,
556
563
  ...(effectiveModelConfig !== undefined ? { effectiveModelConfig } : {}),
557
- ...(typeof resolvedCostUsd === 'number'
564
+ ...(costCompletion.costStatus === 'priced'
558
565
  ? {
559
- costUsd: resolvedCostUsd,
566
+ costUsd: costCompletion.cost,
560
567
  ...(typeof routerMetaForCost.cost === 'number'
561
568
  ? { cost: routerMetaForCost.cost }
562
- : { cost: resolvedCostUsd })
569
+ : { cost: costCompletion.cost })
563
570
  }
564
571
  : {}),
572
+ ...(costCompletion.costStatus ? { costStatus: costCompletion.costStatus } : {}),
565
573
  ...(traceEnabled
566
574
  ? {
567
575
  requestIds: traceRequestIds,
@@ -600,7 +608,7 @@ class AIGateway {
600
608
  usage: tokens
601
609
  };
602
610
  await this.activityManager.logSuccess(activity, {
603
- ...(typeof resolvedCostUsd === 'number' ? { cost: resolvedCostUsd } : {}),
611
+ ...costCompletion,
604
612
  response: activityResponse,
605
613
  endTime: Date.now(),
606
614
  duration: Date.now() - startTime
@@ -21,8 +21,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
21
21
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
22
22
  };
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.getObjectTypesForAgent = exports.getObjectType = exports.OBJECT_TYPES_LIBRARY = exports.assertValidAIRequest = exports.formatDiagnostic = exports.runValidationTests = exports.createValidationTestCases = exports.createTestAIRequest = exports.supportsJSONMode = exports.diagnoseResponse = exports.diagnoseRequest = exports.validateResponse = exports.extractJSON = exports.validateJSON = exports.validateAIRequest = exports.DEFAULT_RATE_LIMIT_ENABLED = exports.DEFAULT_RATE_LIMIT_MIN_INTERVAL_MS = exports.GatewayRateLimiter = exports.runtimeObjects = exports.DebugLogAbstract = exports.createLogxer = exports.gatewayLogDebug = exports.withActivityIdentity = exports.activityIdentityToLogMeta = exports.ensureGatewayRequestIdentity = exports.ActivityManager = exports.Activix = exports.resolveGatewayMemoryPathValue = exports.prepareWorkingMemoryForTemplateRender = exports.parseLooseJsonObject = exports.mapSmartInputPathsInputsToInput = exports.extractCallerInputsBag = exports.coalesceMergedInputBucket = exports.buildMemoryResolutionRootFromWorkingMemory = exports.GATEWAY_DUAL_MEMORY_ROOTS = exports.mergeTemplateRenderOptions = exports.mergeGatewayAndRequestTemplateRenderOptions = exports.pickRequestIdsFromRouterLike = exports.tryExtractRouterLikePayloadFromErrorChain = exports.buildInvokeRejectionMetadata = exports.attachGatewayInvokeRejectionMetadata = exports.autoRegisterProviders = exports.InstructionBackendError = exports.InstructionNotFoundError = exports.AIGateway = exports.FallbackExhaustedError = exports.ProviderNotFoundError = exports.createRouterFromConfig = exports.createRouter = exports.LLMProviderRouter = void 0;
25
- exports.resetObjectTypesLibrary = exports.getObjectTypesLibrary = exports.initializeObjectTypesLibrary = void 0;
24
+ exports.createValidationTestCases = exports.createTestAIRequest = exports.supportsJSONMode = exports.diagnoseResponse = exports.diagnoseRequest = exports.validateResponse = exports.extractJSON = exports.validateJSON = exports.validateAIRequest = exports.DEFAULT_RATE_LIMIT_ENABLED = exports.DEFAULT_RATE_LIMIT_MIN_INTERVAL_MS = exports.GatewayRateLimiter = exports.runtimeObjects = exports.DebugLogAbstract = exports.createLogxer = exports.gatewayLogDebug = exports.withActivityIdentity = exports.activityIdentityToLogMeta = exports.ensureGatewayRequestIdentity = exports.ActivityManager = exports.Activix = exports.resolveGatewayMemoryPathValue = exports.prepareWorkingMemoryForTemplateRender = exports.parseLooseJsonObject = exports.mapSmartInputPathsInputsToInput = exports.extractCallerInputsBag = exports.coalesceMergedInputBucket = exports.buildMemoryResolutionRootFromWorkingMemory = exports.GATEWAY_DUAL_MEMORY_ROOTS = exports.mergeTemplateRenderOptions = exports.mergeGatewayAndRequestTemplateRenderOptions = exports.resolveOutputContractFieldKeys = exports.enrichParsedContentForOutputContract = exports.contractSpecToFieldKeys = exports.hasNonZeroTokenUsage = exports.resolveCostCompletionForActivity = exports.resolveActivityCostCompletion = exports.pickRequestIdsFromRouterLike = exports.tryExtractRouterLikePayloadFromErrorChain = exports.buildInvokeRejectionMetadata = exports.attachGatewayInvokeRejectionMetadata = exports.autoRegisterProviders = exports.InstructionBackendError = exports.InstructionNotFoundError = exports.AIGateway = exports.FallbackExhaustedError = exports.ProviderNotFoundError = exports.createRouterFromConfig = exports.createRouter = exports.LLMProviderRouter = void 0;
25
+ exports.resetObjectTypesLibrary = exports.getObjectTypesLibrary = exports.initializeObjectTypesLibrary = exports.getObjectTypesForAgent = exports.getObjectType = exports.OBJECT_TYPES_LIBRARY = exports.assertValidAIRequest = exports.formatDiagnostic = exports.runValidationTests = void 0;
26
26
  // Re-export router class and types (base functionality)
27
27
  var ai_providers_router_1 = require("@x12i/ai-providers-router");
28
28
  Object.defineProperty(exports, "LLMProviderRouter", { enumerable: true, get: function () { return ai_providers_router_1.LLMProviderRouter; } });
@@ -49,6 +49,13 @@ Object.defineProperty(exports, "attachGatewayInvokeRejectionMetadata", { enumera
49
49
  Object.defineProperty(exports, "buildInvokeRejectionMetadata", { enumerable: true, get: function () { return gateway_utils_js_1.buildInvokeRejectionMetadata; } });
50
50
  Object.defineProperty(exports, "tryExtractRouterLikePayloadFromErrorChain", { enumerable: true, get: function () { return gateway_utils_js_1.tryExtractRouterLikePayloadFromErrorChain; } });
51
51
  Object.defineProperty(exports, "pickRequestIdsFromRouterLike", { enumerable: true, get: function () { return gateway_utils_js_1.pickRequestIdsFromRouterLike; } });
52
+ Object.defineProperty(exports, "resolveActivityCostCompletion", { enumerable: true, get: function () { return gateway_utils_js_1.resolveActivityCostCompletion; } });
53
+ Object.defineProperty(exports, "resolveCostCompletionForActivity", { enumerable: true, get: function () { return gateway_utils_js_1.resolveCostCompletionForActivity; } });
54
+ Object.defineProperty(exports, "hasNonZeroTokenUsage", { enumerable: true, get: function () { return gateway_utils_js_1.hasNonZeroTokenUsage; } });
55
+ var output_contract_normalizer_js_1 = require("./output-contract-normalizer.cjs");
56
+ Object.defineProperty(exports, "contractSpecToFieldKeys", { enumerable: true, get: function () { return output_contract_normalizer_js_1.contractSpecToFieldKeys; } });
57
+ Object.defineProperty(exports, "enrichParsedContentForOutputContract", { enumerable: true, get: function () { return output_contract_normalizer_js_1.enrichParsedContentForOutputContract; } });
58
+ Object.defineProperty(exports, "resolveOutputContractFieldKeys", { enumerable: true, get: function () { return output_contract_normalizer_js_1.resolveOutputContractFieldKeys; } });
52
59
  var template_render_merge_js_1 = require("./template-render-merge.cjs");
53
60
  Object.defineProperty(exports, "mergeGatewayAndRequestTemplateRenderOptions", { enumerable: true, get: function () { return template_render_merge_js_1.mergeGatewayAndRequestTemplateRenderOptions; } });
54
61
  Object.defineProperty(exports, "mergeTemplateRenderOptions", { enumerable: true, get: function () { return template_render_merge_js_1.mergeTemplateRenderOptions; } });
@@ -17,7 +17,10 @@ export { AIGateway } from './gateway.js';
17
17
  export { InstructionNotFoundError, InstructionBackendError } from './instruction-errors.js';
18
18
  export { autoRegisterProviders } from './gateway-provider-auto-register.js';
19
19
  export type { GatewayConfig, ProviderModelRef, ModelConfig, RetryConfig, ChatRequest, AIInvokeRequest, AIRequest, GatewayActionType, GatewayInvokeRejectionMetadata, GatewayTraceRequestIds, GatewayTraceMergedConfig, EnhancedLLMResponse, InstructionMetadata, ValidationRule, TemplateRenderOptions, SmartInputConfig, SmartInputRenderOptions } from './types.js';
20
- export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike } from './gateway-utils.js';
20
+ export { attachGatewayInvokeRejectionMetadata, buildInvokeRejectionMetadata, tryExtractRouterLikePayloadFromErrorChain, pickRequestIdsFromRouterLike, resolveActivityCostCompletion, resolveCostCompletionForActivity, hasNonZeroTokenUsage } from './gateway-utils.js';
21
+ export type { ActivityCostStatus, ResolvedActivityCost } from './gateway-utils.js';
22
+ export { contractSpecToFieldKeys, enrichParsedContentForOutputContract, resolveOutputContractFieldKeys } from './output-contract-normalizer.js';
23
+ export type { OutputContractSpec } from './output-contract-normalizer.js';
21
24
  export { mergeGatewayAndRequestTemplateRenderOptions, mergeTemplateRenderOptions } from './template-render-merge.js';
22
25
  export type { GatewayTemplateRenderRequestSlice } from './template-render-merge.js';
23
26
  export { GATEWAY_DUAL_MEMORY_ROOTS, buildMemoryResolutionRootFromWorkingMemory, coalesceMergedInputBucket, extractCallerInputsBag, mapSmartInputPathsInputsToInput, parseLooseJsonObject, prepareWorkingMemoryForTemplateRender, resolveGatewayMemoryPathValue } from './memory-path-resolution.js';
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ /**
3
+ * Normalizes invoke responses into `output.parsed` when an explicit output contract is forwarded.
4
+ * Does not infer contracts from other request fields — that is upstream (graph-engine / ai-tasks).
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.contractSpecToFieldKeys = contractSpecToFieldKeys;
8
+ exports.resolveOutputContractFieldKeys = resolveOutputContractFieldKeys;
9
+ exports.enrichParsedContentForOutputContract = enrichParsedContentForOutputContract;
10
+ const flex_md_loader_js_1 = require("./flex-md-loader.cjs");
11
+ const memory_path_resolution_js_1 = require("./memory-path-resolution.cjs");
12
+ function isPlainObject(value) {
13
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
14
+ }
15
+ function hasMeaningfulContractValue(value) {
16
+ if (value === undefined || value === null)
17
+ return false;
18
+ if (typeof value === 'string')
19
+ return value.trim().length > 0;
20
+ if (Array.isArray(value))
21
+ return value.length > 0;
22
+ if (typeof value === 'object')
23
+ return Object.keys(value).length > 0;
24
+ return true;
25
+ }
26
+ /**
27
+ * Maps an explicit contract spec to field keys. No inference from descriptions or other request fields.
28
+ */
29
+ function contractSpecToFieldKeys(contract) {
30
+ if (contract == null)
31
+ return [];
32
+ if (Array.isArray(contract)) {
33
+ return contract.filter((k) => typeof k === 'string' && k.trim().length > 0);
34
+ }
35
+ if (!isPlainObject(contract))
36
+ return [];
37
+ const properties = contract.properties;
38
+ if (isPlainObject(properties)) {
39
+ return Object.keys(properties);
40
+ }
41
+ return [];
42
+ }
43
+ /** Graph-engine path: `workingMemory.inputs.outputContract` or merged `input.outputContract`. */
44
+ function readExplicitOutputContractFromWorkingMemory(workingMemory) {
45
+ if (!isPlainObject(workingMemory))
46
+ return undefined;
47
+ const inputs = (0, memory_path_resolution_js_1.extractCallerInputsBag)(workingMemory);
48
+ if (inputs?.outputContract !== undefined)
49
+ return inputs.outputContract;
50
+ const input = (0, memory_path_resolution_js_1.coalesceMergedInputBucket)(workingMemory);
51
+ if (isPlainObject(input) && input.outputContract !== undefined) {
52
+ return input.outputContract;
53
+ }
54
+ const loose = (0, memory_path_resolution_js_1.parseLooseJsonObject)(workingMemory.input);
55
+ if (loose?.outputContract !== undefined)
56
+ return loose.outputContract;
57
+ return undefined;
58
+ }
59
+ /**
60
+ * Resolves field keys only from explicit `outputContract` on the request or graph-forwarded inputs.
61
+ */
62
+ function resolveOutputContractFieldKeys(request) {
63
+ if (request == null || typeof request !== 'object')
64
+ return undefined;
65
+ const r = request;
66
+ const candidates = [
67
+ r.outputContract,
68
+ readExplicitOutputContractFromWorkingMemory(r.workingMemory)
69
+ ];
70
+ for (const candidate of candidates) {
71
+ const keys = contractSpecToFieldKeys(candidate);
72
+ if (keys.length > 0)
73
+ return keys;
74
+ }
75
+ return undefined;
76
+ }
77
+ function asParsedRecord(parsed) {
78
+ if (!isPlainObject(parsed))
79
+ return {};
80
+ return { ...parsed };
81
+ }
82
+ function pickAliasValue(source, key) {
83
+ if (hasMeaningfulContractValue(source[key]))
84
+ return source[key];
85
+ const spaced = key.replace(/([A-Z])/g, ' $1').replace(/^./, (c) => c.toUpperCase());
86
+ const title = spaced.charAt(0).toUpperCase() + spaced.slice(1);
87
+ const aliases = [
88
+ key,
89
+ key.toLowerCase(),
90
+ title,
91
+ title.replace(/\s+/g, ' '),
92
+ key.replace(/([A-Z])/g, '_$1').toLowerCase()
93
+ ];
94
+ for (const alias of aliases) {
95
+ if (hasMeaningfulContractValue(source[alias]))
96
+ return source[alias];
97
+ }
98
+ return undefined;
99
+ }
100
+ /**
101
+ * Fills missing contract keys from markdown sections after flex-md. Only runs when explicit contract keys were supplied.
102
+ */
103
+ async function enrichParsedContentForOutputContract(parsed, rawContent, contractKeys, logger) {
104
+ const base = asParsedRecord(parsed);
105
+ if (!contractKeys?.length)
106
+ return base;
107
+ const missing = contractKeys.filter((k) => !hasMeaningfulContractValue(base[k]));
108
+ if (missing.length === 0)
109
+ return base;
110
+ const content = typeof rawContent === 'string' && rawContent.trim().length > 0
111
+ ? rawContent
112
+ : typeof base.rawText === 'string'
113
+ ? base.rawText
114
+ : '';
115
+ if (!content.trim())
116
+ return base;
117
+ const fromMarkdown = (0, flex_md_loader_js_1.parseMarkdownSectionsFromContent)(content, logger);
118
+ const merged = { ...base };
119
+ for (const key of missing) {
120
+ const value = pickAliasValue(fromMarkdown, key);
121
+ if (hasMeaningfulContractValue(value)) {
122
+ merged[key] = value;
123
+ }
124
+ }
125
+ return merged;
126
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Normalizes invoke responses into `output.parsed` when an explicit output contract is forwarded.
3
+ * Does not infer contracts from other request fields — that is upstream (graph-engine / ai-tasks).
4
+ */
5
+ import type { Logxer } from '@x12i/logxer';
6
+ /** Explicit contract: field names or JSON-schema-style `properties` only. */
7
+ export type OutputContractSpec = string[] | {
8
+ properties: Record<string, unknown>;
9
+ };
10
+ /**
11
+ * Maps an explicit contract spec to field keys. No inference from descriptions or other request fields.
12
+ */
13
+ export declare function contractSpecToFieldKeys(contract: unknown): string[];
14
+ /**
15
+ * Resolves field keys only from explicit `outputContract` on the request or graph-forwarded inputs.
16
+ */
17
+ export declare function resolveOutputContractFieldKeys(request: unknown): string[] | undefined;
18
+ /**
19
+ * Fills missing contract keys from markdown sections after flex-md. Only runs when explicit contract keys were supplied.
20
+ */
21
+ export declare function enrichParsedContentForOutputContract(parsed: unknown, rawContent: string, contractKeys: string[] | undefined, logger?: Logxer): Promise<Record<string, unknown>>;
@@ -701,6 +701,14 @@ interface BaseLLMRequest extends Omit<LLMRequest, 'messages' | 'input' | 'reques
701
701
  * attach heavy diagnostic objects or raw provider payloads.
702
702
  */
703
703
  diagnostics?: DiagnosticsOptions;
704
+ /**
705
+ * Explicit output contract from graph-engine / ai-tasks: field names or `{ properties }`.
706
+ * Also accepted on `workingMemory.inputs.outputContract`. When absent, the gateway does not
707
+ * infer contract fields from other request shapes (`expectedSchema`, config, etc.).
708
+ */
709
+ outputContract?: string[] | {
710
+ properties: Record<string, unknown>;
711
+ };
704
712
  }
705
713
  /**
706
714
  * Chat request for conversational use cases
@@ -932,6 +940,12 @@ export interface EnhancedLLMResponse<TContent = unknown> extends Omit<AIResponse
932
940
  * Cost in USD (if available)
933
941
  */
934
942
  cost?: number;
943
+ /**
944
+ * Billing state for Run Analysis / Activix when usage is recorded.
945
+ * - `priced`: {@link cost} / {@link costUsd} is a finite number from the router
946
+ * - `unpriced`: usage exists but no price table / adapter cost was returned
947
+ */
948
+ costStatus?: 'priced' | 'unpriced';
935
949
  /**
936
950
  * Cost in USD (preferred stable key when the router exposes it).
937
951
  * When both are present, costUsd should mirror cost.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x12i/ai-gateway",
3
- "version": "9.1.6",
3
+ "version": "9.2.0",
4
4
  "description": "AI Gateway - Unified interface for LLM provider routing and management",
5
5
  "type": "module",
6
6
  "exports": {
@@ -60,7 +60,7 @@
60
60
  "author": "x12i",
61
61
  "license": "mit",
62
62
  "dependencies": {
63
- "@x12i/ai-providers-router": "^4.7.7",
63
+ "@x12i/ai-providers-router": "^4.8.0",
64
64
  "@x12i/rendrix": "^4.3.0",
65
65
  "@aws-sdk/s3-request-presigner": "^3.953.0",
66
66
  "@x12i/env": "^4.0.1",