@x12i/ai-providers-router 4.6.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/.metadata/anthropic.response-map.json +1 -0
- package/.metadata/google.response-map.json +1 -0
- package/.metadata/groq.response-map.json +1 -0
- package/.metadata/llm-request-config-registry.json +111 -0
- package/.metadata/llm-response-config-registry.json +1 -0
- package/.metadata/model-aliases.json +1 -0
- package/.metadata/model-normalization.json +1 -0
- package/.metadata/moonshot.response-map.json +1 -0
- package/.metadata/openai.response-map.json +1 -0
- package/.metadata/openrouter_catalog_with_vendor_mapping.json +15781 -0
- package/.metadata/reasoning-support.json +159 -0
- package/.metadata/xai.response-map.json +1 -0
- package/README.md +480 -0
- package/dist/adapters/grok/GrokAdapter.d.ts +50 -0
- package/dist/adapters/grok/GrokAdapter.js +342 -0
- package/dist/adapters/openai/OpenAIAdapter.d.ts +50 -0
- package/dist/adapters/openai/OpenAIAdapter.js +401 -0
- package/dist/adapters/openrouter/OpenRouterAdapter.d.ts +87 -0
- package/dist/adapters/openrouter/OpenRouterAdapter.js +1449 -0
- package/dist/adapters/openrouter/reasoning-capabilities.d.ts +26 -0
- package/dist/adapters/openrouter/reasoning-capabilities.js +79 -0
- package/dist/discovery.d.ts +6 -0
- package/dist/discovery.js +114 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +33 -0
- package/dist/factory.d.ts +15 -0
- package/dist/factory.js +206 -0
- package/dist/gateway.d.ts +22 -0
- package/dist/gateway.js +154 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +42 -0
- package/dist/interceptors.d.ts +10 -0
- package/dist/interceptors.js +1 -0
- package/dist/logger.d.ts +70 -0
- package/dist/logger.js +222 -0
- package/dist/openrouter-catalog.d.ts +119 -0
- package/dist/openrouter-catalog.js +222 -0
- package/dist/providers/OpenRouterProvider.d.ts +16 -0
- package/dist/providers/OpenRouterProvider.js +171 -0
- package/dist/registry/AdapterRegistry.d.ts +86 -0
- package/dist/registry/AdapterRegistry.js +36 -0
- package/dist/registry/ProviderRegistry.d.ts +24 -0
- package/dist/registry/ProviderRegistry.js +46 -0
- package/dist/router/Router.d.ts +33 -0
- package/dist/router/Router.js +258 -0
- package/dist/router/RouterTypes.d.ts +138 -0
- package/dist/router/RouterTypes.js +5 -0
- package/dist/router/RouterWrapper.d.ts +83 -0
- package/dist/router/RouterWrapper.js +744 -0
- package/dist/router.d.ts +13 -0
- package/dist/router.js +8 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.js +1 -0
- package/dist/utils/esm-compat.d.ts +9 -0
- package/dist/utils/esm-compat.js +13 -0
- package/dist/utils/ids.d.ts +4 -0
- package/dist/utils/ids.js +6 -0
- package/package.json +66 -0
|
@@ -0,0 +1,1449 @@
|
|
|
1
|
+
import { openRouterCatalog } from '../../openrouter-catalog.js';
|
|
2
|
+
import { getReasoningCapabilitiesFromRegistry, hasReasoningParamInCatalog } from './reasoning-capabilities.js';
|
|
3
|
+
/**
|
|
4
|
+
* Router-side adapter for OpenRouter provider
|
|
5
|
+
* Converts router requests to ProviderSDKCallSpec and parses responses
|
|
6
|
+
* Does NOT use ai-io-normalizer - parses OpenAI-compatible responses directly
|
|
7
|
+
*/
|
|
8
|
+
export class OpenRouterAdapter {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.provider = 'openrouter';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Normalize model name for OpenRouter using catalog data
|
|
14
|
+
* Uses the catalog to properly map provider + model to OpenRouter format
|
|
15
|
+
*/
|
|
16
|
+
async normalizeModelName(model, providerName) {
|
|
17
|
+
try {
|
|
18
|
+
// Try to normalize using catalog data
|
|
19
|
+
return await openRouterCatalog.normalizeModelName(model, providerName);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
// Fallback to old logic if catalog fails to load
|
|
23
|
+
console.warn('Failed to use OpenRouter catalog for model normalization, falling back to legacy logic:', error);
|
|
24
|
+
// Legacy fallback logic
|
|
25
|
+
if (model.includes('/')) {
|
|
26
|
+
return model;
|
|
27
|
+
}
|
|
28
|
+
if (providerName) {
|
|
29
|
+
const prefix = this.getProviderPrefix(providerName);
|
|
30
|
+
return `${prefix}/${model}`;
|
|
31
|
+
}
|
|
32
|
+
return `openai/${model}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Map provider name to OpenRouter model prefix (legacy fallback)
|
|
37
|
+
*/
|
|
38
|
+
getProviderPrefix(providerName) {
|
|
39
|
+
const mapping = {
|
|
40
|
+
'openai': 'openai',
|
|
41
|
+
'grok': 'xai',
|
|
42
|
+
'xai': 'xai',
|
|
43
|
+
'anthropic': 'anthropic',
|
|
44
|
+
'google': 'google',
|
|
45
|
+
'groq': 'groq',
|
|
46
|
+
'meta': 'meta',
|
|
47
|
+
'mistral': 'mistral',
|
|
48
|
+
'cohere': 'cohere',
|
|
49
|
+
'perplexity': 'perplexity',
|
|
50
|
+
};
|
|
51
|
+
return mapping[providerName.toLowerCase()] || providerName.toLowerCase();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get reasoning capabilities for a model
|
|
55
|
+
* Uses JSON registry for cross-vendor support detection
|
|
56
|
+
*/
|
|
57
|
+
async getReasoningCapabilities(modelId) {
|
|
58
|
+
try {
|
|
59
|
+
// Check if model exists in catalog
|
|
60
|
+
const model = await openRouterCatalog.findModelById(modelId);
|
|
61
|
+
if (!model) {
|
|
62
|
+
return {
|
|
63
|
+
supportsEffort: false,
|
|
64
|
+
supportsMaxTokens: false,
|
|
65
|
+
supportsSummary: false,
|
|
66
|
+
supportsTrace: false,
|
|
67
|
+
supportsEncrypted: false,
|
|
68
|
+
supportsReasoningDetails: false,
|
|
69
|
+
mode: 'none',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// First, try registry-based detection (router-owned source of truth)
|
|
73
|
+
const registryCapabilities = getReasoningCapabilitiesFromRegistry(model.canonicalSlug);
|
|
74
|
+
if (registryCapabilities) {
|
|
75
|
+
return {
|
|
76
|
+
supportsEffort: registryCapabilities.mode === 'effort' || registryCapabilities.mode === 'both',
|
|
77
|
+
supportsMaxTokens: registryCapabilities.mode === 'max_tokens' || registryCapabilities.mode === 'both',
|
|
78
|
+
supportsSummary: registryCapabilities.visibility.summary,
|
|
79
|
+
supportsTrace: registryCapabilities.visibility.text || registryCapabilities.visibility.encrypted,
|
|
80
|
+
supportsEncrypted: registryCapabilities.visibility.encrypted,
|
|
81
|
+
supportsReasoningDetails: registryCapabilities.supportsReasoningDetails,
|
|
82
|
+
supportedEfforts: registryCapabilities.supportedEfforts,
|
|
83
|
+
mode: registryCapabilities.mode,
|
|
84
|
+
vendorId: registryCapabilities.vendorId,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// Fallback: Check catalog metadata for reasoning parameters
|
|
88
|
+
const hasReasoning = hasReasoningParamInCatalog(model);
|
|
89
|
+
if (hasReasoning) {
|
|
90
|
+
// Conservative defaults: assume effort mode if reasoning param exists.
|
|
91
|
+
// IMPORTANT: visibility (summary/trace/encrypted) is UNKNOWN without registry;
|
|
92
|
+
// do NOT assume trace/encrypted support from catalog alone.
|
|
93
|
+
return {
|
|
94
|
+
supportsEffort: true,
|
|
95
|
+
supportsMaxTokens: false,
|
|
96
|
+
supportsSummary: false, // Unknown without registry => false
|
|
97
|
+
supportsTrace: false, // Unknown without registry => false
|
|
98
|
+
supportsEncrypted: false, // Unknown without registry => false
|
|
99
|
+
supportsReasoningDetails: true, // Has reasoning param, so supports reasoning_details
|
|
100
|
+
supportedEfforts: ['low', 'medium', 'high'],
|
|
101
|
+
mode: 'effort',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// No reasoning support
|
|
105
|
+
return {
|
|
106
|
+
supportsEffort: false,
|
|
107
|
+
supportsMaxTokens: false,
|
|
108
|
+
supportsSummary: false,
|
|
109
|
+
supportsTrace: false,
|
|
110
|
+
supportsEncrypted: false,
|
|
111
|
+
supportsReasoningDetails: false,
|
|
112
|
+
mode: 'none',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
// On catalog failure, be conservative
|
|
117
|
+
return {
|
|
118
|
+
supportsEffort: false,
|
|
119
|
+
supportsMaxTokens: false,
|
|
120
|
+
supportsSummary: false,
|
|
121
|
+
supportsTrace: false,
|
|
122
|
+
supportsEncrypted: false,
|
|
123
|
+
supportsReasoningDetails: false,
|
|
124
|
+
mode: 'none',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Map unified reasoning request to provider-specific format
|
|
130
|
+
* Implements deterministic downgrade rules based on capabilities
|
|
131
|
+
* Supports both effort (OpenAI/xAI) and max_tokens (Anthropic/Gemini) modes
|
|
132
|
+
*/
|
|
133
|
+
mapReasoningToProvider(reasoning, capabilities) {
|
|
134
|
+
const applied = {
|
|
135
|
+
effort: reasoning?.effort || 'none',
|
|
136
|
+
visibility: reasoning?.visibility || 'none',
|
|
137
|
+
};
|
|
138
|
+
const warnings = [];
|
|
139
|
+
const errors = [];
|
|
140
|
+
let providerReasoning = undefined;
|
|
141
|
+
const onUnsupported = reasoning?.onUnsupported || 'downgrade';
|
|
142
|
+
const mode = capabilities.mode || 'none';
|
|
143
|
+
// Normalize xhigh -> high before validation (deterministic normalization rule)
|
|
144
|
+
if (applied.effort === 'xhigh') {
|
|
145
|
+
applied.effort = 'high';
|
|
146
|
+
warnings.push({
|
|
147
|
+
code: 'EFFORT_NORMALIZED',
|
|
148
|
+
message: `Reasoning effort 'xhigh' normalized to 'high'`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// Handle effort/max_tokens mapping based on mode
|
|
152
|
+
if (mode === 'effort' || mode === 'both') {
|
|
153
|
+
// Handle effort mapping (OpenAI/xAI style)
|
|
154
|
+
if (applied.effort !== 'none' && capabilities.supportsEffort) {
|
|
155
|
+
// Validate effort is supported
|
|
156
|
+
if (capabilities.supportedEfforts && !capabilities.supportedEfforts.includes(applied.effort)) {
|
|
157
|
+
const error = {
|
|
158
|
+
code: 'EFFORT_UNSUPPORTED',
|
|
159
|
+
message: `Reasoning effort '${applied.effort}' not supported by model`,
|
|
160
|
+
};
|
|
161
|
+
if (onUnsupported === 'error') {
|
|
162
|
+
errors.push(error);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
warnings.push({
|
|
166
|
+
code: 'EFFORT_IGNORED',
|
|
167
|
+
message: `${error.message}, using default`,
|
|
168
|
+
});
|
|
169
|
+
applied.effort = 'none';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
providerReasoning = providerReasoning || {};
|
|
174
|
+
providerReasoning.effort = applied.effort;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else if (applied.effort !== 'none' && !capabilities.supportsEffort) {
|
|
178
|
+
const error = {
|
|
179
|
+
code: 'REASONING_UNSUPPORTED',
|
|
180
|
+
message: `Model does not support reasoning effort control`,
|
|
181
|
+
};
|
|
182
|
+
if (onUnsupported === 'error') {
|
|
183
|
+
errors.push(error);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
warnings.push(error);
|
|
187
|
+
applied.effort = 'none';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Handle max_tokens mapping (Anthropic/Gemini style)
|
|
192
|
+
if ((mode === 'max_tokens' || mode === 'both') && reasoning?.maxTokens !== undefined) {
|
|
193
|
+
if (capabilities.supportsMaxTokens) {
|
|
194
|
+
providerReasoning = providerReasoning || {};
|
|
195
|
+
providerReasoning.max_tokens = reasoning?.maxTokens;
|
|
196
|
+
}
|
|
197
|
+
else if (reasoning?.maxTokens !== undefined) {
|
|
198
|
+
const error = {
|
|
199
|
+
code: 'MAX_TOKENS_UNSUPPORTED',
|
|
200
|
+
message: `Model does not support reasoning.max_tokens`,
|
|
201
|
+
};
|
|
202
|
+
if (onUnsupported === 'error') {
|
|
203
|
+
errors.push(error);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
warnings.push({
|
|
207
|
+
code: 'MAX_TOKENS_IGNORED',
|
|
208
|
+
message: `${error.message}, ignoring maxTokens`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Convert effort to max_tokens if needed (for max_tokens-only models)
|
|
214
|
+
if (mode === 'max_tokens' && applied.effort !== 'none' && reasoning?.maxTokens === undefined) {
|
|
215
|
+
// Convert effort to approximate max_tokens
|
|
216
|
+
// Using OpenRouter's documented ratios: low=25%, medium=50%, high=100%
|
|
217
|
+
const effortToMaxTokens = {
|
|
218
|
+
'low': 1000, // Conservative default
|
|
219
|
+
'medium': 2000,
|
|
220
|
+
'high': 4000,
|
|
221
|
+
};
|
|
222
|
+
const estimatedMaxTokens = effortToMaxTokens[applied.effort] || 2000;
|
|
223
|
+
providerReasoning = providerReasoning || {};
|
|
224
|
+
providerReasoning.max_tokens = estimatedMaxTokens;
|
|
225
|
+
warnings.push({
|
|
226
|
+
code: 'EFFORT_CONVERTED_TO_MAX_TOKENS',
|
|
227
|
+
message: `Reasoning effort '${applied.effort}' converted to max_tokens=${estimatedMaxTokens} (model uses max_tokens mode)`,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
// Handle visibility - support both text and encrypted traces
|
|
231
|
+
if (applied.visibility !== 'none') {
|
|
232
|
+
const supportsRequestedVisibility = (applied.visibility === 'summary' && capabilities.supportsSummary) ||
|
|
233
|
+
(applied.visibility === 'trace' && capabilities.supportsTrace);
|
|
234
|
+
if (supportsRequestedVisibility) {
|
|
235
|
+
// Visibility is supported, keep as requested
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
const error = {
|
|
239
|
+
code: 'VISIBILITY_UNSUPPORTED',
|
|
240
|
+
message: `Reasoning visibility '${applied.visibility}' not supported by model`,
|
|
241
|
+
};
|
|
242
|
+
if (onUnsupported === 'error') {
|
|
243
|
+
errors.push(error);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
warnings.push({
|
|
247
|
+
code: 'VISIBILITY_DOWNGRADED',
|
|
248
|
+
message: `${error.message}, downgraded to 'none'`,
|
|
249
|
+
});
|
|
250
|
+
applied.visibility = 'none';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return { providerReasoning, applied, warnings, errors };
|
|
255
|
+
}
|
|
256
|
+
async buildCallSpec(input) {
|
|
257
|
+
const { requestId, mode, request, exec } = input;
|
|
258
|
+
// Extract model from request (could be in request.model or request.config.model)
|
|
259
|
+
let model = request.model || request.config?.model || 'openai/gpt-4o-mini';
|
|
260
|
+
// Determine the original provider name from the request context
|
|
261
|
+
// When OpenRouter mode is enabled, the interceptor sets request.config.provider
|
|
262
|
+
// to preserve the original provider name (e.g., "openai", "grok") for model mapping
|
|
263
|
+
const originalProvider = request.config?.provider;
|
|
264
|
+
// Normalize model name for OpenRouter using catalog data
|
|
265
|
+
// (e.g., "gpt-4o" + provider="openai" → "openai/gpt-4o")
|
|
266
|
+
model = await this.normalizeModelName(model, originalProvider);
|
|
267
|
+
// Validate model is available in OpenRouter catalog
|
|
268
|
+
try {
|
|
269
|
+
const isAvailable = await openRouterCatalog.isModelAvailable(model);
|
|
270
|
+
if (!isAvailable) {
|
|
271
|
+
console.warn(`Model '${model}' not found in OpenRouter catalog. This may result in a runtime error.`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
// Don't fail the request if catalog validation fails, just log warning
|
|
276
|
+
console.warn('Could not validate model against OpenRouter catalog:', error);
|
|
277
|
+
}
|
|
278
|
+
// Determine operation based on reasoning visibility request
|
|
279
|
+
// If visibility is requested (trace/summary), prefer Responses API if model supports it
|
|
280
|
+
// Otherwise use Chat Completions (OpenRouter's default)
|
|
281
|
+
// Note: reasoning_details can appear in both formats, but Responses API is preferred for reasoning
|
|
282
|
+
// IMPORTANT: Responses API requires reasoning.effort, so only use it for effort-mode models
|
|
283
|
+
const requestedVisibility = request.config?.reasoning?.visibility;
|
|
284
|
+
const useResponsesAPIForReasoning = requestedVisibility && requestedVisibility !== 'none';
|
|
285
|
+
// Check if model supports reasoning and if we should use Responses API
|
|
286
|
+
let useResponsesAPI = false;
|
|
287
|
+
if (useResponsesAPIForReasoning && request.config?.reasoning) {
|
|
288
|
+
const capabilities = await this.getReasoningCapabilities(model);
|
|
289
|
+
// Use Responses API only if:
|
|
290
|
+
// 1. Model supports reasoning details
|
|
291
|
+
// 2. Model uses effort mode (not max_tokens mode) - Responses API requires reasoning.effort
|
|
292
|
+
// Note: reasoning_details can appear in both Chat Completions and Responses API formats
|
|
293
|
+
// Chat Completions supports reasoning_details[] for all vendors, so it's safer for max_tokens models
|
|
294
|
+
const usesEffortMode = capabilities.mode === 'effort' || capabilities.mode === 'both';
|
|
295
|
+
useResponsesAPI = capabilities.supportsReasoningDetails === true && usesEffortMode;
|
|
296
|
+
}
|
|
297
|
+
// Determine operation
|
|
298
|
+
const operation = useResponsesAPI
|
|
299
|
+
? (mode === 'stream' ? 'openai.responses.stream' : 'openai.responses.create')
|
|
300
|
+
: (mode === 'stream' ? 'openai.chat.completions.create' : 'openai.chat.completions.create');
|
|
301
|
+
// Build args based on API type
|
|
302
|
+
const args = {
|
|
303
|
+
model,
|
|
304
|
+
};
|
|
305
|
+
if (useResponsesAPI) {
|
|
306
|
+
// Responses API format: convert messages to input format
|
|
307
|
+
let inputValue;
|
|
308
|
+
if (request.messages && Array.isArray(request.messages)) {
|
|
309
|
+
// Convert messages array to input text format
|
|
310
|
+
const systemMessages = request.messages.filter((m) => m.role === 'system');
|
|
311
|
+
const userMessages = request.messages.filter((m) => m.role === 'user');
|
|
312
|
+
const assistantMessages = request.messages.filter((m) => m.role === 'assistant');
|
|
313
|
+
const parts = [];
|
|
314
|
+
if (systemMessages.length > 0) {
|
|
315
|
+
parts.push(...systemMessages.map((m) => `System: ${m.content || ''}`));
|
|
316
|
+
}
|
|
317
|
+
if (userMessages.length > 0) {
|
|
318
|
+
parts.push(...userMessages.map((m) => `User: ${m.content || ''}`));
|
|
319
|
+
}
|
|
320
|
+
if (assistantMessages.length > 0) {
|
|
321
|
+
parts.push(...assistantMessages.map((m) => `Assistant: ${m.content || ''}`));
|
|
322
|
+
}
|
|
323
|
+
inputValue = parts.join('\n\n');
|
|
324
|
+
}
|
|
325
|
+
else if (typeof request.inputData === 'string') {
|
|
326
|
+
inputValue = request.inputData;
|
|
327
|
+
}
|
|
328
|
+
else if (request.inputData) {
|
|
329
|
+
inputValue = { input_text: String(request.inputData) };
|
|
330
|
+
}
|
|
331
|
+
else if (request.instructions) {
|
|
332
|
+
inputValue = request.instructions;
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
inputValue = JSON.stringify(request);
|
|
336
|
+
}
|
|
337
|
+
args.input = inputValue;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
// Chat Completions API format: use messages array
|
|
341
|
+
let messages = [];
|
|
342
|
+
if (request.messages && Array.isArray(request.messages)) {
|
|
343
|
+
messages = request.messages;
|
|
344
|
+
}
|
|
345
|
+
else if (typeof request.inputData === 'string') {
|
|
346
|
+
messages = [{ role: 'user', content: request.inputData }];
|
|
347
|
+
}
|
|
348
|
+
else if (request.inputData) {
|
|
349
|
+
messages = [{ role: 'user', content: String(request.inputData) }];
|
|
350
|
+
}
|
|
351
|
+
else if (request.instructions) {
|
|
352
|
+
messages = [{ role: 'user', content: request.instructions }];
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
messages = [{ role: 'user', content: JSON.stringify(request) }];
|
|
356
|
+
}
|
|
357
|
+
args.messages = messages;
|
|
358
|
+
}
|
|
359
|
+
// Add config parameters
|
|
360
|
+
if (request.config) {
|
|
361
|
+
if (useResponsesAPI) {
|
|
362
|
+
// Responses API uses max_output_tokens
|
|
363
|
+
if (request.config.maxTokens !== undefined) {
|
|
364
|
+
args.max_output_tokens = request.config.maxTokens;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
// Chat Completions uses max_tokens
|
|
369
|
+
if (request.config.maxTokens !== undefined) {
|
|
370
|
+
args.max_tokens = request.config.maxTokens;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (request.config.temperature !== undefined) {
|
|
374
|
+
args.temperature = request.config.temperature;
|
|
375
|
+
}
|
|
376
|
+
if (request.config.topP !== undefined) {
|
|
377
|
+
args.top_p = request.config.topP;
|
|
378
|
+
}
|
|
379
|
+
if (request.config.stop !== undefined) {
|
|
380
|
+
args.stop = request.config.stop;
|
|
381
|
+
}
|
|
382
|
+
if (request.config.stream !== undefined) {
|
|
383
|
+
args.stream = request.config.stream;
|
|
384
|
+
}
|
|
385
|
+
// Handle unified reasoning API
|
|
386
|
+
if (request.config.reasoning) {
|
|
387
|
+
// Normalize reasoning defaults at adapter boundary
|
|
388
|
+
const normalizedReasoning = {
|
|
389
|
+
effort: request.config.reasoning.effort || 'none',
|
|
390
|
+
visibility: request.config.reasoning.visibility || 'none',
|
|
391
|
+
onUnsupported: request.config.reasoning.onUnsupported || 'downgrade',
|
|
392
|
+
maxTokens: request.config.reasoning.maxTokens,
|
|
393
|
+
};
|
|
394
|
+
// Get capabilities (may have already been fetched above, but fetch again to be safe)
|
|
395
|
+
const capabilities = await this.getReasoningCapabilities(model);
|
|
396
|
+
const { providerReasoning, errors, warnings } = this.mapReasoningToProvider(normalizedReasoning, capabilities);
|
|
397
|
+
// Enforce onUnsupported semantics: never throw in downgrade mode
|
|
398
|
+
// Only throw if onUnsupported is 'error' AND there are actual errors
|
|
399
|
+
if (errors.length > 0 && normalizedReasoning.onUnsupported === 'error') {
|
|
400
|
+
const error = errors[0]; // Use the first error
|
|
401
|
+
throw new Error(`Reasoning request failed: ${error.message}`);
|
|
402
|
+
}
|
|
403
|
+
// In downgrade mode, convert errors to warnings (already done in mapReasoningToProvider, but ensure consistency)
|
|
404
|
+
// The mapper already handles this, but we ensure no errors slip through in downgrade mode
|
|
405
|
+
if (providerReasoning && Object.keys(providerReasoning).length > 0) {
|
|
406
|
+
args.reasoning = providerReasoning;
|
|
407
|
+
}
|
|
408
|
+
// Store reasoning request info for response parsing (with normalized requested)
|
|
409
|
+
args._router_reasoning = {
|
|
410
|
+
requested: normalizedReasoning,
|
|
411
|
+
capabilities,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// Set stream mode for streaming requests
|
|
416
|
+
if (mode === 'stream') {
|
|
417
|
+
args.stream = true;
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
requestId,
|
|
421
|
+
provider: 'openrouter',
|
|
422
|
+
mode,
|
|
423
|
+
operation,
|
|
424
|
+
args,
|
|
425
|
+
exec: exec ? {
|
|
426
|
+
timeoutMs: exec.timeoutMs,
|
|
427
|
+
retries: exec.retries,
|
|
428
|
+
idempotencyKey: exec.idempotencyKey,
|
|
429
|
+
signal: exec.signal,
|
|
430
|
+
} : undefined,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
parseResponse(input) {
|
|
434
|
+
const { requestId, execResult, request } = input;
|
|
435
|
+
const rawResponse = execResult.rawResponse;
|
|
436
|
+
// #region agent log
|
|
437
|
+
console.log('[DEBUG] parseResponse entry:', {
|
|
438
|
+
hasRawResponse: !!rawResponse,
|
|
439
|
+
rawResponseKeys: rawResponse ? Object.keys(rawResponse) : [],
|
|
440
|
+
rawResponseFormat: rawResponse?.format,
|
|
441
|
+
rawResponseOutputType: typeof rawResponse?.output,
|
|
442
|
+
rawResponseOutputIsArray: Array.isArray(rawResponse?.output),
|
|
443
|
+
rawResponseOutputLength: Array.isArray(rawResponse?.output) ? rawResponse.output.length : 'not-array',
|
|
444
|
+
rawMetaKeys: execResult.rawMeta ? Object.keys(execResult.rawMeta) : [],
|
|
445
|
+
rawMetaOriginalResponse: !!execResult.rawMeta?.originalResponse,
|
|
446
|
+
rawMetaRawResponse: !!execResult.rawMeta?.rawResponse,
|
|
447
|
+
});
|
|
448
|
+
// #endregion
|
|
449
|
+
// Note: The provider SDK may normalize Responses API v1 format to Chat Completions format.
|
|
450
|
+
// However, OpenRouter may return both formats in the same response, or preserve the output array.
|
|
451
|
+
// We check both rawResponse and rawMeta for the original response structure.
|
|
452
|
+
const originalResponse = execResult.rawMeta?.originalResponse ||
|
|
453
|
+
execResult.rawMeta?.rawResponse ||
|
|
454
|
+
rawResponse;
|
|
455
|
+
console.log('[DEBUG] originalResponse:', JSON.stringify(originalResponse, null, 2));
|
|
456
|
+
// #region agent log
|
|
457
|
+
const rawMetaOriginalResponseDebug = execResult.rawMeta?.originalResponse;
|
|
458
|
+
console.log('[DEBUG] originalResponse determined:', {
|
|
459
|
+
hasOriginalResponse: !!originalResponse,
|
|
460
|
+
originalResponseKeys: originalResponse ? Object.keys(originalResponse) : [],
|
|
461
|
+
originalResponseFormat: originalResponse?.format,
|
|
462
|
+
originalResponseOutputType: typeof originalResponse?.output,
|
|
463
|
+
originalResponseOutputIsArray: Array.isArray(originalResponse?.output),
|
|
464
|
+
originalResponseOutputLength: Array.isArray(originalResponse?.output) ? originalResponse.output.length : 'not-array',
|
|
465
|
+
hasRawMetaOriginalResponse: !!rawMetaOriginalResponseDebug,
|
|
466
|
+
rawMetaOriginalResponseKeys: rawMetaOriginalResponseDebug ? Object.keys(rawMetaOriginalResponseDebug) : [],
|
|
467
|
+
rawMetaOriginalResponseFormat: rawMetaOriginalResponseDebug?.format,
|
|
468
|
+
rawMetaOriginalResponseOutputLength: Array.isArray(rawMetaOriginalResponseDebug?.output) ? rawMetaOriginalResponseDebug.output.length : 'not-array',
|
|
469
|
+
rawMetaOriginalResponseOutputSample: Array.isArray(rawMetaOriginalResponseDebug?.output) && rawMetaOriginalResponseDebug.output.length > 0
|
|
470
|
+
? rawMetaOriginalResponseDebug.output.slice(0, 2).map((i) => ({ type: i?.type, keys: i ? Object.keys(i) : [] }))
|
|
471
|
+
: [],
|
|
472
|
+
isFromRawMetaOriginalResponse: !!execResult.rawMeta?.originalResponse,
|
|
473
|
+
isFromRawMetaRawResponse: !!execResult.rawMeta?.rawResponse,
|
|
474
|
+
isSameAsRawResponse: originalResponse === rawResponse,
|
|
475
|
+
originalResponseOutputSample: Array.isArray(originalResponse?.output) && originalResponse.output.length > 0
|
|
476
|
+
? originalResponse.output.slice(0, 2).map((i) => ({ type: i?.type, keys: i ? Object.keys(i) : [] }))
|
|
477
|
+
: [],
|
|
478
|
+
});
|
|
479
|
+
// #endregion
|
|
480
|
+
// Parse OpenAI-compatible response directly (no ai-io-normalizer)
|
|
481
|
+
// OpenRouter may return either Chat Completions format or newer Responses API format
|
|
482
|
+
let outputText;
|
|
483
|
+
let usage;
|
|
484
|
+
let reasoning;
|
|
485
|
+
// Extract reasoning request info if present
|
|
486
|
+
const reasoningRequest = request?._callSpec?.args?._router_reasoning;
|
|
487
|
+
// Handle OpenAI Responses API format (openai-responses-v1)
|
|
488
|
+
// OpenRouter may return output array even if format field is not set
|
|
489
|
+
// Check both rawResponse (normalized) and originalResponse (unnormalized)
|
|
490
|
+
// IMPORTANT: Check if output array contains Responses API format items (with 'type' field)
|
|
491
|
+
// vs Chat Completions format items (with 'content', 'role' fields)
|
|
492
|
+
const hasOutputArray = Array.isArray(rawResponse?.output);
|
|
493
|
+
// Check rawMeta.originalResponse directly (might have the unnormalized response with output array)
|
|
494
|
+
const rawMetaOriginalResponse = execResult.rawMeta?.originalResponse;
|
|
495
|
+
const hasOutputArrayInRawMeta = Array.isArray(rawMetaOriginalResponse?.output);
|
|
496
|
+
const hasOutputArrayInOriginal = Array.isArray(originalResponse?.output) && originalResponse !== rawResponse;
|
|
497
|
+
const outputHasResponsesFormat = hasOutputArray && rawResponse.output.length > 0 && rawResponse.output.some((item) => item.type);
|
|
498
|
+
const rawMetaHasResponsesFormat = hasOutputArrayInRawMeta && rawMetaOriginalResponse.output.length > 0 && rawMetaOriginalResponse.output.some((item) => item.type);
|
|
499
|
+
const originalHasResponsesFormat = hasOutputArrayInOriginal && originalResponse.output.length > 0 && originalResponse.output.some((item) => item.type);
|
|
500
|
+
const isResponsesAPIFormat = rawResponse?.format === 'openai-responses-v1' || outputHasResponsesFormat || rawMetaHasResponsesFormat || originalHasResponsesFormat;
|
|
501
|
+
// Determine the correct output array to use (for both parsing and final response)
|
|
502
|
+
// Priority: rawMeta.originalResponse > rawResponse.output > originalResponse.output
|
|
503
|
+
// Declare outside if block so it can be reused when creating responseWithStatus
|
|
504
|
+
let outputArray = [];
|
|
505
|
+
if (rawMetaHasResponsesFormat) {
|
|
506
|
+
outputArray = rawMetaOriginalResponse.output;
|
|
507
|
+
}
|
|
508
|
+
else if (outputHasResponsesFormat) {
|
|
509
|
+
outputArray = rawResponse.output;
|
|
510
|
+
}
|
|
511
|
+
else if (originalHasResponsesFormat) {
|
|
512
|
+
outputArray = originalResponse.output;
|
|
513
|
+
}
|
|
514
|
+
else if (isResponsesAPIFormat && hasOutputArray && rawResponse.output.length > 0) {
|
|
515
|
+
// Fallback: if format is openai-responses-v1 and output exists, use it even if format detection didn't match
|
|
516
|
+
// This handles cases where the output array structure is correct but format detection logic missed it
|
|
517
|
+
outputArray = rawResponse.output;
|
|
518
|
+
}
|
|
519
|
+
// #region agent log
|
|
520
|
+
console.log('[DEBUG] output array detection:', {
|
|
521
|
+
hasOutputArray,
|
|
522
|
+
rawResponseOutputLength: Array.isArray(rawResponse?.output) ? rawResponse.output.length : 'not-array',
|
|
523
|
+
hasOutputArrayInRawMeta,
|
|
524
|
+
rawMetaOutputLength: hasOutputArrayInRawMeta ? rawMetaOriginalResponse.output.length : 'not-array',
|
|
525
|
+
hasOutputArrayInOriginal,
|
|
526
|
+
originalResponseOutputLength: Array.isArray(originalResponse?.output) ? originalResponse.output.length : 'not-array',
|
|
527
|
+
outputHasResponsesFormat,
|
|
528
|
+
rawMetaHasResponsesFormat,
|
|
529
|
+
originalHasResponsesFormat,
|
|
530
|
+
isResponsesAPIFormat,
|
|
531
|
+
selectedOutputArrayLength: outputArray.length,
|
|
532
|
+
selectedOutputArraySource: rawMetaHasResponsesFormat ? 'rawMeta' : (outputHasResponsesFormat ? 'rawResponse' : (originalHasResponsesFormat ? 'originalResponse' : 'none')),
|
|
533
|
+
selectedOutputArraySample: outputArray.length > 0 ? outputArray.slice(0, 2).map((i) => ({ type: i?.type, keys: i ? Object.keys(i) : [] })) : [],
|
|
534
|
+
});
|
|
535
|
+
// #endregion
|
|
536
|
+
// #region agent log
|
|
537
|
+
// Check for choices in multiple locations (Fix 3: Check rawMeta for Choices)
|
|
538
|
+
const choicesInRawResponse = rawResponse?.choices;
|
|
539
|
+
const choicesInRawMeta = execResult.rawMeta?.originalResponse?.choices || execResult.rawMeta?.rawResponse?.choices;
|
|
540
|
+
const choicesInOriginal = originalResponse?.choices;
|
|
541
|
+
const hasChoicesInRawResponse = choicesInRawResponse && Array.isArray(choicesInRawResponse) && choicesInRawResponse.length > 0;
|
|
542
|
+
const hasChoicesInRawMeta = choicesInRawMeta && Array.isArray(choicesInRawMeta) && choicesInRawMeta.length > 0;
|
|
543
|
+
const hasChoicesInOriginal = choicesInOriginal && Array.isArray(choicesInOriginal) && choicesInOriginal.length > 0;
|
|
544
|
+
console.log('[DEBUG] Choices detection:', {
|
|
545
|
+
hasChoicesInRawResponse,
|
|
546
|
+
hasChoicesInRawMeta,
|
|
547
|
+
hasChoicesInOriginal,
|
|
548
|
+
choicesLengthRawResponse: hasChoicesInRawResponse ? choicesInRawResponse.length : 0,
|
|
549
|
+
choicesLengthRawMeta: hasChoicesInRawMeta ? choicesInRawMeta.length : 0,
|
|
550
|
+
choicesLengthOriginal: hasChoicesInOriginal ? choicesInOriginal.length : 0,
|
|
551
|
+
isResponsesAPIFormat,
|
|
552
|
+
outputArrayLength: outputArray.length,
|
|
553
|
+
});
|
|
554
|
+
fetch('http://127.0.0.1:7243/ingest/0d4596d8-b8fa-4ca1-a4f4-2e6f23baa429', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'OpenRouterAdapter.ts:625', message: 'Choices detection', data: { hasChoicesInRawResponse, hasChoicesInRawMeta, hasChoicesInOriginal, choicesLengthRawResponse: hasChoicesInRawResponse ? choicesInRawResponse.length : 0, choicesLengthRawMeta: hasChoicesInRawMeta ? choicesInRawMeta.length : 0, choicesLengthOriginal: hasChoicesInOriginal ? choicesInOriginal.length : 0, isResponsesAPIFormat, outputArrayLength: outputArray.length }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'run1', hypothesisId: 'A' }) }).catch(() => { });
|
|
555
|
+
// #endregion
|
|
556
|
+
console.log('[DEBUG] Format decision:', {
|
|
557
|
+
isResponsesAPIFormat,
|
|
558
|
+
outputArrayLength: outputArray.length,
|
|
559
|
+
willParseResponsesAPI: isResponsesAPIFormat && outputArray.length > 0,
|
|
560
|
+
});
|
|
561
|
+
// PRODUCTION-GRADE FIX: Use unified content extraction function first
|
|
562
|
+
// This ensures we extract content regardless of format detection issues
|
|
563
|
+
const extractedContent = this.extractContentFromResponse(rawResponse, originalResponse, execResult, outputArray);
|
|
564
|
+
if (extractedContent !== undefined) {
|
|
565
|
+
outputText = extractedContent;
|
|
566
|
+
console.log('[DEBUG] Content extracted via unified extraction function:', {
|
|
567
|
+
outputTextLength: outputText.length,
|
|
568
|
+
outputTextPreview: outputText.substring(0, 50),
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
if (isResponsesAPIFormat && outputArray.length > 0) {
|
|
572
|
+
// New Responses API format - parse the output array we selected
|
|
573
|
+
console.log('[DEBUG] Parsing Responses API format');
|
|
574
|
+
const encryptedArtifacts = [];
|
|
575
|
+
let summaryText;
|
|
576
|
+
let traceText;
|
|
577
|
+
// Log output array contents when reasoning was requested (for debugging)
|
|
578
|
+
const reasoningRequested = reasoningRequest?.requested?.effort !== 'none' || reasoningRequest?.requested?.visibility !== 'none';
|
|
579
|
+
if (reasoningRequested && outputArray.length > 0) {
|
|
580
|
+
console.log(`[OpenRouterAdapter] Parsing ${outputArray.length} output items for reasoning artifacts...`);
|
|
581
|
+
}
|
|
582
|
+
// #region agent log
|
|
583
|
+
console.log('[DEBUG] Starting output array parsing:', {
|
|
584
|
+
outputArrayLength: outputArray.length,
|
|
585
|
+
outputArrayItems: outputArray.map((i) => ({ type: i?.type, id: i?.id, hasSummary: !!i?.summary, hasText: !!i?.text })),
|
|
586
|
+
});
|
|
587
|
+
// #endregion
|
|
588
|
+
for (const item of outputArray) {
|
|
589
|
+
// Note: Content extraction is already done by unified function above
|
|
590
|
+
// This loop now focuses on reasoning artifacts only
|
|
591
|
+
if (item.type === 'text' && item.content && !outputText) {
|
|
592
|
+
// Fallback: Extract text content if unified function didn't find it
|
|
593
|
+
if (typeof item.content === 'string' && item.content.trim()) {
|
|
594
|
+
outputText = item.content;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
else if (item.type === 'message' && item.content) {
|
|
598
|
+
// Responses API may return message type with content array or object
|
|
599
|
+
if (Array.isArray(item.content)) {
|
|
600
|
+
for (const contentItem of item.content) {
|
|
601
|
+
// Note: Content extraction is already done by unified function above
|
|
602
|
+
// This is fallback only if unified function didn't find content
|
|
603
|
+
if (!outputText) {
|
|
604
|
+
if (contentItem.type === 'text' && contentItem.text && typeof contentItem.text === 'string' && contentItem.text.trim()) {
|
|
605
|
+
outputText = contentItem.text;
|
|
606
|
+
}
|
|
607
|
+
else if (contentItem.type === 'output_text' && contentItem.text && typeof contentItem.text === 'string' && contentItem.text.trim()) {
|
|
608
|
+
outputText = contentItem.text;
|
|
609
|
+
}
|
|
610
|
+
else if (contentItem.type === 'text' && contentItem.content && typeof contentItem.content === 'string' && contentItem.content.trim()) {
|
|
611
|
+
outputText = contentItem.content;
|
|
612
|
+
}
|
|
613
|
+
else if (!contentItem.type && typeof contentItem === 'string' && contentItem.trim()) {
|
|
614
|
+
outputText = contentItem;
|
|
615
|
+
}
|
|
616
|
+
else if (!contentItem.type && contentItem.text && typeof contentItem.text === 'string' && contentItem.text.trim()) {
|
|
617
|
+
outputText = contentItem.text;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// ✅ NEW: allow summary inside message content
|
|
621
|
+
if (contentItem.type === 'reasoning.summary') {
|
|
622
|
+
summaryText = contentItem.summary ?? contentItem.text ?? summaryText;
|
|
623
|
+
}
|
|
624
|
+
// ✅ NEW: allow readable trace inside message content
|
|
625
|
+
if (contentItem.type === 'reasoning.text') {
|
|
626
|
+
traceText = contentItem.text ?? traceText;
|
|
627
|
+
}
|
|
628
|
+
// existing encrypted handling...
|
|
629
|
+
else if (contentItem.type === 'reasoning' && contentItem.reasoning) {
|
|
630
|
+
// Handle reasoning content within message
|
|
631
|
+
if (contentItem.reasoning.encrypted) {
|
|
632
|
+
encryptedArtifacts.push({
|
|
633
|
+
format: 'openai-responses-v1',
|
|
634
|
+
id: item.id || contentItem.reasoning.id,
|
|
635
|
+
index: item.index,
|
|
636
|
+
data: contentItem.reasoning.encrypted,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
else if (typeof item.content === 'string' && item.content.trim() && !outputText) {
|
|
643
|
+
outputText = item.content;
|
|
644
|
+
}
|
|
645
|
+
else if (item.content && typeof item.content === 'object' && !outputText) {
|
|
646
|
+
// Content might be an object with text or other fields
|
|
647
|
+
if (item.content.text && typeof item.content.text === 'string' && item.content.text.trim()) {
|
|
648
|
+
outputText = item.content.text;
|
|
649
|
+
}
|
|
650
|
+
// Check for reasoning in content object
|
|
651
|
+
if (item.content.reasoning && item.content.reasoning.encrypted) {
|
|
652
|
+
encryptedArtifacts.push({
|
|
653
|
+
format: 'openai-responses-v1',
|
|
654
|
+
id: item.id,
|
|
655
|
+
index: item.index,
|
|
656
|
+
data: item.content.reasoning.encrypted,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
else if (item.type === 'reasoning.summary') {
|
|
662
|
+
// ✅ NEW: handle reasoning.summary as top-level output item
|
|
663
|
+
summaryText = item.summary ?? item.text ?? summaryText;
|
|
664
|
+
// #region agent log
|
|
665
|
+
console.log('[DEBUG] Found reasoning.summary:', { summaryText, itemId: item.id, itemKeys: Object.keys(item) });
|
|
666
|
+
// #endregion
|
|
667
|
+
}
|
|
668
|
+
else if (item.type === 'reasoning.text') {
|
|
669
|
+
// ✅ NEW: handle reasoning.text as top-level output item
|
|
670
|
+
// Append trace text (may be multiple chunks)
|
|
671
|
+
traceText = traceText ? `${traceText}\n${item.text}` : item.text;
|
|
672
|
+
// #region agent log
|
|
673
|
+
console.log('[DEBUG] Found reasoning.text:', { traceText, itemId: item.id });
|
|
674
|
+
// #endregion
|
|
675
|
+
}
|
|
676
|
+
else if (item.type === 'reasoning.encrypted' || (item.reasoning && item.reasoning.encrypted)) {
|
|
677
|
+
// Collect encrypted reasoning artifacts (direct or nested)
|
|
678
|
+
const encryptedData = item.data || item.reasoning?.encrypted || item.reasoning;
|
|
679
|
+
encryptedArtifacts.push({
|
|
680
|
+
format: 'openai-responses-v1',
|
|
681
|
+
id: item.id,
|
|
682
|
+
index: item.index,
|
|
683
|
+
data: encryptedData,
|
|
684
|
+
});
|
|
685
|
+
if (reasoningRequested) {
|
|
686
|
+
console.log(`[OpenRouterAdapter] ✅ Found encrypted reasoning artifact: id=${item.id}, index=${item.index}`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
else if (reasoningRequested && item.type) {
|
|
690
|
+
// Log other item types when reasoning was requested (for debugging)
|
|
691
|
+
console.log(`[OpenRouterAdapter] Output item type: ${item.type} (not reasoning.encrypted)`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (reasoningRequested) {
|
|
695
|
+
console.log(`[OpenRouterAdapter] Extracted ${encryptedArtifacts.length} encrypted reasoning artifacts from ${outputArray.length} output items`);
|
|
696
|
+
}
|
|
697
|
+
// Extract usage if available
|
|
698
|
+
if (rawResponse?.usage) {
|
|
699
|
+
usage = {
|
|
700
|
+
inputTokens: rawResponse.usage.input_tokens,
|
|
701
|
+
outputTokens: rawResponse.usage.output_tokens,
|
|
702
|
+
totalTokens: rawResponse.usage.total_tokens,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
// Build unified reasoning response (ONLY source of truth)
|
|
706
|
+
reasoning = this.buildReasoningResponse(reasoningRequest, {
|
|
707
|
+
encrypted: encryptedArtifacts,
|
|
708
|
+
summaryText,
|
|
709
|
+
traceText,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
// Handle traditional Chat Completions format
|
|
713
|
+
// Fix 3: Check rawMeta for choices as fallback
|
|
714
|
+
else {
|
|
715
|
+
console.log('[DEBUG] Entering Chat Completions parsing branch');
|
|
716
|
+
// Find choices in any location (rawResponse, rawMeta, originalResponse)
|
|
717
|
+
let choices = undefined;
|
|
718
|
+
if (rawResponse?.choices && Array.isArray(rawResponse.choices) && rawResponse.choices.length > 0) {
|
|
719
|
+
choices = rawResponse.choices;
|
|
720
|
+
}
|
|
721
|
+
else if (execResult.rawMeta?.originalResponse?.choices && Array.isArray(execResult.rawMeta.originalResponse.choices) && execResult.rawMeta.originalResponse.choices.length > 0) {
|
|
722
|
+
choices = execResult.rawMeta.originalResponse.choices;
|
|
723
|
+
}
|
|
724
|
+
else if (execResult.rawMeta?.rawResponse?.choices && Array.isArray(execResult.rawMeta.rawResponse.choices) && execResult.rawMeta.rawResponse.choices.length > 0) {
|
|
725
|
+
choices = execResult.rawMeta.rawResponse.choices;
|
|
726
|
+
}
|
|
727
|
+
else if (originalResponse?.choices && Array.isArray(originalResponse.choices) && originalResponse.choices.length > 0) {
|
|
728
|
+
choices = originalResponse.choices;
|
|
729
|
+
}
|
|
730
|
+
console.log('[DEBUG] Choices search result:', {
|
|
731
|
+
foundChoices: !!choices,
|
|
732
|
+
choicesLength: choices?.length || 0,
|
|
733
|
+
choicesSource: choices === rawResponse?.choices ? 'rawResponse' :
|
|
734
|
+
choices === execResult.rawMeta?.originalResponse?.choices ? 'rawMeta.originalResponse' :
|
|
735
|
+
choices === execResult.rawMeta?.rawResponse?.choices ? 'rawMeta.rawResponse' :
|
|
736
|
+
choices === originalResponse?.choices ? 'originalResponse' : 'none',
|
|
737
|
+
});
|
|
738
|
+
if (choices && choices.length > 0) {
|
|
739
|
+
const firstChoice = choices[0];
|
|
740
|
+
// #region agent log
|
|
741
|
+
console.log('[DEBUG] Chat Completions parsing entry:', {
|
|
742
|
+
choicesLength: choices.length,
|
|
743
|
+
hasFirstChoice: !!firstChoice,
|
|
744
|
+
hasMessage: !!firstChoice?.message,
|
|
745
|
+
messageKeys: firstChoice?.message ? Object.keys(firstChoice.message) : [],
|
|
746
|
+
hasContent: firstChoice?.message?.content !== undefined,
|
|
747
|
+
contentType: typeof firstChoice?.message?.content,
|
|
748
|
+
contentValue: firstChoice?.message?.content,
|
|
749
|
+
contentIsNull: firstChoice?.message?.content === null,
|
|
750
|
+
contentIsUndefined: firstChoice?.message?.content === undefined,
|
|
751
|
+
});
|
|
752
|
+
fetch('http://127.0.0.1:7243/ingest/0d4596d8-b8fa-4ca1-a4f4-2e6f23baa429', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'OpenRouterAdapter.ts:758', message: 'Chat Completions parsing entry', data: { choicesLength: choices.length, hasFirstChoice: !!firstChoice, hasMessage: !!firstChoice?.message, messageKeys: firstChoice?.message ? Object.keys(firstChoice.message) : [], hasContent: firstChoice?.message?.content !== undefined, contentType: typeof firstChoice?.message?.content, contentValue: firstChoice?.message?.content, contentIsNull: firstChoice?.message?.content === null, contentIsUndefined: firstChoice?.message?.content === undefined }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'run1', hypothesisId: 'B' }) }).catch(() => { });
|
|
753
|
+
// #endregion
|
|
754
|
+
// Note: Content extraction is already done by unified function above
|
|
755
|
+
// This is fallback only if unified function didn't find content
|
|
756
|
+
if (!outputText && firstChoice.message) {
|
|
757
|
+
const content = firstChoice.message.content;
|
|
758
|
+
if (typeof content === 'string' && content.trim()) {
|
|
759
|
+
outputText = content;
|
|
760
|
+
}
|
|
761
|
+
else if (content !== null && content !== undefined) {
|
|
762
|
+
// Handle non-string content (array, object, etc.)
|
|
763
|
+
if (Array.isArray(content)) {
|
|
764
|
+
const textParts = [];
|
|
765
|
+
for (const part of content) {
|
|
766
|
+
if (typeof part === 'string' && part.trim()) {
|
|
767
|
+
textParts.push(part);
|
|
768
|
+
}
|
|
769
|
+
else if (part && typeof part === 'object' && part.text && typeof part.text === 'string' && part.text.trim()) {
|
|
770
|
+
textParts.push(part.text);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (textParts.length > 0) {
|
|
774
|
+
outputText = textParts.join('\n');
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
else if (typeof content === 'object') {
|
|
778
|
+
const str = String(content);
|
|
779
|
+
if (str && str !== '[object Object]' && str.trim()) {
|
|
780
|
+
outputText = str;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// #region agent log
|
|
786
|
+
console.log('[DEBUG] After content extraction:', {
|
|
787
|
+
outputTextExtracted: !!outputText,
|
|
788
|
+
outputTextLength: outputText?.length || 0,
|
|
789
|
+
outputTextPreview: outputText?.substring(0, 50) || null,
|
|
790
|
+
});
|
|
791
|
+
fetch('http://127.0.0.1:7243/ingest/0d4596d8-b8fa-4ca1-a4f4-2e6f23baa429', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'OpenRouterAdapter.ts:770', message: 'After content extraction', data: { outputTextExtracted: !!outputText, outputTextLength: outputText?.length || 0, outputTextPreview: outputText?.substring(0, 50) || null }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'run1', hypothesisId: 'B' }) }).catch(() => { });
|
|
792
|
+
// #endregion
|
|
793
|
+
// Safety guard: if choices exist but outputText is still empty, log warning
|
|
794
|
+
if (!outputText && choices.length > 0) {
|
|
795
|
+
console.warn('[OpenRouterAdapter] Choices exist but content extraction failed', {
|
|
796
|
+
choicesLength: choices.length,
|
|
797
|
+
firstChoice: {
|
|
798
|
+
hasMessage: !!firstChoice.message,
|
|
799
|
+
messageKeys: firstChoice.message ? Object.keys(firstChoice.message) : [],
|
|
800
|
+
content: firstChoice.message?.content,
|
|
801
|
+
contentType: typeof firstChoice.message?.content,
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
// #region agent log
|
|
805
|
+
fetch('http://127.0.0.1:7243/ingest/0d4596d8-b8fa-4ca1-a4f4-2e6f23baa429', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'OpenRouterAdapter.ts:785', message: 'Content extraction failed warning', data: { choicesLength: choices.length, firstChoiceKeys: Object.keys(firstChoice || {}), messageKeys: firstChoice?.message ? Object.keys(firstChoice.message) : [], content: firstChoice?.message?.content, contentType: typeof firstChoice?.message?.content }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'run1', hypothesisId: 'C' }) }).catch(() => { });
|
|
806
|
+
// #endregion
|
|
807
|
+
}
|
|
808
|
+
// Use choices from the source we found (for consistency)
|
|
809
|
+
const choicesToUse = choices;
|
|
810
|
+
// Extract usage from Chat Completions format
|
|
811
|
+
const usageSource = rawResponse?.usage || execResult.rawMeta?.originalResponse?.usage || originalResponse?.usage;
|
|
812
|
+
if (usageSource) {
|
|
813
|
+
usage = {
|
|
814
|
+
inputTokens: usageSource.prompt_tokens || usageSource.input_tokens,
|
|
815
|
+
outputTokens: usageSource.completion_tokens || usageSource.output_tokens,
|
|
816
|
+
totalTokens: usageSource.total_tokens,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
// ✅ Parse reasoning_details from Chat Completions format (choices[].message.reasoning_details[])
|
|
820
|
+
// This is the standard OpenRouter cross-vendor format
|
|
821
|
+
const encryptedArtifacts = [];
|
|
822
|
+
let summaryText;
|
|
823
|
+
let traceText;
|
|
824
|
+
// Check all choices for reasoning_details (not just first)
|
|
825
|
+
for (const choice of choicesToUse || []) {
|
|
826
|
+
// Also capture plaintext reasoning if provider returns it in message.reasoning
|
|
827
|
+
const msgReasoning = choice.message?.reasoning;
|
|
828
|
+
if (typeof msgReasoning === 'string' && msgReasoning.trim()) {
|
|
829
|
+
traceText = traceText ? `${traceText}\n${msgReasoning}` : msgReasoning;
|
|
830
|
+
}
|
|
831
|
+
const reasoningDetails = choice.message?.reasoning_details;
|
|
832
|
+
if (Array.isArray(reasoningDetails)) {
|
|
833
|
+
for (const d of reasoningDetails) {
|
|
834
|
+
if (d.type === 'reasoning.summary') {
|
|
835
|
+
summaryText = d.summary ?? d.text ?? summaryText;
|
|
836
|
+
}
|
|
837
|
+
if (d.type === 'reasoning.text') {
|
|
838
|
+
// Append trace text (may be multiple chunks)
|
|
839
|
+
traceText = traceText ? `${traceText}\n${d.text}` : d.text;
|
|
840
|
+
}
|
|
841
|
+
if (d.type === 'reasoning.encrypted') {
|
|
842
|
+
encryptedArtifacts.push({
|
|
843
|
+
format: 'openai-responses-v1', // Format may vary by vendor, but we normalize
|
|
844
|
+
id: d.id,
|
|
845
|
+
index: d.index,
|
|
846
|
+
data: d.data ?? d.encrypted ?? d.text,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
// IMPORTANT: Check if output array exists and contains Responses API format items
|
|
853
|
+
// Even in Chat Completions response, OpenRouter may include output array with reasoning artifacts
|
|
854
|
+
// This handles Responses API v1 format (openai-responses-v1, xai-responses-v1, etc.)
|
|
855
|
+
const outputArraySource = rawResponse?.output || execResult.rawMeta?.originalResponse?.output || originalResponse?.output;
|
|
856
|
+
if (Array.isArray(outputArraySource)) {
|
|
857
|
+
for (const item of outputArraySource) {
|
|
858
|
+
if (item.type === 'reasoning.encrypted') {
|
|
859
|
+
encryptedArtifacts.push({
|
|
860
|
+
format: rawResponse?.format || originalResponse?.format || 'openai-responses-v1',
|
|
861
|
+
id: item.id,
|
|
862
|
+
index: item.index,
|
|
863
|
+
data: item.data,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
else if (item.type === 'reasoning.summary') {
|
|
867
|
+
summaryText = item.summary ?? item.text ?? summaryText;
|
|
868
|
+
}
|
|
869
|
+
else if (item.type === 'reasoning.text') {
|
|
870
|
+
// Append trace text (may be multiple chunks)
|
|
871
|
+
traceText = traceText ? `${traceText}\n${item.text}` : item.text;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// Build reasoning response (ONLY source of truth)
|
|
876
|
+
const actualCapabilities = reasoningRequest?.capabilities || {
|
|
877
|
+
supportsEffort: false,
|
|
878
|
+
supportsMaxTokens: false,
|
|
879
|
+
supportsSummary: false,
|
|
880
|
+
supportsTrace: false,
|
|
881
|
+
supportsEncrypted: false,
|
|
882
|
+
supportsReasoningDetails: false,
|
|
883
|
+
};
|
|
884
|
+
reasoning = this.buildReasoningResponse(reasoningRequest ? { ...reasoningRequest, capabilities: actualCapabilities } : undefined, { encrypted: encryptedArtifacts, summaryText, traceText });
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
// Fallback for unknown formats
|
|
888
|
+
console.log('[DEBUG] No choices found - using fallback reasoning response');
|
|
889
|
+
reasoning = this.buildReasoningResponse(reasoningRequest, { encrypted: [] });
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
// STRICT MODE: if caller asked to crash when trace is unavailable, enforce it.
|
|
893
|
+
// This covers the "provider returned no trace artifacts" case (not just capability gating).
|
|
894
|
+
// Note: buildReasoningResponse already checks this, but we add an additional safety check here
|
|
895
|
+
// to ensure we catch any edge cases where reasoning was built but artifacts are missing.
|
|
896
|
+
if (reasoningRequest?.requested?.visibility === 'trace' &&
|
|
897
|
+
reasoningRequest?.requested?.onUnsupported === 'error') {
|
|
898
|
+
const hasTraceArtifact = typeof reasoning?.artifacts?.trace === 'object' &&
|
|
899
|
+
typeof reasoning.artifacts.trace?.text === 'string' &&
|
|
900
|
+
reasoning.artifacts.trace.text.trim().length > 0;
|
|
901
|
+
const hasEncrypted = Array.isArray(reasoning?.artifacts?.encrypted) &&
|
|
902
|
+
reasoning.artifacts.encrypted.length > 0;
|
|
903
|
+
if (!hasTraceArtifact && !hasEncrypted) {
|
|
904
|
+
throw new Error("Reasoning request failed: trace was requested but no trace/encrypted reasoning artifacts were returned by provider");
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
// PRODUCTION-GRADE FIX: Final validation - ensure outputText is always set
|
|
908
|
+
// The unified extraction function should have found content, but if not, log detailed error
|
|
909
|
+
if (!outputText || outputText.trim() === '') {
|
|
910
|
+
console.error('[OpenRouterAdapter] CRITICAL: Failed to extract content from response', {
|
|
911
|
+
hasRawResponse: !!rawResponse,
|
|
912
|
+
rawResponseKeys: rawResponse ? Object.keys(rawResponse) : [],
|
|
913
|
+
hasChoices: !!(rawResponse?.choices || execResult.rawMeta?.originalResponse?.choices),
|
|
914
|
+
choicesLength: rawResponse?.choices?.length || execResult.rawMeta?.originalResponse?.choices?.length || 0,
|
|
915
|
+
hasOutputArray: !!(rawResponse?.output || execResult.rawMeta?.originalResponse?.output),
|
|
916
|
+
outputArrayLength: Array.isArray(rawResponse?.output) ? rawResponse.output.length :
|
|
917
|
+
(Array.isArray(execResult.rawMeta?.originalResponse?.output) ?
|
|
918
|
+
execResult.rawMeta.originalResponse.output.length : 0),
|
|
919
|
+
isResponsesAPIFormat,
|
|
920
|
+
selectedOutputArrayLength: outputArray.length,
|
|
921
|
+
});
|
|
922
|
+
// Set to empty string to ensure consistent response structure
|
|
923
|
+
outputText = '';
|
|
924
|
+
}
|
|
925
|
+
// #region agent log
|
|
926
|
+
console.log('[DEBUG] Before building response:', {
|
|
927
|
+
hasOutputText: !!outputText,
|
|
928
|
+
outputTextLength: outputText?.length || 0,
|
|
929
|
+
outputTextPreview: outputText?.substring(0, 50) || null,
|
|
930
|
+
hasUsage: !!usage,
|
|
931
|
+
});
|
|
932
|
+
fetch('http://127.0.0.1:7243/ingest/0d4596d8-b8fa-4ca1-a4f4-2e6f23baa429', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'OpenRouterAdapter.ts:870', message: 'Before building response', data: { hasOutputText: !!outputText, outputTextLength: outputText?.length || 0, outputTextPreview: outputText?.substring(0, 50) || null, hasUsage: !!usage }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'run1', hypothesisId: 'E' }) }).catch(() => { });
|
|
933
|
+
// #endregion
|
|
934
|
+
// Add status and normalize response format for compatibility
|
|
935
|
+
// IMPORTANT: Preserve original output array if it exists (for reasoning artifacts)
|
|
936
|
+
// OpenRouter may return both choices (normalized) and output (original Responses API format)
|
|
937
|
+
// Use the same outputArray we determined during parsing (already has correct priority logic applied)
|
|
938
|
+
const responseWithStatus = {
|
|
939
|
+
...rawResponse,
|
|
940
|
+
status: 'completed', // Responses are completed if they succeed
|
|
941
|
+
// Use the outputArray we selected during parsing (already has correct source priority)
|
|
942
|
+
// If outputArray is empty, reconstruct from choices for compatibility
|
|
943
|
+
output: outputArray.length > 0
|
|
944
|
+
? outputArray // Use the output array we selected during parsing
|
|
945
|
+
: rawResponse?.choices ? rawResponse.choices.map((choice) => ({
|
|
946
|
+
content: choice.message?.content ? { text: choice.message.content } : null,
|
|
947
|
+
role: choice.message?.role,
|
|
948
|
+
tool_calls: choice.message?.tool_calls,
|
|
949
|
+
})) : [],
|
|
950
|
+
// Also preserve format field if present
|
|
951
|
+
format: rawResponse?.format || (outputArray.length > 0 ? 'openai-responses-v1' : undefined),
|
|
952
|
+
};
|
|
953
|
+
// #region agent log
|
|
954
|
+
console.log('[DEBUG] responseWithStatus created:', {
|
|
955
|
+
finalOutputLength: Array.isArray(responseWithStatus.output) ? responseWithStatus.output.length : 'not-array',
|
|
956
|
+
finalFormat: responseWithStatus.format,
|
|
957
|
+
usedOutputArray: outputArray.length > 0,
|
|
958
|
+
outputArrayLength: outputArray.length,
|
|
959
|
+
finalOutputItems: Array.isArray(responseWithStatus.output) && responseWithStatus.output.length > 0
|
|
960
|
+
? responseWithStatus.output.slice(0, 3).map((i) => ({ type: i?.type, keys: i ? Object.keys(i) : [] }))
|
|
961
|
+
: [],
|
|
962
|
+
});
|
|
963
|
+
// #endregion
|
|
964
|
+
// #region agent log
|
|
965
|
+
console.log('[DEBUG] responseWithStatus created:', {
|
|
966
|
+
finalOutputLength: Array.isArray(responseWithStatus.output) ? responseWithStatus.output.length : 'not-array',
|
|
967
|
+
finalFormat: responseWithStatus.format,
|
|
968
|
+
finalOutputItems: Array.isArray(responseWithStatus.output) && responseWithStatus.output.length > 0
|
|
969
|
+
? responseWithStatus.output.slice(0, 3).map((i) => ({ type: i?.type, keys: i ? Object.keys(i) : [] }))
|
|
970
|
+
: [],
|
|
971
|
+
});
|
|
972
|
+
// #endregion
|
|
973
|
+
// #region agent log
|
|
974
|
+
console.log('[DEBUG] Returning response:', {
|
|
975
|
+
hasOutputText: !!outputText,
|
|
976
|
+
outputTextLength: outputText?.length || 0,
|
|
977
|
+
outputTextPreview: outputText?.substring(0, 50) || null,
|
|
978
|
+
hasUsage: !!usage,
|
|
979
|
+
hasReasoning: !!reasoning,
|
|
980
|
+
provider: 'openrouter',
|
|
981
|
+
});
|
|
982
|
+
fetch('http://127.0.0.1:7243/ingest/0d4596d8-b8fa-4ca1-a4f4-2e6f23baa429', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location: 'OpenRouterAdapter.ts:940', message: 'Returning response', data: { hasOutputText: !!outputText, outputTextLength: outputText?.length || 0, outputTextPreview: outputText?.substring(0, 50) || null, hasUsage: !!usage, hasReasoning: !!reasoning, provider: 'openrouter' }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'run1', hypothesisId: 'E' }) }).catch(() => { });
|
|
983
|
+
// #endregion
|
|
984
|
+
// Build complete response object for ai-activities storage
|
|
985
|
+
const fullResponse = {
|
|
986
|
+
requestId,
|
|
987
|
+
provider: 'openrouter',
|
|
988
|
+
rawResponse: responseWithStatus,
|
|
989
|
+
outputText,
|
|
990
|
+
usage,
|
|
991
|
+
reasoning,
|
|
992
|
+
metadata: {
|
|
993
|
+
model: rawResponse?.model,
|
|
994
|
+
id: rawResponse?.id,
|
|
995
|
+
format: rawResponse?.format,
|
|
996
|
+
...execResult.rawMeta,
|
|
997
|
+
},
|
|
998
|
+
};
|
|
999
|
+
// Build metadata with ai-activities fields
|
|
1000
|
+
// IMPORTANT: Create a clean copy of fullResponse without circular reference
|
|
1001
|
+
// to avoid serialization issues when storing in metadata
|
|
1002
|
+
const fullResponseForActivities = {
|
|
1003
|
+
requestId: fullResponse.requestId,
|
|
1004
|
+
provider: fullResponse.provider,
|
|
1005
|
+
rawResponse: fullResponse.rawResponse,
|
|
1006
|
+
outputText: fullResponse.outputText,
|
|
1007
|
+
usage: fullResponse.usage,
|
|
1008
|
+
reasoning: fullResponse.reasoning,
|
|
1009
|
+
// Don't include metadata in the copy to avoid circular reference
|
|
1010
|
+
};
|
|
1011
|
+
const metadataWithActivities = {
|
|
1012
|
+
...fullResponse.metadata,
|
|
1013
|
+
// Include full response in metadata for ai-activities storage (without circular ref)
|
|
1014
|
+
'ai-activities-response': fullResponseForActivities,
|
|
1015
|
+
// Include request for complete audit trail
|
|
1016
|
+
'ai-activities-request': request,
|
|
1017
|
+
// Also include original raw response and exec result for complete audit trail
|
|
1018
|
+
'ai-activities-raw-response': rawResponse,
|
|
1019
|
+
'ai-activities-original-response': originalResponse,
|
|
1020
|
+
'ai-activities-exec-meta': execResult.rawMeta,
|
|
1021
|
+
};
|
|
1022
|
+
// #region agent log
|
|
1023
|
+
const logDataA = { location: 'OpenRouterAdapter.ts:1091', message: 'Building response with ai-activities metadata', data: { hasFullResponse: !!fullResponse, hasRequest: !!request, hasRawResponse: !!rawResponse, metadataKeys: Object.keys(metadataWithActivities), hasAiActivitiesResponse: !!metadataWithActivities['ai-activities-response'], hasAiActivitiesRequest: !!metadataWithActivities['ai-activities-request'], aiActivitiesKeys: Object.keys(metadataWithActivities).filter(k => k.startsWith('ai-activities')), metadataSample: Object.keys(metadataWithActivities).slice(0, 10), metadataWithActivitiesKeys: Object.keys(metadataWithActivities) }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'run1', hypothesisId: 'A' };
|
|
1024
|
+
console.log('[DEBUG OpenRouterAdapter] Building ai-activities metadata:', JSON.stringify(logDataA, null, 2));
|
|
1025
|
+
console.log('[DEBUG OpenRouterAdapter] Full metadata keys:', Object.keys(metadataWithActivities));
|
|
1026
|
+
console.log('[DEBUG OpenRouterAdapter] ai-activities keys:', Object.keys(metadataWithActivities).filter(k => k.startsWith('ai-activities')));
|
|
1027
|
+
console.log('[DEBUG OpenRouterAdapter] metadataWithActivities object:', JSON.stringify(metadataWithActivities, null, 2));
|
|
1028
|
+
fetch('http://127.0.0.1:7243/ingest/0d4596d8-b8fa-4ca1-a4f4-2e6f23baa429', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(logDataA) }).catch(() => { });
|
|
1029
|
+
// #endregion
|
|
1030
|
+
const finalResponse = {
|
|
1031
|
+
...fullResponse,
|
|
1032
|
+
metadata: metadataWithActivities,
|
|
1033
|
+
};
|
|
1034
|
+
// #region agent log
|
|
1035
|
+
const logDataB = { location: 'OpenRouterAdapter.ts:1125', message: 'Returning response from parseResponse', data: { hasMetadata: !!finalResponse.metadata, metadataKeys: finalResponse.metadata ? Object.keys(finalResponse.metadata) : [], hasAiActivitiesResponse: !!finalResponse.metadata?.['ai-activities-response'], hasAiActivitiesRequest: !!finalResponse.metadata?.['ai-activities-request'], responseKeys: Object.keys(finalResponse), metadataStringified: JSON.stringify(finalResponse.metadata) }, timestamp: Date.now(), sessionId: 'debug-session', runId: 'run1', hypothesisId: 'B' };
|
|
1036
|
+
console.log('[DEBUG OpenRouterAdapter] Returning response:', JSON.stringify(logDataB, null, 2));
|
|
1037
|
+
console.log('[DEBUG OpenRouterAdapter] Final response metadata:', JSON.stringify(finalResponse.metadata, null, 2));
|
|
1038
|
+
console.log('[DEBUG OpenRouterAdapter] Final response metadata keys:', finalResponse.metadata ? Object.keys(finalResponse.metadata) : 'NO METADATA');
|
|
1039
|
+
fetch('http://127.0.0.1:7243/ingest/0d4596d8-b8fa-4ca1-a4f4-2e6f23baa429', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(logDataB) }).catch(() => { });
|
|
1040
|
+
// #endregion
|
|
1041
|
+
return finalResponse;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Comprehensive content extraction function
|
|
1045
|
+
* Tries all possible locations and formats to extract text content
|
|
1046
|
+
* Returns the extracted text or undefined if not found
|
|
1047
|
+
*/
|
|
1048
|
+
extractContentFromResponse(rawResponse, originalResponse, execResult, outputArray) {
|
|
1049
|
+
// Strategy 1: Extract from Responses API format output array
|
|
1050
|
+
if (outputArray && outputArray.length > 0) {
|
|
1051
|
+
for (const item of outputArray) {
|
|
1052
|
+
// Direct text item
|
|
1053
|
+
if (item.type === 'text' && item.content) {
|
|
1054
|
+
if (typeof item.content === 'string' && item.content.trim()) {
|
|
1055
|
+
return item.content;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
// Message item with content array
|
|
1059
|
+
if (item.type === 'message' && item.content) {
|
|
1060
|
+
if (Array.isArray(item.content)) {
|
|
1061
|
+
for (const contentItem of item.content) {
|
|
1062
|
+
// Text content items
|
|
1063
|
+
if (contentItem.type === 'text' && contentItem.text && typeof contentItem.text === 'string') {
|
|
1064
|
+
return contentItem.text;
|
|
1065
|
+
}
|
|
1066
|
+
if (contentItem.type === 'output_text' && contentItem.text && typeof contentItem.text === 'string') {
|
|
1067
|
+
return contentItem.text;
|
|
1068
|
+
}
|
|
1069
|
+
if (contentItem.type === 'text' && contentItem.content && typeof contentItem.content === 'string') {
|
|
1070
|
+
return contentItem.content;
|
|
1071
|
+
}
|
|
1072
|
+
// String content without type
|
|
1073
|
+
if (!contentItem.type && typeof contentItem === 'string' && contentItem.trim()) {
|
|
1074
|
+
return contentItem;
|
|
1075
|
+
}
|
|
1076
|
+
if (!contentItem.type && contentItem.text && typeof contentItem.text === 'string' && contentItem.text.trim()) {
|
|
1077
|
+
return contentItem.text;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
else if (typeof item.content === 'string' && item.content.trim()) {
|
|
1082
|
+
return item.content;
|
|
1083
|
+
}
|
|
1084
|
+
else if (item.content && typeof item.content === 'object' && item.content.text && typeof item.content.text === 'string' && item.content.text.trim()) {
|
|
1085
|
+
return item.content.text;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
// Strategy 2: Extract from Chat Completions format choices
|
|
1091
|
+
const choicesSources = [
|
|
1092
|
+
rawResponse?.choices,
|
|
1093
|
+
execResult.rawMeta?.originalResponse?.choices,
|
|
1094
|
+
execResult.rawMeta?.rawResponse?.choices,
|
|
1095
|
+
originalResponse?.choices,
|
|
1096
|
+
];
|
|
1097
|
+
for (const choices of choicesSources) {
|
|
1098
|
+
if (Array.isArray(choices) && choices.length > 0) {
|
|
1099
|
+
for (const choice of choices) {
|
|
1100
|
+
if (choice.message) {
|
|
1101
|
+
const content = choice.message.content;
|
|
1102
|
+
// String content
|
|
1103
|
+
if (typeof content === 'string' && content.trim()) {
|
|
1104
|
+
return content;
|
|
1105
|
+
}
|
|
1106
|
+
// Array content (multimodal - extract text parts)
|
|
1107
|
+
if (Array.isArray(content)) {
|
|
1108
|
+
const textParts = [];
|
|
1109
|
+
for (const part of content) {
|
|
1110
|
+
if (typeof part === 'string' && part.trim()) {
|
|
1111
|
+
textParts.push(part);
|
|
1112
|
+
}
|
|
1113
|
+
else if (part && typeof part === 'object') {
|
|
1114
|
+
if (part.type === 'text' && part.text && typeof part.text === 'string' && part.text.trim()) {
|
|
1115
|
+
textParts.push(part.text);
|
|
1116
|
+
}
|
|
1117
|
+
else if (part.text && typeof part.text === 'string' && part.text.trim()) {
|
|
1118
|
+
textParts.push(part.text);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
if (textParts.length > 0) {
|
|
1123
|
+
return textParts.join('\n');
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
// Object content (try to extract text)
|
|
1127
|
+
if (content && typeof content === 'object' && !Array.isArray(content)) {
|
|
1128
|
+
if (content.text && typeof content.text === 'string' && content.text.trim()) {
|
|
1129
|
+
return content.text;
|
|
1130
|
+
}
|
|
1131
|
+
// Try to stringify if it's a simple object
|
|
1132
|
+
try {
|
|
1133
|
+
const str = String(content);
|
|
1134
|
+
if (str && str !== '[object Object]' && str.trim()) {
|
|
1135
|
+
return str;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
catch {
|
|
1139
|
+
// Ignore
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
// Strategy 3: Direct text fields
|
|
1147
|
+
const textFields = [
|
|
1148
|
+
rawResponse?.output_text,
|
|
1149
|
+
rawResponse?.text,
|
|
1150
|
+
execResult.rawMeta?.originalResponse?.output_text,
|
|
1151
|
+
execResult.rawMeta?.originalResponse?.text,
|
|
1152
|
+
originalResponse?.output_text,
|
|
1153
|
+
originalResponse?.text,
|
|
1154
|
+
];
|
|
1155
|
+
for (const text of textFields) {
|
|
1156
|
+
if (typeof text === 'string' && text.trim()) {
|
|
1157
|
+
return text;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
// Strategy 4: Check rawResponse.output for text (non-array case)
|
|
1161
|
+
if (rawResponse?.output && typeof rawResponse.output === 'string' && rawResponse.output.trim()) {
|
|
1162
|
+
return rawResponse.output;
|
|
1163
|
+
}
|
|
1164
|
+
if (originalResponse?.output && typeof originalResponse.output === 'string' && originalResponse.output.trim()) {
|
|
1165
|
+
return originalResponse.output;
|
|
1166
|
+
}
|
|
1167
|
+
return undefined;
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Build unified reasoning response from request info and parsed artifacts
|
|
1171
|
+
* Implements deterministic downgrade rules based on what was actually returned
|
|
1172
|
+
* This is the ONLY source of truth for reasoning response structure
|
|
1173
|
+
*/
|
|
1174
|
+
buildReasoningResponse(reasoningRequest, parsed) {
|
|
1175
|
+
const { encrypted: encryptedArtifacts, summaryText, traceText } = parsed;
|
|
1176
|
+
// Default values
|
|
1177
|
+
const requested = reasoningRequest?.requested || { effort: 'none', visibility: 'none' };
|
|
1178
|
+
const onUnsupported = reasoningRequest?.requested?.onUnsupported || 'downgrade';
|
|
1179
|
+
const capabilities = reasoningRequest?.capabilities || {
|
|
1180
|
+
supportsEffort: false,
|
|
1181
|
+
supportsMaxTokens: false,
|
|
1182
|
+
supportsSummary: false,
|
|
1183
|
+
supportsTrace: false,
|
|
1184
|
+
supportsEncrypted: false,
|
|
1185
|
+
supportsReasoningDetails: false,
|
|
1186
|
+
};
|
|
1187
|
+
// Determine applied values, warnings, and errors
|
|
1188
|
+
// Note: In parseResponse phase, errors should have already been handled in buildCallSpec
|
|
1189
|
+
// But we still convert any remaining errors to warnings here for safety
|
|
1190
|
+
const { applied, warnings, errors } = reasoningRequest
|
|
1191
|
+
? this.mapReasoningToProvider(reasoningRequest.requested, capabilities)
|
|
1192
|
+
: { applied: { effort: 'none', visibility: 'none' }, warnings: [], errors: [] };
|
|
1193
|
+
// In response parsing phase, convert any remaining errors to warnings (shouldn't happen if buildCallSpec worked correctly)
|
|
1194
|
+
const allWarnings = [...warnings, ...errors.map(error => ({
|
|
1195
|
+
code: error.code,
|
|
1196
|
+
message: error.message
|
|
1197
|
+
}))];
|
|
1198
|
+
// Build artifacts based on what was actually returned
|
|
1199
|
+
const artifacts = {};
|
|
1200
|
+
if (encryptedArtifacts.length > 0) {
|
|
1201
|
+
artifacts.encrypted = encryptedArtifacts;
|
|
1202
|
+
}
|
|
1203
|
+
if (summaryText) {
|
|
1204
|
+
artifacts.summary = { text: summaryText, format: 'text/plain' };
|
|
1205
|
+
}
|
|
1206
|
+
if (traceText) {
|
|
1207
|
+
artifacts.trace = { text: traceText, format: 'text/plain' };
|
|
1208
|
+
}
|
|
1209
|
+
// DETERMINISTIC DOWNGRADE RULES: Adjust applied visibility based on what was actually returned
|
|
1210
|
+
const finalApplied = { ...applied };
|
|
1211
|
+
// Determine availability flags based on what was actually returned
|
|
1212
|
+
const supportsEncrypted = encryptedArtifacts.length > 0;
|
|
1213
|
+
const supportsSummary = !!summaryText;
|
|
1214
|
+
const supportsTrace = encryptedArtifacts.length > 0 || !!traceText; // Either encrypted OR text satisfies trace
|
|
1215
|
+
// Apply visibility based on what was actually returned
|
|
1216
|
+
if (requested.visibility === 'summary') {
|
|
1217
|
+
if (summaryText) {
|
|
1218
|
+
// Summary exists, use it
|
|
1219
|
+
finalApplied.visibility = 'summary';
|
|
1220
|
+
}
|
|
1221
|
+
else {
|
|
1222
|
+
// Summary requested but not returned
|
|
1223
|
+
if (onUnsupported === 'error') {
|
|
1224
|
+
throw new Error('Reasoning visibility=summary was requested, but no summary was returned.');
|
|
1225
|
+
}
|
|
1226
|
+
finalApplied.visibility = 'none';
|
|
1227
|
+
allWarnings.push({
|
|
1228
|
+
code: 'VISIBILITY_DOWNGRADED',
|
|
1229
|
+
message: 'Requested summary visibility but no summary returned, downgraded to none',
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
else if (requested.visibility === 'trace') {
|
|
1234
|
+
// Trace visibility is satisfied by EITHER encrypted artifacts OR readable text
|
|
1235
|
+
if (encryptedArtifacts.length > 0 || traceText) {
|
|
1236
|
+
// Trace exists (encrypted or readable), use it
|
|
1237
|
+
finalApplied.visibility = 'trace';
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
// Trace requested but not returned
|
|
1241
|
+
if (onUnsupported === 'error') {
|
|
1242
|
+
throw new Error('Reasoning visibility=trace was requested, but no trace was returned (no encrypted artifacts or trace text).');
|
|
1243
|
+
}
|
|
1244
|
+
finalApplied.visibility = 'none';
|
|
1245
|
+
allWarnings.push({
|
|
1246
|
+
code: 'VISIBILITY_DOWNGRADED',
|
|
1247
|
+
message: 'Requested trace visibility but no trace returned (no encrypted artifacts or trace text), downgraded to none',
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
// Update capabilities based on what we actually found
|
|
1252
|
+
const actualCapabilities = {
|
|
1253
|
+
...capabilities,
|
|
1254
|
+
supportsEncrypted,
|
|
1255
|
+
supportsSummary,
|
|
1256
|
+
supportsTrace,
|
|
1257
|
+
};
|
|
1258
|
+
// Log reasoning summary
|
|
1259
|
+
this.logReasoningSummary(requested, finalApplied, artifacts, allWarnings);
|
|
1260
|
+
return {
|
|
1261
|
+
requested,
|
|
1262
|
+
applied: finalApplied,
|
|
1263
|
+
artifacts,
|
|
1264
|
+
availability: actualCapabilities,
|
|
1265
|
+
warnings: allWarnings,
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* Log reasoning processing summary
|
|
1270
|
+
*/
|
|
1271
|
+
logReasoningSummary(requested, applied, artifacts, warnings) {
|
|
1272
|
+
// Only log if reasoning was actually requested
|
|
1273
|
+
if (requested.effort !== 'none' || requested.visibility !== 'none') {
|
|
1274
|
+
console.log('🧠 Reasoning Processing Summary:');
|
|
1275
|
+
console.log(` Requested: effort=${requested.effort}, visibility=${requested.visibility}`);
|
|
1276
|
+
console.log(` Applied: effort=${applied.effort}, visibility=${applied.visibility}`);
|
|
1277
|
+
if (artifacts.encrypted && artifacts.encrypted.length > 0) {
|
|
1278
|
+
console.log(` 📦 Encrypted artifacts: ${artifacts.encrypted.length} items`);
|
|
1279
|
+
artifacts.encrypted.forEach((artifact, i) => {
|
|
1280
|
+
const dataLen = artifact.data ? (typeof artifact.data === 'string' ? artifact.data.length : JSON.stringify(artifact.data).length) : 0;
|
|
1281
|
+
const dataPrefix = artifact.data && typeof artifact.data === 'string'
|
|
1282
|
+
? artifact.data.substring(0, Math.min(32, artifact.data.length))
|
|
1283
|
+
: 'N/A';
|
|
1284
|
+
console.log(` [${i}] ${artifact.format} (id: ${artifact.id || 'N/A'}, dataLen=${dataLen}, dataPrefix="${dataPrefix}")`);
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
if (warnings && warnings.length > 0) {
|
|
1288
|
+
console.log(` ⚠️ Warnings: ${warnings.length}`);
|
|
1289
|
+
warnings.forEach((warning) => {
|
|
1290
|
+
console.log(` ${warning.code}: ${warning.message}`);
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
console.log(''); // Empty line for readability
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
parseStreamChunk(input) {
|
|
1297
|
+
const { requestId, chunk } = input;
|
|
1298
|
+
const raw = chunk.raw;
|
|
1299
|
+
const events = [];
|
|
1300
|
+
// Emit provider_raw event
|
|
1301
|
+
events.push({
|
|
1302
|
+
type: 'provider_raw',
|
|
1303
|
+
requestId,
|
|
1304
|
+
provider: 'openrouter',
|
|
1305
|
+
raw,
|
|
1306
|
+
});
|
|
1307
|
+
// Handle OpenAI Responses API streaming format
|
|
1308
|
+
if (raw?.format === 'openai-responses-v1' && Array.isArray(raw.output)) {
|
|
1309
|
+
for (const item of raw.output) {
|
|
1310
|
+
if (item.type === 'text' && item.content) {
|
|
1311
|
+
events.push({
|
|
1312
|
+
type: 'output_text_delta',
|
|
1313
|
+
requestId,
|
|
1314
|
+
delta: item.content,
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
// reasoning.encrypted items are streamed but encrypted
|
|
1318
|
+
// Could potentially emit reasoning_trace_delta for encrypted items if needed
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
// Parse traditional OpenAI Chat Completions stream chunk format
|
|
1322
|
+
// OpenAI stream format: { choices: [{ delta: { content: "..." } }] }
|
|
1323
|
+
else if (raw?.choices && Array.isArray(raw.choices)) {
|
|
1324
|
+
for (const choice of raw.choices) {
|
|
1325
|
+
if (choice.delta?.content) {
|
|
1326
|
+
events.push({
|
|
1327
|
+
type: 'output_text_delta',
|
|
1328
|
+
requestId,
|
|
1329
|
+
delta: choice.delta.content,
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
return events;
|
|
1335
|
+
}
|
|
1336
|
+
finalizeStream(input) {
|
|
1337
|
+
const { requestId, collected, finalRaw } = input;
|
|
1338
|
+
// Use finalRaw if available, otherwise use last raw event
|
|
1339
|
+
const rawResponse = finalRaw || (collected.rawEvents.length > 0 ? collected.rawEvents[collected.rawEvents.length - 1] : {});
|
|
1340
|
+
// Try to extract usage from final response
|
|
1341
|
+
let usage;
|
|
1342
|
+
const finalResponse = rawResponse;
|
|
1343
|
+
// Handle both formats for usage extraction
|
|
1344
|
+
if (finalResponse?.usage) {
|
|
1345
|
+
if (finalResponse.format === 'openai-responses-v1') {
|
|
1346
|
+
usage = {
|
|
1347
|
+
inputTokens: finalResponse.usage.input_tokens,
|
|
1348
|
+
outputTokens: finalResponse.usage.output_tokens,
|
|
1349
|
+
totalTokens: finalResponse.usage.total_tokens,
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
else {
|
|
1353
|
+
// Traditional Chat Completions format
|
|
1354
|
+
usage = {
|
|
1355
|
+
inputTokens: finalResponse.usage.prompt_tokens,
|
|
1356
|
+
outputTokens: finalResponse.usage.completion_tokens,
|
|
1357
|
+
totalTokens: finalResponse.usage.total_tokens,
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
// For streaming finalize, we don't have access to original reasoning request
|
|
1362
|
+
// Create a basic reasoning response based on available data
|
|
1363
|
+
const encryptedArtifacts = [];
|
|
1364
|
+
// Extract encrypted artifacts from final response if present
|
|
1365
|
+
if (finalResponse?.format === 'openai-responses-v1' && Array.isArray(finalResponse.output)) {
|
|
1366
|
+
for (const item of finalResponse.output) {
|
|
1367
|
+
if (item.type === 'reasoning.encrypted') {
|
|
1368
|
+
encryptedArtifacts.push({
|
|
1369
|
+
format: 'openai-responses-v1',
|
|
1370
|
+
id: item.id,
|
|
1371
|
+
index: item.index,
|
|
1372
|
+
data: item.data,
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
// Build basic reasoning response (request details not available in streaming finalize)
|
|
1378
|
+
const reasoning = {
|
|
1379
|
+
requested: { effort: 'none', visibility: 'none' },
|
|
1380
|
+
applied: { effort: 'none', visibility: encryptedArtifacts.length > 0 ? 'trace' : 'none' },
|
|
1381
|
+
artifacts: encryptedArtifacts.length > 0 ? { encrypted: encryptedArtifacts } : {},
|
|
1382
|
+
availability: {
|
|
1383
|
+
supportsEffort: false,
|
|
1384
|
+
supportsSummary: false,
|
|
1385
|
+
supportsTrace: false,
|
|
1386
|
+
supportsEncrypted: encryptedArtifacts.length > 0,
|
|
1387
|
+
},
|
|
1388
|
+
};
|
|
1389
|
+
// Build complete response object for ai-activities storage
|
|
1390
|
+
const fullResponse = {
|
|
1391
|
+
requestId,
|
|
1392
|
+
provider: 'openrouter',
|
|
1393
|
+
rawResponse,
|
|
1394
|
+
outputText: collected.outputText,
|
|
1395
|
+
usage,
|
|
1396
|
+
reasoning,
|
|
1397
|
+
metadata: {
|
|
1398
|
+
model: finalResponse?.model,
|
|
1399
|
+
id: finalResponse?.id,
|
|
1400
|
+
format: finalResponse?.format, // Include format for debugging
|
|
1401
|
+
},
|
|
1402
|
+
};
|
|
1403
|
+
// IMPORTANT: Create a clean copy of fullResponse without circular reference
|
|
1404
|
+
const fullResponseForActivities = {
|
|
1405
|
+
requestId: fullResponse.requestId,
|
|
1406
|
+
provider: fullResponse.provider,
|
|
1407
|
+
rawResponse: fullResponse.rawResponse,
|
|
1408
|
+
outputText: fullResponse.outputText,
|
|
1409
|
+
usage: fullResponse.usage,
|
|
1410
|
+
reasoning: fullResponse.reasoning,
|
|
1411
|
+
// Don't include metadata in the copy to avoid circular reference
|
|
1412
|
+
};
|
|
1413
|
+
return {
|
|
1414
|
+
...fullResponse,
|
|
1415
|
+
metadata: {
|
|
1416
|
+
...fullResponse.metadata,
|
|
1417
|
+
// Include full response in metadata for ai-activities storage (without circular ref)
|
|
1418
|
+
'ai-activities-response': fullResponseForActivities,
|
|
1419
|
+
// Include request for complete audit trail
|
|
1420
|
+
'ai-activities-request': input.request,
|
|
1421
|
+
'ai-activities-raw-response': rawResponse,
|
|
1422
|
+
'ai-activities-collected-events': collected.rawEvents,
|
|
1423
|
+
},
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
parseBatchItem(input) {
|
|
1427
|
+
const { requestId, item } = input;
|
|
1428
|
+
if (item.error) {
|
|
1429
|
+
return {
|
|
1430
|
+
requestId,
|
|
1431
|
+
error: item.error,
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
// Parse OpenAI-compatible batch item response directly (no ai-io-normalizer)
|
|
1435
|
+
const rawResponse = item.rawResponse;
|
|
1436
|
+
let outputText;
|
|
1437
|
+
if (rawResponse?.choices && Array.isArray(rawResponse.choices) && rawResponse.choices.length > 0) {
|
|
1438
|
+
const firstChoice = rawResponse.choices[0];
|
|
1439
|
+
if (firstChoice.message?.content) {
|
|
1440
|
+
outputText = firstChoice.message.content;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return {
|
|
1444
|
+
requestId,
|
|
1445
|
+
rawResponse,
|
|
1446
|
+
outputText,
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
}
|