@x12i/ai-gateway 10.4.4 → 11.0.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.
package/README.md CHANGED
@@ -8,7 +8,7 @@ Unified gateway for LLM provider routing, structured logging, optional Activix a
8
8
  |------|----------|
9
9
  | **Routing** | Registers providers (or lazy-registers from env), invokes the router with merged model config, retries, and optional fallback chain. |
10
10
  | **`invoke()`** | Builds messages from instructions + prompt templates + `workingMemory`; requires runtime **identity** and **actionType** / **actionRef**. |
11
- | **`invokeChat()`** | Raw chat-style requests; no instruction builder or action classification. |
11
+ | **`invokeChat()`** | Same **`buildMessages`** path as **`invoke()`** (instructions + prompt templates + `workingMemory`). |
12
12
  | **Cost** | Steps A→D on every successful **`invoke()`** / **`invokeChat()`**: router cost first, then **`@x12i/ai-tools`** catalog via **`calculateFromRecord`** when still unpriced. Single path — **`resolveCostCompletionWithAiTools`**. |
13
13
  | **Activix** | Optional Mongo-backed activity rows; billing written from gateway-computed slice on **`completeRecord`** (`outer.cost` + root fields). No Activix **`autoCost`** re-pricing. |
14
14
  | **Trace mode** | `diagnostics.mode === 'trace'` adds `metadata.attempts[]`, `metadata.usage`, and per-attempt **`costUsd`** / **`costStatus`**. |
