@aj-archipelago/cortex 1.4.21 → 1.4.22

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/config.js CHANGED
@@ -455,6 +455,15 @@ var config = convict({
455
455
  "Content-Type": "application/json"
456
456
  },
457
457
  },
458
+ "replicate-qwen-image-edit-2511": {
459
+ "type": "REPLICATE-API",
460
+ "url": "https://api.replicate.com/v1/models/qwen/qwen-image-edit-2511/predictions",
461
+ "headers": {
462
+ "Prefer": "wait",
463
+ "Authorization": "Token {{REPLICATE_API_KEY}}",
464
+ "Content-Type": "application/json"
465
+ },
466
+ },
458
467
  "replicate-seedream-4": {
459
468
  "type": "REPLICATE-API",
460
469
  "url": "https://api.replicate.com/v1/models/bytedance/seedream-4/predictions",
@@ -464,6 +473,15 @@ var config = convict({
464
473
  "Content-Type": "application/json"
465
474
  },
466
475
  },
476
+ "replicate-flux-2-pro": {
477
+ "type": "REPLICATE-API",
478
+ "url": "https://api.replicate.com/v1/models/black-forest-labs/flux-2-pro/predictions",
479
+ "headers": {
480
+ "Prefer": "wait",
481
+ "Authorization": "Token {{REPLICATE_API_KEY}}",
482
+ "Content-Type": "application/json"
483
+ },
484
+ },
467
485
  "azure-video-translate": {
468
486
  "type": "AZURE-VIDEO-TRANSLATE",
469
487
  "url": "https://eastus.api.cognitive.microsoft.com/videotranslation",
@@ -6,6 +6,7 @@ import { setupCache } from 'axios-cache-interceptor';
6
6
  import Redis from 'ioredis';
7
7
  import logger from './logger.js';
8
8
  import { v4 as uuidv4 } from 'uuid';
9
+ import { sanitizeBase64 } from './util.js';
9
10
 
10
11
  const connectionString = config.get('storageConnectionString');
11
12
 
@@ -229,10 +230,10 @@ const requestWithMonitor = async (endpoint, url, data, axiosConfigObj) => {
229
230
  let response;
230
231
  try {
231
232
  if (axiosConfigObj?.method == 'GET'){
232
- logger.debug(`Getting ${url} with data: ${JSON.stringify(data)}`);
233
+ logger.debug(`Getting ${url} with data: ${JSON.stringify(sanitizeBase64(data))}`);
233
234
  response = await cortexAxios.get(url, axiosConfigObj);
234
235
  } else {
235
- logger.debug(`Posting ${url} with data: ${JSON.stringify(data)}`);
236
+ logger.debug(`Posting ${url} with data: ${JSON.stringify(sanitizeBase64(data))}`);
236
237
  response = await cortexAxios.post(url, data, axiosConfigObj);
237
238
  }
238
239
  } catch (error) {
package/lib/util.js CHANGED
@@ -294,6 +294,75 @@ function removeImageAndFileFromMessage(message) {
294
294
  return modifiedMessage;
295
295
  }
296
296
 
297
+ /**
298
+ * Recursively sanitizes base64 data in objects/arrays to prevent logging large base64 strings
299
+ * Replaces base64 data with a placeholder string
300
+ */
301
+ function sanitizeBase64(obj) {
302
+ if (obj === null || obj === undefined) {
303
+ return obj;
304
+ }
305
+
306
+ // Handle strings - check for base64 data URLs or long base64 strings
307
+ if (typeof obj === 'string') {
308
+ // Check if it's a data URL with base64
309
+ if (obj.startsWith('data:') && obj.includes('base64,')) {
310
+ return '* base64 data truncated for log *';
311
+ }
312
+ // Check if it's a long base64 string (likely base64 if > 100 chars and matches base64 pattern)
313
+ if (obj.length > 100 && /^[A-Za-z0-9+/=]+$/.test(obj) && obj.length % 4 === 0) {
314
+ return '* base64 data truncated for log *';
315
+ }
316
+ return obj;
317
+ }
318
+
319
+ // Handle arrays
320
+ if (Array.isArray(obj)) {
321
+ return obj.map(item => sanitizeBase64(item));
322
+ }
323
+
324
+ // Handle objects
325
+ if (typeof obj === 'object') {
326
+ const sanitized = {};
327
+ for (const [key, value] of Object.entries(obj)) {
328
+ // Special handling for known base64 fields
329
+ if (key === 'data' && typeof value === 'string' && value.length > 50) {
330
+ // Check if it looks like base64
331
+ if (/^[A-Za-z0-9+/=]+$/.test(value) && value.length % 4 === 0) {
332
+ sanitized[key] = '* base64 data truncated for log *';
333
+ continue;
334
+ }
335
+ }
336
+ // Handle image_url.url with base64
337
+ if (key === 'url' && typeof value === 'string' && value.startsWith('data:') && value.includes('base64,')) {
338
+ sanitized[key] = '* base64 data truncated for log *';
339
+ continue;
340
+ }
341
+ // Handle source.data (Claude format)
342
+ if (key === 'source' && typeof value === 'object' && value?.type === 'base64' && value?.data) {
343
+ sanitized[key] = {
344
+ ...value,
345
+ data: '* base64 data truncated for log *'
346
+ };
347
+ continue;
348
+ }
349
+ // Handle inlineData.data (Gemini format)
350
+ if (key === 'inlineData' && typeof value === 'object' && value?.data) {
351
+ sanitized[key] = {
352
+ ...value,
353
+ data: '* base64 data truncated for log *'
354
+ };
355
+ continue;
356
+ }
357
+ // Recursively sanitize nested objects
358
+ sanitized[key] = sanitizeBase64(value);
359
+ }
360
+ return sanitized;
361
+ }
362
+
363
+ return obj;
364
+ }
365
+
297
366
  export {
298
367
  getUniqueId,
299
368
  getSearchResultId,
@@ -303,5 +372,6 @@ export {
303
372
  chatArgsHasType,
304
373
  convertSrtToText,
305
374
  alignSubtitles,
306
- removeOldImageAndFileContent
375
+ removeOldImageAndFileContent,
376
+ sanitizeBase64
307
377
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.4.21",
3
+ "version": "1.4.22",
4
4
  "description": "Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.",
5
5
  "private": false,
6
6
  "repository": {
@@ -13,7 +13,13 @@ export default {
13
13
  output_format: "webp",
14
14
  output_quality: 80,
15
15
  steps: 4,
16
- input_image: "", // URL to input image for models that support it
17
- input_image_2: "", // URL to second input image for models that support it
16
+ input_image: "", // URL to a single input image (primary field for models that support image input)
17
+ input_image_1: "", // URL to the first input image when providing multiple input images
18
+ input_image_2: "", // URL to the second input image when providing multiple input images
19
+ input_image_3: "", // URL to the third input image when providing multiple input images
20
+ input_images: { type: "array", items: { type: "string" } }, // Array of input image URLs (alternative to input_image_*, max 8 for flux-2-pro)
21
+ // Flux 2 Pro specific parameters
22
+ resolution: "1 MP", // Options: "match_input_image", "0.5 MP", "1 MP", "2 MP", "4 MP" (flux-2-pro only)
23
+ seed: { type: "integer" }, // Optional seed for reproducible results
18
24
  },
19
25
  };
@@ -3,7 +3,7 @@ export default {
3
3
 
4
4
  enableDuplicateRequests: false,
5
5
  inputParameters: {
6
- model: "replicate-qwen-image", // Options: "replicate-qwen-image" or "replicate-qwen-image-edit-plus"
6
+ model: "replicate-qwen-image", // Options: "replicate-qwen-image", "replicate-qwen-image-edit-plus", or "replicate-qwen-image-edit-2511"
7
7
  negativePrompt: "",
8
8
  width: 1024,
9
9
  height: 1024,
@@ -93,9 +93,9 @@ export default {
93
93
  let model = "replicate-seedream-4";
94
94
  let prompt = args.detailedInstructions || "";
95
95
 
96
- // If we have input images, use the flux-kontext-max model
96
+ // If we have input images, use the qwen-image-edit-2511 model
97
97
  if (args.inputImages && Array.isArray(args.inputImages) && args.inputImages.length > 0) {
98
- model = "replicate-qwen-image-edit-plus";
98
+ model = "replicate-qwen-image-edit-2511";
99
99
  }
100
100
 
101
101
  pathwayResolver.tool = JSON.stringify({ toolUsed: "image" });
@@ -139,8 +139,8 @@ export default {
139
139
  params.input_image_3 = resolvedInputImages[2];
140
140
  }
141
141
 
142
- // Set default aspectRatio for qwen-image-edit-plus model
143
- if (model === "replicate-qwen-image-edit-plus") {
142
+ // Set default aspectRatio for qwen-image-edit-2511 model
143
+ if (model === "replicate-qwen-image-edit-2511") {
144
144
  params.aspectRatio = "match_input_image";
145
145
  }
146
146
 
@@ -4,6 +4,7 @@ import { requestState } from '../requestState.js';
4
4
  import { addCitationsToResolver } from '../../lib/pathwayTools.js';
5
5
  import CortexResponse from '../../lib/cortexResponse.js';
6
6
  import axios from 'axios';
7
+ import { sanitizeBase64 } from "../../lib/util.js";
7
8
 
8
9
  async function convertContentItem(item, maxImageSize, plugin) {
9
10
  let imageUrl = "";
@@ -576,12 +577,7 @@ class Claude3VertexPlugin extends OpenAIVisionPlugin {
576
577
  let totalUnits;
577
578
  messages.forEach((message, index) => {
578
579
  const content = Array.isArray(message.content)
579
- ? message.content.map((item) => {
580
- if (item.source && item.source.type === 'base64') {
581
- item.source.data = '* base64 data truncated for log *';
582
- }
583
- return JSON.stringify(item);
584
- }).join(", ")
580
+ ? message.content.map((item) => JSON.stringify(sanitizeBase64(item))).join(", ")
585
581
  : message.content;
586
582
  const { length, units } = this.getLength(content);
587
583
  const preview = this.shortenContent(content);
@@ -1,6 +1,7 @@
1
1
  import Claude3VertexPlugin from "./claude3VertexPlugin.js";
2
2
  import logger from "../../lib/logger.js";
3
3
  import axios from 'axios';
4
+ import { sanitizeBase64 } from "../../lib/util.js";
4
5
 
5
6
  // Claude 4 default maximum file size limit (30MB) for both images and PDFs
6
7
  const CLAUDE4_DEFAULT_MAX_FILE_SIZE = 30 * 1024 * 1024; // 30MB
@@ -475,13 +476,10 @@ class Claude4VertexPlugin extends Claude3VertexPlugin {
475
476
  messages.forEach((message, index) => {
476
477
  const content = Array.isArray(message.content)
477
478
  ? message.content.map((item) => {
478
- if (item.source && item.source.type === 'base64') {
479
- item.source.data = '* base64 data truncated for log *';
480
- }
481
479
  if (item.type === 'document') {
482
- return `{type: document, source: ${JSON.stringify(item.source)}}`;
480
+ return `{type: document, source: ${JSON.stringify(sanitizeBase64(item.source))}}`;
483
481
  }
484
- return JSON.stringify(item);
482
+ return JSON.stringify(sanitizeBase64(item));
485
483
  }).join(", ")
486
484
  : message.content;
487
485
  const { length, units } = this.getLength(content);
@@ -500,13 +498,10 @@ class Claude4VertexPlugin extends Claude3VertexPlugin {
500
498
  const message = messages[0];
501
499
  const content = Array.isArray(message.content)
502
500
  ? message.content.map((item) => {
503
- if (item.source && item.source.type === 'base64') {
504
- item.source.data = '* base64 data truncated for log *';
505
- }
506
501
  if (item.type === 'document') {
507
- return `{type: document, source: ${JSON.stringify(item.source)}}`;
502
+ return `{type: document, source: ${JSON.stringify(sanitizeBase64(item.source))}}`;
508
503
  }
509
- return JSON.stringify(item);
504
+ return JSON.stringify(sanitizeBase64(item));
510
505
  }).join(", ")
511
506
  : message.content;
512
507
  const { length, units } = this.getLength(content);
@@ -24,8 +24,6 @@ class Gemini3ReasoningVisionPlugin extends Gemini3ImagePlugin {
24
24
  } else {
25
25
  // Fallback: use documented dummy signature to prevent 400 errors
26
26
  // This allows the request to proceed but may affect reasoning quality
27
- const toolName = toolCall?.function?.name || 'unknown';
28
- logger.warn(`Missing thoughtSignature for tool "${toolName}"; using fallback. This may indicate thoughtSignatures were lost during history persistence.`);
29
27
  part.thoughtSignature = "skip_thought_signature_validator";
30
28
  }
31
29
  return part;
@@ -4,7 +4,7 @@
4
4
 
5
5
  import OpenAIVisionPlugin from './openAiVisionPlugin.js';
6
6
  import logger from '../../lib/logger.js';
7
- import { extractCitationTitle } from '../../lib/util.js';
7
+ import { extractCitationTitle, sanitizeBase64 } from '../../lib/util.js';
8
8
  import CortexResponse from '../../lib/cortexResponse.js';
9
9
  import { requestState } from '../requestState.js';
10
10
  import { addCitationsToResolver } from '../../lib/pathwayTools.js';
@@ -37,15 +37,7 @@ class GrokResponsesPlugin extends OpenAIVisionPlugin {
37
37
  let totalLength = 0;
38
38
  let totalUnits;
39
39
  messages.forEach((message, index) => {
40
- const content = message.content === undefined ? JSON.stringify(message) : (Array.isArray(message.content) ? message.content.map(item => {
41
- if (item.type === 'image_url' && item.image_url?.url?.startsWith('data:')) {
42
- return JSON.stringify({
43
- type: 'image_url',
44
- image_url: { url: '* base64 data truncated for log *' }
45
- });
46
- }
47
- return JSON.stringify(item);
48
- }).join(', ') : message.content);
40
+ const content = message.content === undefined ? JSON.stringify(sanitizeBase64(message)) : (Array.isArray(message.content) ? message.content.map(item => JSON.stringify(sanitizeBase64(item))).join(', ') : message.content);
49
41
  const { length, units } = this.getLength(content);
50
42
  const displayContent = this.shortenContent(content);
51
43
 
@@ -62,15 +54,7 @@ class GrokResponsesPlugin extends OpenAIVisionPlugin {
62
54
  logger.info(`[grok responses request contained ${totalLength} ${totalUnits}]`);
63
55
  } else if (messages && messages.length === 1) {
64
56
  const message = messages[0];
65
- const content = Array.isArray(message.content) ? message.content.map(item => {
66
- if (item.type === 'image_url' && item.image_url?.url?.startsWith('data:')) {
67
- return JSON.stringify({
68
- type: 'image_url',
69
- image_url: { url: '* base64 data truncated for log *' }
70
- });
71
- }
72
- return JSON.stringify(item);
73
- }).join(', ') : message.content;
57
+ const content = Array.isArray(message.content) ? message.content.map(item => JSON.stringify(sanitizeBase64(item))).join(', ') : message.content;
74
58
  const { length, units } = this.getLength(content);
75
59
  logger.info(`[grok responses request sent containing ${length} ${units}]`);
76
60
  logger.verbose(`${this.shortenContent(content)}`);
@@ -1,5 +1,6 @@
1
1
  import OpenAIVisionPlugin from './openAiVisionPlugin.js';
2
2
  import logger from '../../lib/logger.js';
3
+ import { sanitizeBase64 } from '../../lib/util.js';
3
4
  import { extractCitationTitle } from '../../lib/util.js';
4
5
  import CortexResponse from '../../lib/cortexResponse.js';
5
6
 
@@ -28,15 +29,7 @@ class GrokVisionPlugin extends OpenAIVisionPlugin {
28
29
  let totalUnits;
29
30
  messages.forEach((message, index) => {
30
31
  //message.content string or array
31
- const content = message.content === undefined ? JSON.stringify(message) : (Array.isArray(message.content) ? message.content.map(item => {
32
- if (item.type === 'image_url' && item.image_url?.url?.startsWith('data:')) {
33
- return JSON.stringify({
34
- type: 'image_url',
35
- image_url: { url: '* base64 data truncated for log *' }
36
- });
37
- }
38
- return JSON.stringify(item);
39
- }).join(', ') : message.content);
32
+ const content = message.content === undefined ? JSON.stringify(sanitizeBase64(message)) : (Array.isArray(message.content) ? message.content.map(item => JSON.stringify(sanitizeBase64(item))).join(', ') : message.content);
40
33
  const { length, units } = this.getLength(content);
41
34
  const displayContent = this.shortenContent(content);
42
35
 
@@ -54,15 +47,7 @@ class GrokVisionPlugin extends OpenAIVisionPlugin {
54
47
  logger.info(`[grok request contained ${totalLength} ${totalUnits}]`);
55
48
  } else {
56
49
  const message = messages[0];
57
- const content = Array.isArray(message.content) ? message.content.map(item => {
58
- if (item.type === 'image_url' && item.image_url?.url?.startsWith('data:')) {
59
- return JSON.stringify({
60
- type: 'image_url',
61
- image_url: { url: '* base64 data truncated for log *' }
62
- });
63
- }
64
- return JSON.stringify(item);
65
- }).join(', ') : message.content;
50
+ const content = Array.isArray(message.content) ? message.content.map(item => JSON.stringify(sanitizeBase64(item))).join(', ') : message.content;
66
51
  const { length, units } = this.getLength(content);
67
52
  logger.info(`[grok request sent containing ${length} ${units}]`);
68
53
  logger.verbose(`${this.shortenContent(content)}`);
@@ -594,6 +594,9 @@ class ModelPlugin {
594
594
  if (error.response) {
595
595
  logger.error(`Response status: ${error.response.status}`);
596
596
  logger.error(`Response headers: ${JSON.stringify(error.response.headers)}`);
597
+ if (error.response.data) {
598
+ logger.error(`Response data: ${JSON.stringify(error.response.data)}`);
599
+ }
597
600
  }
598
601
  if (error.data) {
599
602
  logger.error(`Additional error data: ${JSON.stringify(error.data)}`);
@@ -3,6 +3,7 @@ import logger from '../../lib/logger.js';
3
3
  import { requestState } from '../requestState.js';
4
4
  import { addCitationsToResolver } from '../../lib/pathwayTools.js';
5
5
  import CortexResponse from '../../lib/cortexResponse.js';
6
+ import { sanitizeBase64 } from '../../lib/util.js';
6
7
  function safeJsonParse(content) {
7
8
  try {
8
9
  const parsedContent = JSON.parse(content);
@@ -158,15 +159,7 @@ class OpenAIVisionPlugin extends OpenAIChatPlugin {
158
159
  let totalUnits;
159
160
  messages.forEach((message, index) => {
160
161
  //message.content string or array
161
- const content = message.content === undefined ? JSON.stringify(message) : (Array.isArray(message.content) ? message.content.map(item => {
162
- if (item.type === 'image_url' && item.image_url?.url?.startsWith('data:')) {
163
- return JSON.stringify({
164
- type: 'image_url',
165
- image_url: { url: '* base64 data truncated for log *' }
166
- });
167
- }
168
- return JSON.stringify(item);
169
- }).join(', ') : message.content);
162
+ const content = message.content === undefined ? JSON.stringify(sanitizeBase64(message)) : (Array.isArray(message.content) ? message.content.map(item => JSON.stringify(sanitizeBase64(item))).join(', ') : message.content);
170
163
  const { length, units } = this.getLength(content);
171
164
  const displayContent = this.shortenContent(content);
172
165
 
@@ -184,15 +177,7 @@ class OpenAIVisionPlugin extends OpenAIChatPlugin {
184
177
  logger.info(`[chat request contained ${totalLength} ${totalUnits}]`);
185
178
  } else {
186
179
  const message = messages[0];
187
- const content = Array.isArray(message.content) ? message.content.map(item => {
188
- if (item.type === 'image_url' && item.image_url?.url?.startsWith('data:')) {
189
- return JSON.stringify({
190
- type: 'image_url',
191
- image_url: { url: '* base64 data truncated for log *' }
192
- });
193
- }
194
- return JSON.stringify(item);
195
- }).join(', ') : message.content;
180
+ const content = Array.isArray(message.content) ? message.content.map(item => JSON.stringify(sanitizeBase64(item))).join(', ') : message.content;
196
181
  const { length, units } = this.getLength(content);
197
182
  logger.info(`[request sent containing ${length} ${units}]`);
198
183
  logger.verbose(`${this.shortenContent(content)}`);
@@ -5,6 +5,64 @@ import logger from "../../lib/logger.js";
5
5
  import axios from "axios";
6
6
  import mime from "mime-types";
7
7
 
8
+ // Helper function to collect images from various parameter sources
9
+ const collectImages = (candidate, accumulator) => {
10
+ if (!candidate) return;
11
+ if (Array.isArray(candidate)) {
12
+ candidate.forEach((item) => collectImages(item, accumulator));
13
+ return;
14
+ }
15
+ accumulator.push(candidate);
16
+ };
17
+
18
+ // Helper function to normalize image entries to strings
19
+ const normalizeImageEntry = (entry) => {
20
+ if (!entry) return null;
21
+ if (typeof entry === "string") {
22
+ return entry;
23
+ }
24
+ if (typeof entry === "object") {
25
+ if (Array.isArray(entry)) {
26
+ return null;
27
+ }
28
+ if (entry.value) {
29
+ return entry.value;
30
+ }
31
+ if (entry.url) {
32
+ return entry.url;
33
+ }
34
+ if (entry.path) {
35
+ return entry.path;
36
+ }
37
+ }
38
+ return null;
39
+ };
40
+
41
+ // Helper function to omit undefined/null values from an object
42
+ const omitUndefined = (obj) =>
43
+ Object.fromEntries(
44
+ Object.entries(obj).filter(([, value]) => value !== undefined && value !== null),
45
+ );
46
+
47
+ // Helper function to collect and normalize images from combined parameters
48
+ const collectNormalizedImages = (combinedParameters, additionalFields = []) => {
49
+ const imageCandidates = [];
50
+ const defaultFields = [
51
+ 'image', 'images', 'input_image', 'input_images',
52
+ 'input_image_1', 'input_image_2', 'input_image_3',
53
+ 'image_1', 'image_2'
54
+ ];
55
+ const allFields = [...defaultFields, ...additionalFields];
56
+
57
+ allFields.forEach(field => {
58
+ collectImages(combinedParameters[field], imageCandidates);
59
+ });
60
+
61
+ return imageCandidates
62
+ .map((candidate) => normalizeImageEntry(candidate))
63
+ .filter((candidate) => candidate && typeof candidate === 'string');
64
+ };
65
+
8
66
  class ReplicateApiPlugin extends ModelPlugin {
9
67
  constructor(pathway, model) {
10
68
  super(pathway, model);
@@ -139,67 +197,55 @@ class ReplicateApiPlugin extends ModelPlugin {
139
197
  const goFast = combinedParameters.go_fast ?? combinedParameters.goFast ?? true;
140
198
  const disableSafetyChecker = combinedParameters.disable_safety_checker ?? combinedParameters.disableSafetyChecker ?? false;
141
199
 
142
- const collectImages = (candidate, accumulator) => {
143
- if (!candidate) return;
144
- if (Array.isArray(candidate)) {
145
- candidate.forEach((item) => collectImages(item, accumulator));
146
- return;
147
- }
148
- accumulator.push(candidate);
149
- };
200
+ const normalizedImages = collectNormalizedImages(combinedParameters);
150
201
 
151
- const imageCandidates = [];
152
- collectImages(combinedParameters.image, imageCandidates);
153
- collectImages(combinedParameters.images, imageCandidates);
154
- collectImages(combinedParameters.input_image, imageCandidates);
155
- collectImages(combinedParameters.input_images, imageCandidates);
156
- collectImages(combinedParameters.input_image_1, imageCandidates);
157
- collectImages(combinedParameters.input_image_2, imageCandidates);
158
- collectImages(combinedParameters.input_image_3, imageCandidates);
159
- collectImages(combinedParameters.image_1, imageCandidates);
160
- collectImages(combinedParameters.image_2, imageCandidates);
161
-
162
- const normalizeImageEntry = (entry) => {
163
- if (!entry) return null;
164
- if (typeof entry === "string") {
165
- return entry; // Return the URL string directly
166
- }
167
- if (typeof entry === "object") {
168
- if (Array.isArray(entry)) {
169
- return null;
170
- }
171
- if (entry.value) {
172
- return entry.value; // Return the value as a string
173
- }
174
- if (entry.url) {
175
- return entry.url; // Return the URL as a string
176
- }
177
- if (entry.path) {
178
- return entry.path; // Return the path as a string
179
- }
180
- }
181
- return null;
202
+ const basePayload = omitUndefined({
203
+ prompt: modelPromptText,
204
+ go_fast: goFast,
205
+ aspect_ratio: aspectRatio,
206
+ output_format: outputFormat,
207
+ output_quality: outputQuality,
208
+ disable_safety_checker: disableSafetyChecker,
209
+ });
210
+
211
+ // For qwen-image-edit-plus, always include the image array if we have images
212
+ const inputPayload = {
213
+ ...basePayload,
214
+ ...(normalizedImages.length > 0 ? { image: normalizedImages } : {})
182
215
  };
183
216
 
184
- const normalizedImages = imageCandidates
185
- .map((candidate) => normalizeImageEntry(candidate))
186
- .filter((candidate) => candidate && typeof candidate === 'string');
217
+ requestParameters = {
218
+ input: inputPayload,
219
+ };
220
+ break;
221
+ }
222
+ case "replicate-qwen-image-edit-2511": {
223
+ const validRatios = ["1:1", "16:9", "9:16", "4:3", "3:4", "match_input_image"];
224
+ const validOutputFormats = ["webp", "jpg", "png"];
225
+
226
+ const aspectRatio = validRatios.includes(combinedParameters.aspect_ratio ?? combinedParameters.aspectRatio)
227
+ ? (combinedParameters.aspect_ratio ?? combinedParameters.aspectRatio)
228
+ : "match_input_image";
229
+ const outputFormat = validOutputFormats.includes(combinedParameters.output_format ?? combinedParameters.outputFormat)
230
+ ? (combinedParameters.output_format ?? combinedParameters.outputFormat)
231
+ : "webp";
232
+ const outputQuality = combinedParameters.output_quality ?? combinedParameters.outputQuality ?? 95;
233
+ const goFast = combinedParameters.go_fast ?? combinedParameters.goFast ?? true;
234
+ const disableSafetyChecker = combinedParameters.disable_safety_checker ?? combinedParameters.disableSafetyChecker ?? false;
187
235
 
188
- const omitUndefined = (obj) =>
189
- Object.fromEntries(
190
- Object.entries(obj).filter(([, value]) => value !== undefined && value !== null),
191
- );
236
+ const normalizedImages = collectNormalizedImages(combinedParameters);
192
237
 
193
238
  const basePayload = omitUndefined({
194
239
  prompt: modelPromptText,
195
240
  go_fast: goFast,
196
241
  aspect_ratio: aspectRatio,
197
242
  output_format: outputFormat,
198
- output_quality: outputQuality,
243
+ output_quality: Math.max(0, Math.min(100, outputQuality)),
199
244
  disable_safety_checker: disableSafetyChecker,
245
+ ...(Number.isInteger(combinedParameters.seed) && combinedParameters.seed > 0 ? { seed: combinedParameters.seed } : {}),
200
246
  });
201
247
 
202
- // For qwen-image-edit-plus, always include the image array if we have images
248
+ // For qwen-image-edit-2511, format images as array of strings (not objects)
203
249
  const inputPayload = {
204
250
  ...basePayload,
205
251
  ...(normalizedImages.length > 0 ? { image: normalizedImages } : {})
@@ -280,58 +326,7 @@ class ReplicateApiPlugin extends ModelPlugin {
280
326
  const validRatios = ["1:1", "4:3", "3:4", "16:9", "9:16", "match_input_image"];
281
327
  const validSequentialModes = ["disabled", "auto"];
282
328
 
283
- // Collect input images from multiple parameter sources (same pattern as qwen-image-edit-plus)
284
- const collectImages = (candidate, accumulator) => {
285
- if (!candidate) return;
286
- if (Array.isArray(candidate)) {
287
- candidate.forEach((item) => collectImages(item, accumulator));
288
- return;
289
- }
290
- accumulator.push(candidate);
291
- };
292
-
293
- const imageCandidates = [];
294
- collectImages(combinedParameters.image, imageCandidates);
295
- collectImages(combinedParameters.images, imageCandidates);
296
- collectImages(combinedParameters.input_image, imageCandidates);
297
- collectImages(combinedParameters.input_images, imageCandidates);
298
- collectImages(combinedParameters.input_image_1, imageCandidates);
299
- collectImages(combinedParameters.input_image_2, imageCandidates);
300
- collectImages(combinedParameters.input_image_3, imageCandidates);
301
- collectImages(combinedParameters.image_1, imageCandidates);
302
- collectImages(combinedParameters.image_2, imageCandidates);
303
- collectImages(combinedParameters.imageInput, imageCandidates);
304
-
305
- const normalizeImageEntry = (entry) => {
306
- if (!entry) return null;
307
- if (typeof entry === "string") {
308
- return entry; // Return the URL string directly
309
- }
310
- if (typeof entry === "object") {
311
- if (Array.isArray(entry)) {
312
- return null;
313
- }
314
- if (entry.value) {
315
- return entry.value; // Return the value as a string
316
- }
317
- if (entry.url) {
318
- return entry.url; // Return the URL as a string
319
- }
320
- if (entry.path) {
321
- return entry.path; // Return the path as a string
322
- }
323
- }
324
- return null;
325
- };
326
-
327
- const normalizedImages = imageCandidates
328
- .map((candidate) => normalizeImageEntry(candidate))
329
- .filter((candidate) => candidate && typeof candidate === 'string');
330
-
331
- const omitUndefined = (obj) =>
332
- Object.fromEntries(
333
- Object.entries(obj).filter(([, value]) => value !== undefined && value !== null),
334
- );
329
+ const normalizedImages = collectNormalizedImages(combinedParameters, ['imageInput']);
335
330
 
336
331
  const basePayload = omitUndefined({
337
332
  prompt: modelPromptText,
@@ -341,7 +336,7 @@ class ReplicateApiPlugin extends ModelPlugin {
341
336
  max_images: combinedParameters.maxImages || combinedParameters.numberResults || 1,
342
337
  aspect_ratio: validRatios.includes(combinedParameters.aspectRatio) ? combinedParameters.aspectRatio : "4:3",
343
338
  sequential_image_generation: validSequentialModes.includes(combinedParameters.sequentialImageGeneration) ? combinedParameters.sequentialImageGeneration : "disabled",
344
- ...(combinedParameters.seed && Number.isInteger(combinedParameters.seed) && combinedParameters.seed > 0 ? { seed: combinedParameters.seed } : {}),
339
+ ...(Number.isInteger(combinedParameters.seed) && combinedParameters.seed > 0 ? { seed: combinedParameters.seed } : {}),
345
340
  });
346
341
 
347
342
  // For seedream-4, include the image_input array if we have images
@@ -350,6 +345,74 @@ class ReplicateApiPlugin extends ModelPlugin {
350
345
  ...(normalizedImages.length > 0 ? { image_input: normalizedImages } : {})
351
346
  };
352
347
 
348
+ requestParameters = {
349
+ input: inputPayload,
350
+ };
351
+ break;
352
+ }
353
+ case "replicate-flux-2-pro": {
354
+ const validResolutions = ["match_input_image", "0.5 MP", "1 MP", "2 MP", "4 MP"];
355
+ const validRatios = [
356
+ "match_input_image",
357
+ "custom",
358
+ "1:1",
359
+ "16:9",
360
+ "3:2",
361
+ "2:3",
362
+ "4:5",
363
+ "5:4",
364
+ "9:16",
365
+ "3:4",
366
+ "4:3"
367
+ ];
368
+ const validOutputFormats = ["webp", "jpg", "png"];
369
+
370
+ const normalizedImages = collectNormalizedImages(combinedParameters).slice(0, 8); // Maximum 8 images
371
+
372
+ const aspectRatio = validRatios.includes(combinedParameters.aspect_ratio ?? combinedParameters.aspectRatio)
373
+ ? (combinedParameters.aspect_ratio ?? combinedParameters.aspectRatio)
374
+ : "1:1";
375
+
376
+ const resolution = validResolutions.includes(combinedParameters.resolution)
377
+ ? combinedParameters.resolution
378
+ : "1 MP";
379
+
380
+ const outputFormat = validOutputFormats.includes(combinedParameters.output_format ?? combinedParameters.outputFormat)
381
+ ? (combinedParameters.output_format ?? combinedParameters.outputFormat)
382
+ : "webp";
383
+
384
+ const outputQuality = combinedParameters.output_quality ?? combinedParameters.outputQuality ?? 80;
385
+ const safetyTolerance = combinedParameters.safety_tolerance ?? combinedParameters.safetyTolerance ?? 2;
386
+
387
+ // Validate and round width/height to multiples of 32 if provided
388
+ let width = combinedParameters.width;
389
+ let height = combinedParameters.height;
390
+
391
+ if (width !== undefined && width !== null) {
392
+ width = Math.max(256, Math.min(2048, Math.round(width / 32) * 32));
393
+ }
394
+ if (height !== undefined && height !== null) {
395
+ height = Math.max(256, Math.min(2048, Math.round(height / 32) * 32));
396
+ }
397
+
398
+ const basePayload = omitUndefined({
399
+ prompt: modelPromptText,
400
+ aspect_ratio: aspectRatio,
401
+ resolution: resolution,
402
+ output_format: outputFormat,
403
+ output_quality: Math.max(0, Math.min(100, outputQuality)),
404
+ safety_tolerance: Math.max(1, Math.min(5, safetyTolerance)),
405
+ ...(width !== undefined && width !== null ? { width } : {}),
406
+ ...(height !== undefined && height !== null ? { height } : {}),
407
+ ...(Number.isInteger(combinedParameters.seed) && combinedParameters.seed > 0 ? { seed: combinedParameters.seed } : {}),
408
+ });
409
+
410
+ // Include input_images array if we have images
411
+ const inputPayload = {
412
+ ...basePayload,
413
+ ...(normalizedImages.length > 0 ? { input_images: normalizedImages } : {})
414
+ };
415
+
353
416
  requestParameters = {
354
417
  input: inputPayload,
355
418
  };
@@ -2,6 +2,7 @@ import { fulfillWithTimeout } from '../lib/promiser.js';
2
2
  import { PathwayResolver } from './pathwayResolver.js';
3
3
  import CortexResponse from '../lib/cortexResponse.js';
4
4
  import { withRequestLoggingDisabled } from '../lib/logger.js';
5
+ import { sanitizeBase64 } from '../lib/util.js';
5
6
 
6
7
  // This resolver uses standard parameters required by Apollo server:
7
8
  // (parent, args, contextValue, info)
@@ -41,9 +42,37 @@ const rootResolver = async (parent, args, contextValue, info) => {
41
42
  let resultData = pathwayResolver.pathwayResultData ? JSON.stringify(pathwayResolver.pathwayResultData) : null;
42
43
 
43
44
  const { warnings, errors, previousResult, savedContextId, tool } = pathwayResolver;
44
-
45
- // Add request parameters back as debug
46
- const debug = pathwayResolver.prompts.map(prompt => prompt.debugInfo || '').join('\n').trim();
45
+
46
+ // Add request parameters back as debug - sanitize base64 data before returning
47
+ const debug = pathwayResolver.prompts.map(prompt => {
48
+ if (!prompt.debugInfo) return '';
49
+ try {
50
+ // Try to parse entire debugInfo as JSON first (for single JSON object)
51
+ try {
52
+ const parsed = JSON.parse(prompt.debugInfo);
53
+ return JSON.stringify(sanitizeBase64(parsed));
54
+ } catch (e) {
55
+ // Not a single JSON object, try line-by-line
56
+ const lines = prompt.debugInfo.split('\n');
57
+ return lines.map(line => {
58
+ const trimmed = line.trim();
59
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
60
+ try {
61
+ const parsed = JSON.parse(line);
62
+ return JSON.stringify(sanitizeBase64(parsed));
63
+ } catch (e) {
64
+ // Not valid JSON on this line, return as-is
65
+ return line;
66
+ }
67
+ }
68
+ return line;
69
+ }).join('\n');
70
+ }
71
+ } catch (e) {
72
+ // If sanitization fails, return original
73
+ return prompt.debugInfo;
74
+ }
75
+ }).join('\n').trim();
47
76
 
48
77
  return {
49
78
  debug,
@@ -9,7 +9,7 @@ import ws from 'ws';
9
9
  // Define models to test - 4.1 as default, include grok 4
10
10
  const TEST_MODELS = [
11
11
  'oai-gpt41', // Default 4.1 model
12
- 'xai-grok-4' // Grok 4 model
12
+ 'xai-grok-4-fast-reasoning' // Grok 4 model
13
13
  ];
14
14
 
15
15
  let testServer;