@x12i/ai-gateway 10.4.4 → 11.0.1
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 +5 -5
- package/dist/activity-manager.js +3 -19
- package/dist/gateway-memory.js +0 -7
- package/dist/gateway-messages.d.ts +2 -5
- package/dist/gateway-messages.js +2 -6
- package/dist/gateway-utils.js +1 -1
- package/dist/gateway-validation.js +13 -0
- package/dist/gateway.d.ts +0 -4
- package/dist/gateway.js +49 -36
- package/dist/message-builder.d.ts +4 -0
- package/dist/message-builder.js +147 -256
- package/dist/openrouter-runtime-adapter/create-openrouter-runtime-provider.js +54 -0
- package/dist/openrouter-runtime-adapter/map-gateway-request.js +20 -4
- package/dist/openrouter-runtime-adapter/validate-server-tools.js +4 -14
- package/dist/troubleshooting-helper.js +7 -4
- package/dist/types.d.ts +3 -7
- package/dist-cjs/activity-manager.cjs +3 -19
- package/dist-cjs/gateway-memory.cjs +0 -7
- package/dist-cjs/gateway-messages.cjs +2 -6
- package/dist-cjs/gateway-messages.d.ts +2 -5
- package/dist-cjs/gateway-utils.cjs +1 -1
- package/dist-cjs/gateway-validation.cjs +13 -0
- package/dist-cjs/gateway.cjs +49 -36
- package/dist-cjs/gateway.d.ts +0 -4
- package/dist-cjs/message-builder.cjs +147 -256
- package/dist-cjs/message-builder.d.ts +4 -0
- package/dist-cjs/openrouter-runtime-adapter/create-openrouter-runtime-provider.cjs +54 -0
- package/dist-cjs/openrouter-runtime-adapter/map-gateway-request.cjs +20 -4
- package/dist-cjs/openrouter-runtime-adapter/validate-server-tools.cjs +4 -14
- package/dist-cjs/troubleshooting-helper.cjs +7 -4
- package/dist-cjs/types.d.ts +3 -7
- package/package.json +2 -2
package/dist/message-builder.js
CHANGED
|
@@ -5,19 +5,38 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { parseTemplate } from './template-parser.js';
|
|
7
7
|
import { mergeGatewayAndRequestTemplateRenderOptions } from './template-render-merge.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function
|
|
12
|
-
|
|
8
|
+
function hasNonEmptyPrompt(prompt) {
|
|
9
|
+
return typeof prompt === 'string' && prompt.trim() !== '';
|
|
10
|
+
}
|
|
11
|
+
function getWorkingMemoryInput(workingMemory) {
|
|
12
|
+
if (workingMemory == null || typeof workingMemory !== 'object')
|
|
13
|
+
return undefined;
|
|
14
|
+
return workingMemory.input;
|
|
15
|
+
}
|
|
16
|
+
function hasResolvableInput(workingMemory) {
|
|
17
|
+
const input = getWorkingMemoryInput(workingMemory);
|
|
18
|
+
return input !== undefined && input !== null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Serializes workingMemory.input when no prompt template is provided.
|
|
22
|
+
*/
|
|
23
|
+
export function formatInputFallback(input) {
|
|
24
|
+
if (input === undefined || input === null) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
if (typeof input === 'string') {
|
|
28
|
+
return input;
|
|
29
|
+
}
|
|
30
|
+
if (typeof input === 'object') {
|
|
31
|
+
return JSON.stringify(input, null, 2);
|
|
32
|
+
}
|
|
33
|
+
return String(input);
|
|
13
34
|
}
|
|
14
35
|
/**
|
|
15
|
-
* Builds user message
|
|
36
|
+
* Builds user message from prompt template or workingMemory.input fallback.
|
|
16
37
|
*/
|
|
17
38
|
async function buildUserMessage(request, config, shortTermMemory, experienceMemory, knowledgeMemory, templateRenderOptions) {
|
|
18
39
|
const { logger } = config;
|
|
19
|
-
const parts = [];
|
|
20
|
-
// Validate that input field is not provided (removed - use workingMemory.input instead)
|
|
21
40
|
if ('input' in request && request.input !== undefined) {
|
|
22
41
|
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.`);
|
|
23
42
|
err.code = 'INPUT_FIELD_DEPRECATED';
|
|
@@ -32,213 +51,127 @@ async function buildUserMessage(request, config, shortTermMemory, experienceMemo
|
|
|
32
51
|
errorCode: 'INPUT_FIELD_DEPRECATED',
|
|
33
52
|
hasPrompt: !!request.prompt,
|
|
34
53
|
hasWorkingMemory: !!request.workingMemory,
|
|
35
|
-
hasInputInWorkingMemory:
|
|
54
|
+
hasInputInWorkingMemory: hasResolvableInput(request.workingMemory)
|
|
36
55
|
});
|
|
37
56
|
throw err;
|
|
38
57
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const err = new Error(`Prompt is required for AI requests. Provide prompt template text. The template should contain variables such as {{input}} resolved from workingMemory.input.`);
|
|
45
|
-
err.code = 'PROMPT_REQUIRED';
|
|
46
|
-
err.details = {
|
|
47
|
-
missingField: 'prompt',
|
|
48
|
-
hasWorkingMemory: !!request.workingMemory,
|
|
49
|
-
hasInputInWorkingMemory: !!request.workingMemory?.input
|
|
50
|
-
};
|
|
51
|
-
logger.error('Prompt is required for AI requests', {
|
|
58
|
+
if (hasNonEmptyPrompt(request.prompt)) {
|
|
59
|
+
if (typeof request.prompt !== 'string') {
|
|
60
|
+
throw new Error(`Prompt must be a string template, but received: ${typeof request.prompt}`);
|
|
61
|
+
}
|
|
62
|
+
logger.info('Parsing prompt template text', {
|
|
52
63
|
jobId: request.identity.jobId,
|
|
53
64
|
agentId: request.agentId,
|
|
54
|
-
|
|
65
|
+
promptLength: request.prompt.length,
|
|
66
|
+
promptPreview: request.prompt.substring(0, 200),
|
|
67
|
+
hasWorkingMemory: !!request.workingMemory
|
|
55
68
|
});
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
jobId: request.identity.jobId,
|
|
63
|
-
agentId: request.agentId,
|
|
64
|
-
promptLength: request.prompt.length,
|
|
65
|
-
promptPreview: request.prompt.substring(0, 200),
|
|
66
|
-
hasWorkingMemory: !!request.workingMemory
|
|
67
|
-
});
|
|
68
|
-
try {
|
|
69
|
-
if (!request.workingMemory) {
|
|
70
|
-
const err = new Error(`workingMemory is required for prompt template rendering but was not provided.`);
|
|
71
|
-
err.code = 'WORKING_MEMORY_REQUIRED';
|
|
72
|
-
err.details = { requiresVariables: true };
|
|
73
|
-
logger.error('workingMemory required for prompt template rendering', {
|
|
74
|
-
jobId: request.identity.jobId,
|
|
75
|
-
agentId: request.agentId,
|
|
76
|
-
errorCode: 'WORKING_MEMORY_REQUIRED'
|
|
77
|
-
});
|
|
78
|
-
throw err;
|
|
79
|
-
}
|
|
80
|
-
const parsedPrompt = await parseTemplate(request.prompt, request.workingMemory, undefined, shortTermMemory, experienceMemory, knowledgeMemory, templateRenderOptions, logger);
|
|
81
|
-
if (!parsedPrompt || parsedPrompt.trim() === '') {
|
|
82
|
-
const workingMemoryObj = request.workingMemory;
|
|
83
|
-
const err = new Error(`Prompt template rendered to empty string. This may indicate missing template variables or empty template content.`);
|
|
84
|
-
err.code = 'PROMPT_RENDERED_EMPTY';
|
|
85
|
-
err.details = {
|
|
86
|
-
promptLength: request.prompt.length,
|
|
87
|
-
renderedLength: 0,
|
|
88
|
-
hasWorkingMemory: !!request.workingMemory,
|
|
89
|
-
workingMemoryKeys: workingMemoryObj ? Object.keys(workingMemoryObj) : []
|
|
90
|
-
};
|
|
91
|
-
logger.error('Prompt template rendered to empty string', {
|
|
92
|
-
jobId: request.identity.jobId,
|
|
93
|
-
agentId: request.agentId,
|
|
94
|
-
errorCode: 'PROMPT_RENDERED_EMPTY',
|
|
95
|
-
hasWorkingMemory: !!request.workingMemory,
|
|
96
|
-
workingMemoryKeys: workingMemoryObj ? Object.keys(workingMemoryObj) : []
|
|
97
|
-
});
|
|
98
|
-
throw err;
|
|
99
|
-
}
|
|
100
|
-
logger.info('Prompt text parsed successfully', {
|
|
69
|
+
try {
|
|
70
|
+
if (!request.workingMemory) {
|
|
71
|
+
const err = new Error(`workingMemory is required for prompt template rendering but was not provided.`);
|
|
72
|
+
err.code = 'WORKING_MEMORY_REQUIRED';
|
|
73
|
+
err.details = { requiresVariables: true };
|
|
74
|
+
logger.error('workingMemory required for prompt template rendering', {
|
|
101
75
|
jobId: request.identity.jobId,
|
|
102
76
|
agentId: request.agentId,
|
|
103
|
-
|
|
104
|
-
parsedLength: parsedPrompt.length,
|
|
105
|
-
parsedPreview: parsedPrompt.substring(0, 200)
|
|
77
|
+
errorCode: 'WORKING_MEMORY_REQUIRED'
|
|
106
78
|
});
|
|
107
|
-
|
|
79
|
+
throw err;
|
|
108
80
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
else if (err.name === 'TemplateResolutionError') {
|
|
122
|
-
errorCode = 'TEMPLATE_RESOLUTION_ERROR';
|
|
123
|
-
errorMessage = err.message;
|
|
124
|
-
}
|
|
125
|
-
else if (err.message.includes('Template variable') || err.message.includes('missing')) {
|
|
126
|
-
errorCode = 'TEMPLATE_VARIABLE_MISSING';
|
|
127
|
-
errorMessage = err.message;
|
|
128
|
-
}
|
|
129
|
-
logger.error('Failed to render prompt template', {
|
|
81
|
+
const parsedPrompt = await parseTemplate(request.prompt, request.workingMemory, undefined, shortTermMemory, experienceMemory, knowledgeMemory, templateRenderOptions, logger);
|
|
82
|
+
if (!parsedPrompt || parsedPrompt.trim() === '') {
|
|
83
|
+
const workingMemoryObj = request.workingMemory;
|
|
84
|
+
const err = new Error(`Prompt template rendered to empty string. This may indicate missing template variables or empty template content.`);
|
|
85
|
+
err.code = 'PROMPT_RENDERED_EMPTY';
|
|
86
|
+
err.details = {
|
|
87
|
+
promptLength: request.prompt.length,
|
|
88
|
+
renderedLength: 0,
|
|
89
|
+
hasWorkingMemory: !!request.workingMemory,
|
|
90
|
+
workingMemoryKeys: workingMemoryObj ? Object.keys(workingMemoryObj) : []
|
|
91
|
+
};
|
|
92
|
+
logger.error('Prompt template rendered to empty string', {
|
|
130
93
|
jobId: request.identity.jobId,
|
|
131
94
|
agentId: request.agentId,
|
|
132
|
-
errorCode,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
errorStack: err.stack
|
|
95
|
+
errorCode: 'PROMPT_RENDERED_EMPTY',
|
|
96
|
+
hasWorkingMemory: !!request.workingMemory,
|
|
97
|
+
workingMemoryKeys: workingMemoryObj ? Object.keys(workingMemoryObj) : []
|
|
136
98
|
});
|
|
137
|
-
|
|
138
|
-
structuredError.code = errorCode;
|
|
139
|
-
structuredError.details = {
|
|
140
|
-
type: 'prompt',
|
|
141
|
-
originalError: err.message
|
|
142
|
-
};
|
|
143
|
-
throw structuredError;
|
|
99
|
+
throw err;
|
|
144
100
|
}
|
|
101
|
+
logger.info('Prompt text parsed successfully', {
|
|
102
|
+
jobId: request.identity.jobId,
|
|
103
|
+
agentId: request.agentId,
|
|
104
|
+
originalLength: request.prompt.length,
|
|
105
|
+
parsedLength: parsedPrompt.length,
|
|
106
|
+
parsedPreview: parsedPrompt.substring(0, 200)
|
|
107
|
+
});
|
|
108
|
+
return parsedPrompt;
|
|
145
109
|
}
|
|
146
|
-
|
|
147
|
-
const err =
|
|
148
|
-
|
|
110
|
+
catch (error) {
|
|
111
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
112
|
+
let errorCode = 'PROMPT_TEMPLATE_ERROR';
|
|
113
|
+
let errorMessage = `Failed to render prompt template: ${err.message}`;
|
|
114
|
+
if (err.message.includes('not found') || err.message.includes('does not exist')) {
|
|
115
|
+
errorCode = 'PROMPT_NOT_FOUND';
|
|
116
|
+
errorMessage = err.message;
|
|
117
|
+
}
|
|
118
|
+
else if (err.message.includes('rendered to empty')) {
|
|
119
|
+
errorCode = 'PROMPT_RENDERED_EMPTY';
|
|
120
|
+
errorMessage = err.message;
|
|
121
|
+
}
|
|
122
|
+
else if (err.name === 'TemplateResolutionError') {
|
|
123
|
+
errorCode = 'TEMPLATE_RESOLUTION_ERROR';
|
|
124
|
+
errorMessage = err.message;
|
|
125
|
+
}
|
|
126
|
+
else if (err.message.includes('Template variable') || err.message.includes('missing')) {
|
|
127
|
+
errorCode = 'TEMPLATE_VARIABLE_MISSING';
|
|
128
|
+
errorMessage = err.message;
|
|
129
|
+
}
|
|
130
|
+
logger.error('Failed to render prompt template', {
|
|
149
131
|
jobId: request.identity.jobId,
|
|
150
132
|
agentId: request.agentId,
|
|
151
|
-
|
|
133
|
+
errorCode,
|
|
134
|
+
error: err.message,
|
|
135
|
+
errorName: err.name,
|
|
136
|
+
errorStack: err.stack
|
|
152
137
|
});
|
|
153
|
-
|
|
138
|
+
const structuredError = new Error(errorMessage);
|
|
139
|
+
structuredError.code = errorCode;
|
|
140
|
+
structuredError.details = {
|
|
141
|
+
type: 'prompt',
|
|
142
|
+
originalError: err.message
|
|
143
|
+
};
|
|
144
|
+
throw structuredError;
|
|
154
145
|
}
|
|
155
146
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
err.details = {
|
|
165
|
-
promptPreview: typeof request.prompt === 'string' ? request.prompt.substring(0, 200) : request.prompt,
|
|
166
|
-
hasWorkingMemory: !!request.workingMemory,
|
|
167
|
-
workingMemoryKeys: request.workingMemory ? Object.keys(request.workingMemory) : [],
|
|
168
|
-
partsLength: parts.length
|
|
169
|
-
};
|
|
170
|
-
logger.error('Prompt provided but resulted in empty user message', {
|
|
147
|
+
if (hasResolvableInput(request.workingMemory)) {
|
|
148
|
+
const fallback = formatInputFallback(getWorkingMemoryInput(request.workingMemory));
|
|
149
|
+
if (!fallback.trim()) {
|
|
150
|
+
const err = new Error(`workingMemory.input is present but empty. Provide a prompt template or non-empty workingMemory.input.`);
|
|
151
|
+
err.code = 'USER_CONTENT_REQUIRED';
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
logger.info('Using workingMemory.input fallback for user message', {
|
|
171
155
|
jobId: request.identity.jobId,
|
|
172
156
|
agentId: request.agentId,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
partsLength: parts.length,
|
|
176
|
-
parts: parts.map(p => ({ length: p.length, preview: p.substring(0, 50) }))
|
|
157
|
+
fallbackLength: fallback.length,
|
|
158
|
+
fallbackPreview: fallback.substring(0, 200)
|
|
177
159
|
});
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
return userMessage;
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Checks if instructions already meet the required flex-md compliance level
|
|
184
|
-
* Uses flex-md SDK to validate compliance
|
|
185
|
-
*
|
|
186
|
-
* @param instructionsText - Instructions to check
|
|
187
|
-
* @param complianceLevel - Required compliance level (L0, L1, L2, L3)
|
|
188
|
-
* @returns true if instructions meet the compliance level
|
|
189
|
-
*/
|
|
190
|
-
async function hasFlexMdContract(instructionsText, complianceLevel = 'L0') {
|
|
191
|
-
if (!instructionsText || instructionsText.trim() === '') {
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
try {
|
|
195
|
-
// Try to use flex-md SDK to validate compliance
|
|
196
|
-
const { loadFlexMd } = await import('./flex-md-loader.js');
|
|
197
|
-
const flexMd = await loadFlexMd();
|
|
198
|
-
// Check if validateMarkdownAgainstOfs is available
|
|
199
|
-
if (flexMd.validateMarkdownAgainstOfs && typeof flexMd.validateMarkdownAgainstOfs === 'function') {
|
|
200
|
-
try {
|
|
201
|
-
// Try to validate - this might require a spec, so we catch errors
|
|
202
|
-
const result = flexMd.validateMarkdownAgainstOfs(instructionsText, { strictness: complianceLevel });
|
|
203
|
-
// If validation passes, instructions meet compliance
|
|
204
|
-
if (result && result.valid !== false) {
|
|
205
|
-
return true;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
catch (e) {
|
|
209
|
-
// Validation function might need more parameters, fall through to pattern matching
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
catch (error) {
|
|
214
|
-
// flex-md SDK not available or error - fall through to pattern matching
|
|
215
|
-
}
|
|
216
|
-
// Fallback: Pattern-based checking
|
|
217
|
-
const text = instructionsText.toLowerCase();
|
|
218
|
-
// Check for key flex-md enforcement phrases
|
|
219
|
-
const flexMdIndicators = [
|
|
220
|
-
'markdown',
|
|
221
|
-
'fenced block',
|
|
222
|
-
'```markdown',
|
|
223
|
-
'```json',
|
|
224
|
-
'reply in markdown',
|
|
225
|
-
'return your entire answer'
|
|
226
|
-
];
|
|
227
|
-
// Check for enforcement language based on compliance level
|
|
228
|
-
const enforcementIndicators = [];
|
|
229
|
-
if (complianceLevel === 'L2' || complianceLevel === 'L3') {
|
|
230
|
-
// L2/L3 require fenced block container
|
|
231
|
-
enforcementIndicators.push('fenced block', '```markdown', 'single ```markdown', 'entire answer inside', 'nothing else');
|
|
232
|
-
}
|
|
233
|
-
if (complianceLevel === 'L1' || complianceLevel === 'L2' || complianceLevel === 'L3') {
|
|
234
|
-
// L1+ requires section headings
|
|
235
|
-
enforcementIndicators.push('section', 'heading', 'include these');
|
|
160
|
+
return fallback;
|
|
236
161
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
162
|
+
const err = new Error(`User message requires prompt template text or workingMemory.input. Provide prompt or workingMemory.input.`);
|
|
163
|
+
err.code = 'USER_CONTENT_REQUIRED';
|
|
164
|
+
err.details = {
|
|
165
|
+
hasPrompt: hasNonEmptyPrompt(request.prompt),
|
|
166
|
+
hasWorkingMemory: !!request.workingMemory,
|
|
167
|
+
hasInputInWorkingMemory: false
|
|
168
|
+
};
|
|
169
|
+
logger.error('No prompt or workingMemory.input for user message', {
|
|
170
|
+
jobId: request.identity.jobId,
|
|
171
|
+
agentId: request.agentId,
|
|
172
|
+
errorCode: 'USER_CONTENT_REQUIRED'
|
|
173
|
+
});
|
|
174
|
+
throw err;
|
|
242
175
|
}
|
|
243
176
|
/**
|
|
244
177
|
* Main function to build messages
|
|
@@ -247,13 +180,11 @@ export async function buildMessages(request, config, options = {}) {
|
|
|
247
180
|
const { parsedSnapshot } = options;
|
|
248
181
|
const { logger } = config;
|
|
249
182
|
const messages = [];
|
|
250
|
-
// Step 1: Instructions as template text (parsed with full memory context)
|
|
251
|
-
let instructionsText = '';
|
|
252
|
-
// Extract memory context from options
|
|
253
183
|
const shortTermMemory = options.shortTermMemory;
|
|
254
184
|
const experienceMemory = options.experienceMemory;
|
|
255
185
|
const knowledgeMemory = options.knowledgeMemory;
|
|
256
186
|
const templateRenderOptions = mergeGatewayAndRequestTemplateRenderOptions(config.templateRendering, request);
|
|
187
|
+
let instructionsText = '';
|
|
257
188
|
if (request.instructions) {
|
|
258
189
|
if (typeof request.instructions === 'string') {
|
|
259
190
|
logger.info('Using instructions as template text', {
|
|
@@ -268,16 +199,9 @@ export async function buildMessages(request, config, options = {}) {
|
|
|
268
199
|
instructionsText = JSON.stringify(request.instructions);
|
|
269
200
|
}
|
|
270
201
|
}
|
|
271
|
-
// NO SYSTEM CONTEXT FALLBACK - removed default instruction fallback
|
|
272
|
-
// Instructions must be provided explicitly - no defaults allowed
|
|
273
|
-
// If instructionsText is empty here, it's a bad request
|
|
274
|
-
// Step 2: Parse instructions template with full memory context
|
|
275
|
-
// Rendrix handles token resolution, so we just parse directly
|
|
276
202
|
if (instructionsText) {
|
|
277
|
-
instructionsText = await parseTemplate(instructionsText, request.workingMemory, undefined,
|
|
278
|
-
shortTermMemory, experienceMemory, knowledgeMemory, templateRenderOptions, logger);
|
|
203
|
+
instructionsText = await parseTemplate(instructionsText, request.workingMemory, undefined, shortTermMemory, experienceMemory, knowledgeMemory, templateRenderOptions, logger);
|
|
279
204
|
}
|
|
280
|
-
// Instructions must be provided explicitly — no packaged block injection
|
|
281
205
|
if (!instructionsText || instructionsText.trim() === '') {
|
|
282
206
|
const errorMessage = 'No instructions available - cannot proceed without clear instructions. This is a bad request.';
|
|
283
207
|
logger.error(errorMessage, {
|
|
@@ -292,11 +216,9 @@ export async function buildMessages(request, config, options = {}) {
|
|
|
292
216
|
role: 'system',
|
|
293
217
|
content: instructionsText
|
|
294
218
|
});
|
|
295
|
-
// Store resolved/parsed content for activity (parsed = content after resolution + Rendrix, not the key)
|
|
296
219
|
if (parsedSnapshot) {
|
|
297
220
|
parsedSnapshot.instructions = instructionsText;
|
|
298
221
|
}
|
|
299
|
-
// Log the final system message for verification
|
|
300
222
|
logger.info('Final system message constructed', {
|
|
301
223
|
jobId: request.identity.jobId,
|
|
302
224
|
agentId: request.agentId,
|
|
@@ -305,65 +227,34 @@ export async function buildMessages(request, config, options = {}) {
|
|
|
305
227
|
hasFencedBlock: instructionsText.includes('```markdown'),
|
|
306
228
|
hasJson: instructionsText.toLowerCase().includes('json'),
|
|
307
229
|
messagePreview: instructionsText.substring(0, 500),
|
|
308
|
-
fullMessage: instructionsText
|
|
230
|
+
fullMessage: instructionsText
|
|
309
231
|
});
|
|
310
|
-
// Step 7: Add context (if provided) with full memory context
|
|
311
|
-
if (request.context) {
|
|
312
|
-
const contextText = typeof request.context === 'string' ? request.context : JSON.stringify(request.context);
|
|
313
|
-
const parsedContext = await parseTemplate(contextText, request.workingMemory, undefined, // taskConfig removed - no longer used
|
|
314
|
-
shortTermMemory, experienceMemory, knowledgeMemory, templateRenderOptions, logger);
|
|
315
|
-
if (parsedContext && parsedContext.trim() !== '') {
|
|
316
|
-
messages.push({
|
|
317
|
-
role: 'assistant',
|
|
318
|
-
content: parsedContext
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
// Step 8: Build user message with full memory context
|
|
323
232
|
const userMessage = await buildUserMessage(request, config, shortTermMemory, experienceMemory, knowledgeMemory, templateRenderOptions);
|
|
324
|
-
if (userMessage
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
});
|
|
329
|
-
// Store resolved/parsed prompt for activity (parsed = content after resolution + Rendrix, not the key)
|
|
330
|
-
if (parsedSnapshot) {
|
|
331
|
-
parsedSnapshot.prompt = userMessage;
|
|
332
|
-
}
|
|
333
|
-
// Log the user message for verification
|
|
334
|
-
logger.info('Final user message constructed', {
|
|
233
|
+
if (!userMessage || userMessage.trim() === '') {
|
|
234
|
+
const err = new Error(`User message is required but was empty after prompt render or input fallback.`);
|
|
235
|
+
err.code = 'USER_CONTENT_REQUIRED';
|
|
236
|
+
logger.error('User message empty after build', {
|
|
335
237
|
jobId: request.identity.jobId,
|
|
336
238
|
agentId: request.agentId,
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
fullMessage: userMessage // Log full message for debugging
|
|
239
|
+
errorCode: 'USER_CONTENT_REQUIRED',
|
|
240
|
+
hasPrompt: hasNonEmptyPrompt(request.prompt)
|
|
340
241
|
});
|
|
242
|
+
throw err;
|
|
341
243
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
promptPreview: typeof request.prompt === 'string' ? request.prompt.substring(0, 200) : request.prompt,
|
|
349
|
-
hasWorkingMemory: !!request.workingMemory
|
|
350
|
-
};
|
|
351
|
-
logger.error('Prompt provided but no user message created', {
|
|
352
|
-
jobId: request.identity.jobId,
|
|
353
|
-
agentId: request.agentId,
|
|
354
|
-
prompt: request.prompt,
|
|
355
|
-
errorCode: 'PROMPT_NO_USER_MESSAGE'
|
|
356
|
-
});
|
|
357
|
-
throw err;
|
|
358
|
-
}
|
|
359
|
-
// If no prompt was provided, it's just a warning (input-only requests are valid)
|
|
360
|
-
logger.warn('No user message to add', {
|
|
361
|
-
jobId: request.identity.jobId,
|
|
362
|
-
agentId: request.agentId,
|
|
363
|
-
hasPrompt: !!request.prompt
|
|
364
|
-
});
|
|
244
|
+
messages.push({
|
|
245
|
+
role: 'user',
|
|
246
|
+
content: userMessage
|
|
247
|
+
});
|
|
248
|
+
if (parsedSnapshot) {
|
|
249
|
+
parsedSnapshot.prompt = userMessage;
|
|
365
250
|
}
|
|
366
|
-
|
|
251
|
+
logger.info('Final user message constructed', {
|
|
252
|
+
jobId: request.identity.jobId,
|
|
253
|
+
agentId: request.agentId,
|
|
254
|
+
messageLength: userMessage.length,
|
|
255
|
+
messagePreview: userMessage.substring(0, 200),
|
|
256
|
+
fullMessage: userMessage
|
|
257
|
+
});
|
|
367
258
|
logger.info('Complete message structure', {
|
|
368
259
|
jobId: request.identity.jobId,
|
|
369
260
|
agentId: request.agentId,
|
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import { createOpenRouterRuntime, RuntimeConfigError, OpenRouterHttpError, } from '@x12i/openrouter-runtime';
|
|
2
2
|
import { OPENROUTER_RUNTIME_OPERATION } from './map-gateway-request.js';
|
|
3
3
|
import { throwMappedOpenRouterHttpError, throwMappedRuntimeConfigError, throwMappedRuntimeResponseErrors, } from './map-runtime-errors.js';
|
|
4
|
+
function summarizeServerTools(serverTools) {
|
|
5
|
+
if (!serverTools)
|
|
6
|
+
return undefined;
|
|
7
|
+
const out = {};
|
|
8
|
+
for (const [key, value] of Object.entries(serverTools)) {
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
out[key] = value.map((item) => item.mode).join(',');
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (value && typeof value === 'object' && 'mode' in value && typeof value.mode === 'string') {
|
|
14
|
+
out[key] = value.mode;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return Object.keys(out).length ? out : undefined;
|
|
18
|
+
}
|
|
19
|
+
function summarizeCompiledTools(compiled) {
|
|
20
|
+
const tools = Array.isArray(compiled.body.tools) ? compiled.body.tools : [];
|
|
21
|
+
const toolTypes = tools
|
|
22
|
+
.map((tool) => (tool && typeof tool === 'object' && 'type' in tool ? String(tool.type) : undefined))
|
|
23
|
+
.filter((type) => typeof type === 'string');
|
|
24
|
+
return {
|
|
25
|
+
apiMode: compiled.apiMode,
|
|
26
|
+
toolCount: tools.length,
|
|
27
|
+
toolTypes,
|
|
28
|
+
bodyKeys: Object.keys(compiled.body),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
4
31
|
export function createOpenRouterRuntimeProvider(options) {
|
|
5
32
|
const runtime = options.runtime ??
|
|
6
33
|
createOpenRouterRuntime({
|
|
@@ -9,6 +36,22 @@ export function createOpenRouterRuntimeProvider(options) {
|
|
|
9
36
|
retry: { enabled: false },
|
|
10
37
|
onPolicyViolation: 'throw',
|
|
11
38
|
},
|
|
39
|
+
logger: options.logger
|
|
40
|
+
? {
|
|
41
|
+
debug: (event, data) => options.logger?.debug(event, data),
|
|
42
|
+
info: (event, data) => options.logger?.info(event, data),
|
|
43
|
+
warn: (event, data) => options.logger?.warn(event, data),
|
|
44
|
+
error: (event, data) => options.logger?.error(event, data),
|
|
45
|
+
}
|
|
46
|
+
: undefined,
|
|
47
|
+
hooks: options.logger
|
|
48
|
+
? {
|
|
49
|
+
beforeOpenRouterRequest: (compiled) => {
|
|
50
|
+
const summary = summarizeCompiledTools(compiled);
|
|
51
|
+
options.logger?.debug('OpenRouter runtime compiled request tools', summary);
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
: undefined,
|
|
12
55
|
});
|
|
13
56
|
return {
|
|
14
57
|
name: 'openrouter',
|
|
@@ -22,6 +65,17 @@ export function createOpenRouterRuntimeProvider(options) {
|
|
|
22
65
|
throw new Error(`Unsupported OpenRouter runtime operation: ${spec.operation}`);
|
|
23
66
|
}
|
|
24
67
|
const runtimeRequest = spec.args;
|
|
68
|
+
const rawOverrideKeys = runtimeRequest.rawOpenRouterOverrides
|
|
69
|
+
? Object.keys(runtimeRequest.rawOpenRouterOverrides)
|
|
70
|
+
: [];
|
|
71
|
+
options.logger?.debug('OpenRouter runtime request server tools', {
|
|
72
|
+
requestId: spec.requestId,
|
|
73
|
+
model: runtimeRequest.model,
|
|
74
|
+
apiMode: runtimeRequest.apiMode,
|
|
75
|
+
serverTools: summarizeServerTools(runtimeRequest.serverTools),
|
|
76
|
+
rawOverrideKeys,
|
|
77
|
+
rawOverridesToolsPresent: Object.prototype.hasOwnProperty.call(runtimeRequest.rawOpenRouterOverrides ?? {}, 'tools'),
|
|
78
|
+
});
|
|
25
79
|
try {
|
|
26
80
|
const response = await runtime.run(runtimeRequest);
|
|
27
81
|
if (response.status !== 'completed') {
|
|
@@ -22,14 +22,30 @@ function buildGenerationOverrides(config) {
|
|
|
22
22
|
overrides.stop = config.stop;
|
|
23
23
|
return overrides;
|
|
24
24
|
}
|
|
25
|
+
function hasActiveServerTools(serverTools) {
|
|
26
|
+
if (!serverTools)
|
|
27
|
+
return false;
|
|
28
|
+
return Object.values(serverTools).some((value) => {
|
|
29
|
+
if (Array.isArray(value))
|
|
30
|
+
return value.some((item) => item.mode !== 'disabled');
|
|
31
|
+
return !!value && value.mode !== 'disabled';
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function buildRawOpenRouterOverrides(generationOverrides, trustedRaw, serverTools) {
|
|
35
|
+
const raw = { ...trustedRaw };
|
|
36
|
+
if (hasActiveServerTools(serverTools)) {
|
|
37
|
+
delete raw.tools;
|
|
38
|
+
}
|
|
39
|
+
const merged = { ...generationOverrides, ...raw };
|
|
40
|
+
return Object.keys(merged).length ? merged : undefined;
|
|
41
|
+
}
|
|
25
42
|
export function mapGatewayRequestToRuntimeRequest(request, exec) {
|
|
26
43
|
const config = request.config ?? {};
|
|
27
44
|
const openrouter = config.openrouter;
|
|
28
45
|
const generationOverrides = buildGenerationOverrides(config);
|
|
29
46
|
const trustedRaw = openrouter?.rawOverrides ?? {};
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
: undefined;
|
|
47
|
+
const serverTools = mapGatewayServerTools(config.serverTools);
|
|
48
|
+
const rawOpenRouterOverrides = buildRawOpenRouterOverrides(generationOverrides, trustedRaw, serverTools);
|
|
33
49
|
const aiRequestId = (typeof request.aiRequestId === 'string' && request.aiRequestId) ||
|
|
34
50
|
(typeof request.identity?.aiRequestId === 'string' ? request.identity.aiRequestId : undefined);
|
|
35
51
|
return {
|
|
@@ -42,7 +58,7 @@ export function mapGatewayRequestToRuntimeRequest(request, exec) {
|
|
|
42
58
|
reasoning: config.reasoning != null && typeof config.reasoning === 'object'
|
|
43
59
|
? config.reasoning
|
|
44
60
|
: undefined,
|
|
45
|
-
serverTools
|
|
61
|
+
serverTools,
|
|
46
62
|
execution: exec?.timeoutMs ? { timeoutMs: exec.timeoutMs } : undefined,
|
|
47
63
|
rawOpenRouterOverrides,
|
|
48
64
|
metadata: {
|
|
@@ -73,22 +73,12 @@ export function applyPostRoutingServerToolsPolicy(input) {
|
|
|
73
73
|
return { serverTools, warnings, forceOpenRouter: false };
|
|
74
74
|
}
|
|
75
75
|
validateApplyPatchConfig(serverTools, openrouter);
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
throw new ProviderConfigError('OPENROUTER_SERVER_TOOL_REQUIRES_KEY', 'Required OpenRouter server tools need OPENROUTER_API_KEY or GatewayConfig.openrouter.apiKey', { codeAliases: ['OPENROUTER_SERVER_TOOL_REQUIRES_OPENROUTER_KEY'] });
|
|
79
|
-
}
|
|
80
|
-
if (provider !== 'openrouter') {
|
|
81
|
-
provider = 'openrouter';
|
|
82
|
-
return { serverTools, warnings, forceOpenRouter: true };
|
|
83
|
-
}
|
|
84
|
-
return { serverTools, warnings, forceOpenRouter: false };
|
|
76
|
+
if (!openRouterApiKey) {
|
|
77
|
+
throw new ProviderConfigError('OPENROUTER_SERVER_TOOL_REQUIRES_KEY', 'OpenRouter server tools need OPENROUTER_API_KEY or GatewayConfig.openrouter.apiKey', { codeAliases: ['OPENROUTER_SERVER_TOOL_REQUIRES_OPENROUTER_KEY'] });
|
|
85
78
|
}
|
|
86
79
|
if (provider !== 'openrouter') {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
message: 'OpenRouter server tools were requested but the final provider is not openrouter; tools were stripped',
|
|
90
|
-
});
|
|
91
|
-
return { serverTools: undefined, warnings, forceOpenRouter: false };
|
|
80
|
+
provider = 'openrouter';
|
|
81
|
+
return { serverTools, warnings, forceOpenRouter: true };
|
|
92
82
|
}
|
|
93
83
|
return { serverTools, warnings, forceOpenRouter: false };
|
|
94
84
|
}
|