@@ -152,7 +152,7 @@ import {
152
152
 
153
153
  ### Template rendering (`defaults/template-rendering.json`)
154
154
 
155
- Used by **@x12i/rendrix** when parsing `instructions`, `prompt`, and `context`:
155
+ Used by **@x12i/rendrix** when parsing `instructions` and `prompt`:
156
156
 
157
157
  1. Loaded at gateway init from `defaults/template-rendering.json` (copied to `dist/defaults/` on build).
158
158
  2. Merged with `GatewayConfig.templateRendering`.
@@ -549,7 +549,6 @@ export class ActivityManager {
549
549
  // Build request object snapshots (raw = incoming; parsed = constructed messages/meta)
550
550
  const rawSnapshot = request._rawRequest ?? {
551
551
  instructions: request.instructions,
552
- context: request.context,
553
552
  prompt: request.prompt,
554
553
  messages: request.messages,
555
554
  workingMemory: request.workingMemory,
@@ -559,31 +558,26 @@ export class ActivityManager {
559
558
  const requestData = {};
560
559
  // raw snapshot (only allowed fields)
561
560
  if (rawSnapshot.instructions !== undefined ||
562
- rawSnapshot.context !== undefined ||
563
561
  rawSnapshot.prompt !== undefined) {
564
562
  requestData.raw = {
565
563
  instructions: rawSnapshot.instructions,
566
- context: rawSnapshot.context,
567
564
  prompt: rawSnapshot.prompt
568
565
  };
569
566
  }
570
567
  // parsed snapshot (only allowed fields)
571
568
  // Ensure parsed is populated if parsedSnapshot has data, even if individual fields are undefined
572
569
  if (parsedSnapshot.instructions !== undefined ||
573
- parsedSnapshot.context !== undefined ||
574
570
  parsedSnapshot.prompt !== undefined) {
575
571
  requestData.parsed = {
576
572
  instructions: parsedSnapshot.instructions,
577
- context: parsedSnapshot.context,
578
573
  prompt: parsedSnapshot.prompt
579
574
  };
580
575
  }
581
576
  else if (Object.keys(parsedSnapshot).length > 0) {
582
- // If parsedSnapshot exists but doesn't have instructions/context/prompt,
577
+ // If parsedSnapshot exists but doesn't have instructions/prompt,
583
578
  // still create parsed with what's available (mirror of raw request after processing)
584
579
  requestData.parsed = {
585
580
  instructions: rawSnapshot.instructions,
586
- context: rawSnapshot.context,
587
581
  prompt: rawSnapshot.prompt
588
582
  };
589
583
  }
@@ -631,11 +625,9 @@ export class ActivityManager {
631
625
  // Only attach if any field is present
632
626
  const hasRequest = (requestData.raw &&
633
627
  (requestData.raw.instructions !== undefined ||
634
- requestData.raw.context !== undefined ||
635
628
  requestData.raw.prompt !== undefined)) ||
636
629
  (requestData.parsed &&
637
630
  (requestData.parsed.instructions !== undefined ||
638
- requestData.parsed.context !== undefined ||
639
631
  requestData.parsed.prompt !== undefined)) ||
640
632
  requestData.messages !== undefined ||
641
633
  requestData.workingMemory !== undefined ||
@@ -750,7 +742,6 @@ export class ActivityManager {
750
742
  // Build request object snapshots (same as startActivity)
751
743
  const rawSnapshot = request._rawRequest ?? {
752
744
  instructions: request.instructions,
753
- context: request.context,
754
745
  prompt: request.prompt,
755
746
  messages: request.messages,
756
747
  workingMemory: request.workingMemory,
@@ -760,21 +751,17 @@ export class ActivityManager {
760
751
  const requestData = {};
761
752
  // raw snapshot
762
753
  if (rawSnapshot.instructions !== undefined ||
763
- rawSnapshot.context !== undefined ||
764
754
  rawSnapshot.prompt !== undefined) {
765
755
  requestData.raw = {
766
756
  instructions: rawSnapshot.instructions,
767
- context: rawSnapshot.context,
768
757
  prompt: rawSnapshot.prompt
769
758
  };
770
759
  }
771
760
  // parsed snapshot
772
761
  if (parsedSnapshot.instructions !== undefined ||
773
- parsedSnapshot.context !== undefined ||
774
762
  parsedSnapshot.prompt !== undefined) {
775
763
  requestData.parsed = {
776
764
  instructions: parsedSnapshot.instructions,
777
- context: parsedSnapshot.context,
778
765
  prompt: parsedSnapshot.prompt
779
766
  };
780
767
  }
@@ -802,20 +789,17 @@ export class ActivityManager {
802
789
  requestData.workingMemory = rawSnapshot.workingMemory;
803
790
  }
804
791
  // Add skill-specific request structure
805
- if (rawSnapshot.workingMemory || rawSnapshot.context) {
792
+ if (rawSnapshot.workingMemory) {
806
793
  requestData.skill = {
807
- variables: rawSnapshot.workingMemory,
808
- context: rawSnapshot.context
794
+ variables: rawSnapshot.workingMemory
809
795
  };
810
796
  }
811
797
  // Only attach if any field is present
812
798
  const hasRequest = (requestData.raw &&
813
799
  (requestData.raw.instructions !== undefined ||
814
- requestData.raw.context !== undefined ||
815
800
  requestData.raw.prompt !== undefined)) ||
816
801
  (requestData.parsed &&
817
802
  (requestData.parsed.instructions !== undefined ||
818
- requestData.parsed.context !== undefined ||
819
803
  requestData.parsed.prompt !== undefined)) ||
820
804
  requestData.messages !== undefined ||
821
805
  requestData.workingMemory !== undefined ||
@@ -104,10 +104,6 @@ export function buildWorkingMemory(request, existingWorkingMemory, otherMemories
104
104
  if (!workingMemory.job.objective && request.instructions) {
105
105
  workingMemory.job.objective = request.instructions;
106
106
  }
107
- if (!workingMemory.job.context && request.context) {
108
- workingMemory.job.context = request.context;
109
- }
110
- // Input field has been removed - data should come from workingMemory.input
111
107
  if (!workingMemory.job.narrative && request.prompt) {
112
108
  workingMemory.job.narrative = request.prompt;
113
109
  }
@@ -118,9 +114,6 @@ export function buildWorkingMemory(request, existingWorkingMemory, otherMemories
118
114
  if (!workingMemory.task.objective && request.instructions) {
119
115
  workingMemory.task.objective = request.instructions;
120
116
  }
121
- if (!workingMemory.task.context && request.context) {
122
- workingMemory.task.context = request.context;
123
- }
124
117
  // Input field has been removed - data should come from workingMemory.input
125
118
  if (!workingMemory.task.id && request.identity.taskId) {
126
119
  workingMemory.task.id = request.identity.taskId;
@@ -7,14 +7,11 @@ import type { Logxer } from '@x12i/logxer';
7
7
  import { type MessageBuilderConfig } from './message-builder.js';
8
8
  type Request = ChatRequest | AIRequest;
9
9
  /**
10
- * Constructs messages from instructions/prompt/input/context
10
+ * Constructs messages from instructions and prompt (two-message contract).
11
11
  *
12
12
  * Uses direct message builder which handles:
13
- * - Token resolution (3-tier system)
14
13
  * - Template parsing (via Rendrix, @x12i/rendrix)
15
- * - Instruction block resolution and composition
16
- * - Flex-md format specification
17
- * - Message assembly
14
+ * - Message assembly: system (instructions) + user (prompt or input fallback)
18
15
  */
19
16
  export declare function constructMessages(request: Request, config: MessageBuilderConfig, logger: Logxer, parsedSnapshot?: any): Promise<Array<{
20
17
  role: string;
@@ -13,21 +13,17 @@ function isAIRequest(request) {
13
13
  'reasoningEncrypted' in request;
14
14
  }
15
15
  /**
16
- * Constructs messages from instructions/prompt/input/context
16
+ * Constructs messages from instructions and prompt (two-message contract).
17
17
  *
18
18
  * Uses direct message builder which handles:
19
- * - Token resolution (3-tier system)
20
19
  * - Template parsing (via Rendrix, @x12i/rendrix)
21
- * - Instruction block resolution and composition
22
- * - Flex-md format specification
23
- * - Message assembly
20
+ * - Message assembly: system (instructions) + user (prompt or input fallback)
24
21
  */
25
22
  export async function constructMessages(request, config, logger, parsedSnapshot) {
26
23
  logger.verbose('Constructing messages from request', {
27
24
  jobId: request.identity.jobId,
28
25
  agentId: request.agentId,
29
26
  hasInstructions: !!request.instructions,
30
- hasContext: !!request.context,
31
27
  hasPrompt: !!request.prompt,
32
28
  hasWorkingMemory: !!request.workingMemory
33
29
  });
@@ -2,6 +2,17 @@
2
2
  * Gateway Validation Module
3
3
  * Basic validation for clean proxy implementation
4
4
  */
5
+ function rejectRemovedContextField(request) {
6
+ if ('context' in request && request.context !== undefined) {
7
+ const err = new Error(`The 'context' field has been removed. Context is not sent to the LLM. Merge background data into workingMemory upstream and use the prompt template for the user turn.`);
8
+ err.code = 'CONTEXT_FIELD_REMOVED';
9
+ err.details = {
10
+ field: 'context',
11
+ alternative: 'workingMemory + prompt template'
12
+ };
13
+ throw err;
14
+ }
15
+ }
5
16
  function validateMandatoryRuntimeIdentity(request) {
6
17
  const id = request.identity;
7
18
  if (id === undefined || id === null || typeof id !== 'object') {
@@ -20,6 +31,7 @@ export function validateChatRequest(request) {
20
31
  throw new Error('agentId is required');
21
32
  }
22
33
  validateMandatoryRuntimeIdentity(request);
34
+ rejectRemovedContextField(request);
23
35
  // Reject input field - it has been removed
24
36
  if ('input' in request && request.input !== undefined) {
25
37
  const err = new Error(`The 'input' field has been removed. Use workingMemory.input instead for template rendering. Prompt templates should contain {{input}} which will be resolved from workingMemory.input.`);
@@ -44,6 +56,7 @@ export function validateAIRequest(request) {
44
56
  throw new Error('agentId is required for AI requests');
45
57
  }
46
58
  validateMandatoryRuntimeIdentity(request);
59
+ rejectRemovedContextField(request);
47
60
  if (!request.actionType ||
48
61
  !GATEWAY_ACTION_TYPES.includes(request.actionType)) {
49
62
  throw new Error(`actionType is required and must be one of: ${GATEWAY_ACTION_TYPES.join(', ')}`);
package/dist/gateway.d.ts CHANGED
@@ -29,10 +29,6 @@ export declare class AIGateway {
29
29
  * Invoke AI request (with structured output support)
30
30
  */
31
31
  invoke<TContent = unknown>(request: AIInvokeRequest): Promise<EnhancedLLMResponse<TContent>>;
32
- /**
33
- * Build simple messages from request (instructions and prompt as literal template text; no registry).
34
- */
35
- private buildSimpleMessages;
36
32
  register(provider: any): void;
37
33
  listProviders(): string[];
38
34
  getRouter(): LLMProviderRouter;
package/dist/gateway.js CHANGED
@@ -93,8 +93,54 @@ export class AIGateway {
93
93
  const startTime = Date.now();
94
94
  // Generate simple task type ID
95
95
  const taskTypeId = request.taskTypeId || `task-${Date.now()}`;
96
- // Simple message construction
97
- const messages = this.buildSimpleMessages(request);
96
+ const parsedSnapshot = {};
97
+ let messages = [];
98
+ try {
99
+ const builtMessages = await buildMessages(request, this.messageBuilderConfig, {
100
+ parsedSnapshot
101
+ });
102
+ messages = builtMessages.messages;
103
+ }
104
+ catch (error) {
105
+ const err = error instanceof Error ? error : new Error(String(error));
106
+ const endTime = Date.now();
107
+ const duration = endTime - startTime;
108
+ const errWithCode = err;
109
+ const isResolutionError = err.name === 'InstructionNotFoundError' ||
110
+ err.name === 'InstructionBackendError' ||
111
+ err.name === 'TemplateResolutionError' ||
112
+ errWithCode.code === 'PROMPT_NOT_FOUND' ||
113
+ errWithCode.code === 'PROMPT_RESOLUTION_ERROR' ||
114
+ errWithCode.code === 'PROMPT_RENDERED_EMPTY' ||
115
+ errWithCode.code === 'TEMPLATE_RESOLUTION_ERROR' ||
116
+ errWithCode.code === 'TEMPLATE_VARIABLE_MISSING' ||
117
+ errWithCode.code === 'USER_CONTENT_REQUIRED' ||
118
+ err.message.includes('Failed to resolve') ||
119
+ err.message.includes('Failed to render prompt template') ||
120
+ err.message.includes('not found') ||
121
+ err.message.includes('Instruction not found') ||
122
+ err.message.includes('Prompt not found');
123
+ if (isResolutionError && this.activityManager) {
124
+ await this.activityManager.logBadRequest(request, err, {
125
+ endTime,
126
+ duration,
127
+ error: err.message,
128
+ errorType: errWithCode.code || 'MessageBuildError',
129
+ diagnosticInfo: {
130
+ errorCode: errWithCode.code,
131
+ errorName: err.name,
132
+ failureType: 'validation-failure',
133
+ stage: 'message-building',
134
+ prompt: request.prompt,
135
+ instructions: typeof request.instructions === 'string' ? request.instructions.substring(0, 100) : '(object)'
136
+ },
137
+ failureType: 'validation-failure'
138
+ }, startTime);
139
+ }
140
+ throw err;
141
+ }
142
+ parsedSnapshot.messages = messages;
143
+ request._parsedRequest = parsedSnapshot;
98
144
  // Merge config (modelConfig > request.config > gateway defaults)
99
145
  const aiTools = await this.getAiTools();
100
146
  const mergedConfig = await mergeConfig(request, this.config, this.logger, {
@@ -247,6 +293,7 @@ export class AIGateway {
247
293
  errWithCode.code === 'PROMPT_RENDERED_EMPTY' ||
248
294
  errWithCode.code === 'TEMPLATE_RESOLUTION_ERROR' ||
249
295
  errWithCode.code === 'TEMPLATE_VARIABLE_MISSING' ||
296
+ errWithCode.code === 'USER_CONTENT_REQUIRED' ||
250
297
  err.message.includes('Failed to resolve') ||
251
298
  err.message.includes('Failed to render prompt template') ||
252
299
  err.message.includes('not found') ||
@@ -284,9 +331,6 @@ export class AIGateway {
284
331
  parsedSnapshot.messages = messages;
285
332
  // parsed.instructions and parsed.prompt are set by buildMessages to the resolved/rendered content
286
333
  // (after key resolution and Rendrix). Do not overwrite with raw request keys.
287
- if (parsedSnapshot.context === undefined) {
288
- parsedSnapshot.context = request.context;
289
- }
290
334
  // Attach parsedSnapshot to request for activity tracking
291
335
  request._parsedRequest = parsedSnapshot;
292
336
  // Merge config (modelConfig > request.config > gateway defaults)
@@ -775,37 +819,6 @@ export class AIGateway {
775
819
  }
776
820
  });
777
821
  }
778
- /**
779
- * Build simple messages from request (instructions and prompt as literal template text; no registry).
780
- */
781
- buildSimpleMessages(request) {
782
- const messages = [];
783
- // Add instructions as system message if present
784
- if (request.instructions) {
785
- const instructions = typeof request.instructions === 'string'
786
- ? request.instructions
787
- : 'Default instructions';
788
- messages.push({ role: 'system', content: instructions });
789
- }
790
- // Add context as assistant message if present
791
- if (request.context) {
792
- const context = typeof request.context === 'string'
793
- ? request.context
794
- : JSON.stringify(request.context);
795
- messages.push({ role: 'assistant', content: context });
796
- }
797
- // Add prompt/input as user message
798
- // Input field has been removed - prompt template should contain {{input}} which resolves from workingMemory.input
799
- const userContent = request.prompt || '';
800
- if (userContent) {
801
- messages.push({ role: 'user', content: userContent });
802
- }
803
- // Add direct messages if present
804
- if (request.messages) {
805
- messages.push(...request.messages);
806
- }
807
- return messages;
808
- }
809
822
  // Provider management methods
810
823
  register(provider) {
811
824
  this.router.registerProvider(provider);
@@ -28,6 +28,10 @@ export interface BuiltMessages {
28
28
  hasObjectTypes?: boolean;
29
29
  };
30
30
  }
31
+ /**
32
+ * Serializes workingMemory.input when no prompt template is provided.
33
+ */
34
+ export declare function formatInputFallback(input: unknown): string;
31
35
  /**
32
36
  * Main function to build messages
33
37
  */