@aj-archipelago/cortex 1.4.2 → 1.4.3

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.
Files changed (86) hide show
  1. package/README.md +1 -0
  2. package/config.js +1 -1
  3. package/helper-apps/cortex-autogen2/.dockerignore +1 -0
  4. package/helper-apps/cortex-autogen2/Dockerfile +6 -10
  5. package/helper-apps/cortex-autogen2/Dockerfile.worker +2 -0
  6. package/helper-apps/cortex-autogen2/agents.py +203 -2
  7. package/helper-apps/cortex-autogen2/main.py +1 -1
  8. package/helper-apps/cortex-autogen2/pyproject.toml +12 -0
  9. package/helper-apps/cortex-autogen2/requirements.txt +14 -0
  10. package/helper-apps/cortex-autogen2/services/redis_publisher.py +1 -1
  11. package/helper-apps/cortex-autogen2/services/run_analyzer.py +1 -1
  12. package/helper-apps/cortex-autogen2/task_processor.py +431 -229
  13. package/helper-apps/cortex-autogen2/test_entity_fetcher.py +305 -0
  14. package/helper-apps/cortex-autogen2/tests/README.md +240 -0
  15. package/helper-apps/cortex-autogen2/tests/TEST_REPORT.md +342 -0
  16. package/helper-apps/cortex-autogen2/tests/__init__.py +8 -0
  17. package/helper-apps/cortex-autogen2/tests/analysis/__init__.py +1 -0
  18. package/helper-apps/cortex-autogen2/tests/analysis/improvement_suggester.py +224 -0
  19. package/helper-apps/cortex-autogen2/tests/analysis/trend_analyzer.py +211 -0
  20. package/helper-apps/cortex-autogen2/tests/cli/__init__.py +1 -0
  21. package/helper-apps/cortex-autogen2/tests/cli/run_tests.py +296 -0
  22. package/helper-apps/cortex-autogen2/tests/collectors/__init__.py +1 -0
  23. package/helper-apps/cortex-autogen2/tests/collectors/log_collector.py +252 -0
  24. package/helper-apps/cortex-autogen2/tests/collectors/progress_collector.py +182 -0
  25. package/helper-apps/cortex-autogen2/tests/conftest.py +15 -0
  26. package/helper-apps/cortex-autogen2/tests/database/__init__.py +1 -0
  27. package/helper-apps/cortex-autogen2/tests/database/repository.py +501 -0
  28. package/helper-apps/cortex-autogen2/tests/database/schema.sql +108 -0
  29. package/helper-apps/cortex-autogen2/tests/evaluators/__init__.py +1 -0
  30. package/helper-apps/cortex-autogen2/tests/evaluators/llm_scorer.py +294 -0
  31. package/helper-apps/cortex-autogen2/tests/evaluators/prompts.py +250 -0
  32. package/helper-apps/cortex-autogen2/tests/evaluators/wordcloud_validator.py +168 -0
  33. package/helper-apps/cortex-autogen2/tests/metrics/__init__.py +1 -0
  34. package/helper-apps/cortex-autogen2/tests/metrics/collector.py +155 -0
  35. package/helper-apps/cortex-autogen2/tests/orchestrator.py +576 -0
  36. package/helper-apps/cortex-autogen2/tests/test_cases.yaml +279 -0
  37. package/helper-apps/cortex-autogen2/tests/test_data.db +0 -0
  38. package/helper-apps/cortex-autogen2/tests/utils/__init__.py +3 -0
  39. package/helper-apps/cortex-autogen2/tests/utils/connectivity.py +112 -0
  40. package/helper-apps/cortex-autogen2/tools/azure_blob_tools.py +74 -24
  41. package/helper-apps/cortex-autogen2/tools/entity_api_registry.json +38 -0
  42. package/helper-apps/cortex-autogen2/tools/file_tools.py +1 -1
  43. package/helper-apps/cortex-autogen2/tools/search_tools.py +436 -238
  44. package/helper-apps/cortex-file-handler/package-lock.json +2 -2
  45. package/helper-apps/cortex-file-handler/package.json +1 -1
  46. package/helper-apps/cortex-file-handler/scripts/setup-test-containers.js +4 -5
  47. package/helper-apps/cortex-file-handler/src/blobHandler.js +36 -144
  48. package/helper-apps/cortex-file-handler/src/services/FileConversionService.js +5 -3
  49. package/helper-apps/cortex-file-handler/src/services/storage/AzureStorageProvider.js +34 -1
  50. package/helper-apps/cortex-file-handler/src/services/storage/GCSStorageProvider.js +22 -0
  51. package/helper-apps/cortex-file-handler/src/services/storage/LocalStorageProvider.js +28 -1
  52. package/helper-apps/cortex-file-handler/src/services/storage/StorageFactory.js +29 -4
  53. package/helper-apps/cortex-file-handler/src/services/storage/StorageProvider.js +11 -0
  54. package/helper-apps/cortex-file-handler/src/services/storage/StorageService.js +1 -1
  55. package/helper-apps/cortex-file-handler/tests/blobHandler.test.js +3 -2
  56. package/helper-apps/cortex-file-handler/tests/checkHashShortLived.test.js +8 -1
  57. package/helper-apps/cortex-file-handler/tests/containerConversionFlow.test.js +5 -2
  58. package/helper-apps/cortex-file-handler/tests/containerNameParsing.test.js +14 -7
  59. package/helper-apps/cortex-file-handler/tests/containerParameterFlow.test.js +5 -2
  60. package/helper-apps/cortex-file-handler/tests/storage/StorageFactory.test.js +31 -19
  61. package/package.json +1 -1
  62. package/server/modelExecutor.js +4 -0
  63. package/server/plugins/claude4VertexPlugin.js +540 -0
  64. package/server/plugins/openAiWhisperPlugin.js +43 -2
  65. package/tests/integration/rest/vendors/claude_streaming.test.js +121 -0
  66. package/tests/unit/plugins/claude4VertexPlugin.test.js +462 -0
  67. package/tests/unit/plugins/claude4VertexToolConversion.test.js +413 -0
  68. package/helper-apps/cortex-autogen/.funcignore +0 -8
  69. package/helper-apps/cortex-autogen/Dockerfile +0 -10
  70. package/helper-apps/cortex-autogen/OAI_CONFIG_LIST +0 -6
  71. package/helper-apps/cortex-autogen/agents.py +0 -493
  72. package/helper-apps/cortex-autogen/agents_extra.py +0 -14
  73. package/helper-apps/cortex-autogen/config.py +0 -18
  74. package/helper-apps/cortex-autogen/data_operations.py +0 -29
  75. package/helper-apps/cortex-autogen/function_app.py +0 -44
  76. package/helper-apps/cortex-autogen/host.json +0 -15
  77. package/helper-apps/cortex-autogen/main.py +0 -38
  78. package/helper-apps/cortex-autogen/prompts.py +0 -196
  79. package/helper-apps/cortex-autogen/prompts_extra.py +0 -5
  80. package/helper-apps/cortex-autogen/requirements.txt +0 -9
  81. package/helper-apps/cortex-autogen/search.py +0 -85
  82. package/helper-apps/cortex-autogen/test.sh +0 -40
  83. package/helper-apps/cortex-autogen/tools/sasfileuploader.py +0 -66
  84. package/helper-apps/cortex-autogen/utils.py +0 -88
  85. package/helper-apps/cortex-autogen2/DigiCertGlobalRootCA.crt.pem +0 -22
  86. package/helper-apps/cortex-autogen2/poetry.lock +0 -3652
@@ -16,6 +16,42 @@ class OpenAIWhisperPlugin extends ModelPlugin {
16
16
  super(pathway, model);
17
17
  }
18
18
 
19
+ // Minimal 429 retry wrapper for Whisper API calls
20
+ async executeWhisperRequest(cortexRequest) {
21
+ const maxRetries = 9;
22
+
23
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
24
+ try {
25
+ return await this.executeRequest(cortexRequest);
26
+ } catch (error) {
27
+
28
+ // Check if it's a 429 error
29
+ const is429 = error?.status === 429 ||
30
+ error?.response?.status === 429 ||
31
+ error?.message?.includes('429');
32
+
33
+ if (!is429 || attempt === maxRetries - 1) {
34
+ // Not a 429 or max retries reached, rethrow
35
+ throw error;
36
+ }
37
+
38
+ // Calculate backoff delay (exponential with jitter)
39
+ const retryAfter = error?.response?.headers?.['retry-after'];
40
+ // Fix: Validate parseInt result to prevent NaN
41
+ const baseDelay = retryAfter && !isNaN(parseInt(retryAfter))
42
+ ? parseInt(retryAfter) * 1000
43
+ : 2000 * Math.pow(2, attempt);
44
+ const jitter = baseDelay * 0.2 * Math.random();
45
+ const delay = baseDelay + jitter;
46
+
47
+ logger.warn(`Whisper 429 error (attempt ${attempt + 1}/${maxRetries}). Retrying in ${Math.round(delay)}ms`);
48
+ await new Promise(resolve => setTimeout(resolve, delay));
49
+ }
50
+ }
51
+
52
+ // Remove unreachable code - this line was never reached
53
+ }
54
+
19
55
  // Execute the request to the OpenAI Whisper API
20
56
  async execute(text, parameters, prompt, cortexRequest) {
21
57
  const { pathwayResolver } = cortexRequest;
@@ -47,7 +83,9 @@ class OpenAIWhisperPlugin extends ModelPlugin {
47
83
  };
48
84
 
49
85
  cortexRequest.initCallback = whisperInitCallback;
50
- return this.executeRequest(cortexRequest);
86
+
87
+ // return this.executeRequest(cortexRequest);
88
+ return this.executeWhisperRequest(cortexRequest);
51
89
 
52
90
  } catch (err) {
53
91
  logger.error(`Error getting word timestamped data from api: ${err}`);
@@ -72,7 +110,10 @@ class OpenAIWhisperPlugin extends ModelPlugin {
72
110
  cortexRequest.initCallback = whisperInitCallback;
73
111
 
74
112
  sendProgress(true, true);
75
- const res = await this.executeRequest(cortexRequest);
113
+
114
+ // const res = await this.executeRequest(cortexRequest);
115
+ const res = await this.executeWhisperRequest(cortexRequest);
116
+
76
117
  if (!res) {
77
118
  throw new Error('Received null or empty response');
78
119
  }
@@ -44,4 +44,125 @@ test('Claude SSE chat stream returns OAI-style chunks', async (t) => {
44
44
  t.true(assertAnyContentDelta(chunks));
45
45
  });
46
46
 
47
+ test('Claude 4 SSE chat stream with document block (PDF)', async (t) => {
48
+ const baseUrl = `http://localhost:${process.env.CORTEX_PORT}/v1`;
49
+
50
+ // Pick an available Claude 4 model from /models
51
+ let model = null;
52
+ try {
53
+ const res = await got(`${baseUrl}/models`, { responseType: 'json' });
54
+ const ids = (res.body?.data || []).map(m => m.id);
55
+ // Look for claude-4 or claude-45 models specifically
56
+ model = ids.find(id => /^claude-(4|45)/.test(id));
57
+ } catch (_) {}
58
+
59
+ if (!model) {
60
+ t.pass('Skipping - no Claude 4+ model configured');
61
+ return;
62
+ }
63
+
64
+ // Create a simple PDF document with base64 encoding (sample dummy PDF)
65
+ // This is a minimal valid PDF
66
+ const pdfContent = '%PDF-1.4\n%âãÏÓ\n1 0 obj\n<</Type/Catalog/Pages 2 0 R>>\nendobj\n2 0 obj\n<</Type/Pages/Kids[3 0 R]/Count 1>>\nendobj\n3 0 obj\n<</Type/Page/Parent 2 0 R/Resources<</Font<</F1 4 0 R>>>>/MediaBox[0 0 612 792]/Contents 5 0 R>>\nendobj\n4 0 obj\n<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>\nendobj\n5 0 obj\n<</Length 44>>\nstream\nBT\n/F1 12 Tf\n100 700 Td\n(Sample PDF) Tj\nET\nendstream\nendobj\nxref\n0 6\n0000000000 65535 f\n0000000010 00000 n\n0000000053 00000 n\n0000000102 00000 n\n0000000211 00000 n\n0000000280 00000 n\ntrailer\n<</Size 6/Root 1 0 R>>\nstartxref\n369\n%%EOF';
67
+ const base64Pdf = Buffer.from(pdfContent).toString('base64');
68
+
69
+ const payload = {
70
+ model,
71
+ messages: [
72
+ {
73
+ role: 'user',
74
+ content: [
75
+ {
76
+ type: 'text',
77
+ text: 'Please analyze this PDF document and tell me what you see. Be concise.'
78
+ },
79
+ {
80
+ type: 'document',
81
+ source: {
82
+ type: 'base64',
83
+ media_type: 'application/pdf',
84
+ data: base64Pdf
85
+ }
86
+ }
87
+ ]
88
+ }
89
+ ],
90
+ stream: true,
91
+ };
92
+
93
+ try {
94
+ const chunks = await collectSSEChunks(baseUrl, '/chat/completions', payload);
95
+ t.true(chunks.length > 0, 'Should receive SSE chunks');
96
+ chunks.forEach(ch => assertOAIChatChunkBasics(t, ch));
97
+ t.true(assertAnyContentDelta(chunks), 'Should have content delta in chunks');
98
+ } catch (err) {
99
+ // If the model doesn't support this format yet, skip gracefully
100
+ if (err.message && err.message.includes('document')) {
101
+ t.pass('Document blocks not yet supported by this model endpoint');
102
+ } else {
103
+ throw err;
104
+ }
105
+ }
106
+ });
107
+
108
+ test('Claude 4 SSE chat stream with text document', async (t) => {
109
+ const baseUrl = `http://localhost:${process.env.CORTEX_PORT}/v1`;
110
+
111
+ // Pick an available Claude 4 model from /models
112
+ let model = null;
113
+ try {
114
+ const res = await got(`${baseUrl}/models`, { responseType: 'json' });
115
+ const ids = (res.body?.data || []).map(m => m.id);
116
+ // Look for claude-4 or claude-45 models specifically
117
+ model = ids.find(id => /^claude-(4|45)/.test(id));
118
+ } catch (_) {}
119
+
120
+ if (!model) {
121
+ t.pass('Skipping - no Claude 4+ model configured');
122
+ return;
123
+ }
124
+
125
+ // Create a simple text document with base64 encoding
126
+ const textContent = 'This is a sample text document.\nIt contains multiple lines.\nThe document discusses the capabilities of Claude models with document support.';
127
+ const base64Text = Buffer.from(textContent).toString('base64');
128
+
129
+ const payload = {
130
+ model,
131
+ messages: [
132
+ {
133
+ role: 'user',
134
+ content: [
135
+ {
136
+ type: 'text',
137
+ text: 'Please summarize this text document for me in one sentence.'
138
+ },
139
+ {
140
+ type: 'document',
141
+ source: {
142
+ type: 'base64',
143
+ media_type: 'text/plain',
144
+ data: base64Text
145
+ }
146
+ }
147
+ ]
148
+ }
149
+ ],
150
+ stream: true,
151
+ };
152
+
153
+ try {
154
+ const chunks = await collectSSEChunks(baseUrl, '/chat/completions', payload);
155
+ t.true(chunks.length > 0, 'Should receive SSE chunks');
156
+ chunks.forEach(ch => assertOAIChatChunkBasics(t, ch));
157
+ t.true(assertAnyContentDelta(chunks), 'Should have content delta in chunks');
158
+ } catch (err) {
159
+ // If the model doesn't support this format yet, skip gracefully
160
+ if (err.message && err.message.includes('document')) {
161
+ t.pass('Document blocks not yet supported by this model endpoint');
162
+ } else {
163
+ throw err;
164
+ }
165
+ }
166
+ });
167
+
47
168
 
@@ -0,0 +1,462 @@
1
+ import test from 'ava';
2
+ import Claude4VertexPlugin from '../../../server/plugins/claude4VertexPlugin.js';
3
+ import { mockPathwayResolverMessages } from '../../helpers/mocks.js';
4
+ import { config } from '../../../config.js';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+
8
+ // Helper function to load test data from files
9
+ function loadTestData(filename) {
10
+ try {
11
+ const filePath = path.join(process.cwd(), 'tests', 'data', filename);
12
+ return fs.readFileSync(filePath, 'utf8');
13
+ } catch (error) {
14
+ // Throw error to make test failures explicit when required data is missing
15
+ throw new Error(`Failed to load required test data file ${filename}: ${error.message}`);
16
+ }
17
+ }
18
+
19
+ const { pathway, model } = mockPathwayResolverMessages;
20
+
21
+ test('constructor', (t) => {
22
+ const plugin = new Claude4VertexPlugin(pathway, model);
23
+ t.is(plugin.config, config);
24
+ t.is(plugin.pathwayPrompt, mockPathwayResolverMessages.pathway.prompt);
25
+ t.true(plugin.isMultiModal);
26
+ });
27
+
28
+ test('parseResponse', (t) => {
29
+ const plugin = new Claude4VertexPlugin(pathway, model);
30
+
31
+ // Test text content response
32
+ const dataWithTextContent = {
33
+ content: [
34
+ { type: 'text', text: 'Hello, World!' }
35
+ ],
36
+ usage: { input_tokens: 10, output_tokens: 5 },
37
+ stop_reason: 'end_turn'
38
+ };
39
+ const resultWithTextContent = plugin.parseResponse(dataWithTextContent);
40
+ t.truthy(resultWithTextContent.output_text === 'Hello, World!');
41
+ t.truthy(resultWithTextContent.finishReason === 'stop');
42
+ t.truthy(resultWithTextContent.usage);
43
+ t.truthy(resultWithTextContent.metadata.model === plugin.modelName);
44
+
45
+ // Test tool calls response
46
+ const dataWithToolCalls = {
47
+ content: [
48
+ {
49
+ type: 'tool_use',
50
+ id: 'tool_1',
51
+ name: 'search_web',
52
+ input: { query: 'test search' }
53
+ }
54
+ ],
55
+ usage: { input_tokens: 15, output_tokens: 8 },
56
+ stop_reason: 'tool_use'
57
+ };
58
+ const resultWithToolCalls = plugin.parseResponse(dataWithToolCalls);
59
+ t.truthy(resultWithToolCalls.output_text === '');
60
+ t.truthy(resultWithToolCalls.finishReason === 'tool_calls');
61
+ t.truthy(resultWithToolCalls.toolCalls);
62
+ t.truthy(resultWithToolCalls.toolCalls.length === 1);
63
+ t.truthy(resultWithToolCalls.toolCalls[0].id === 'tool_1');
64
+ t.truthy(resultWithToolCalls.toolCalls[0].function.name === 'search_web');
65
+ t.truthy(resultWithToolCalls.toolCalls[0].function.arguments === '{"query":"test search"}');
66
+
67
+ // Test data without content (should return original data)
68
+ const dataWithoutContent = {};
69
+ const resultWithoutContent = plugin.parseResponse(dataWithoutContent);
70
+ t.deepEqual(resultWithoutContent, dataWithoutContent);
71
+
72
+ // Test null data (should return null)
73
+ const dataNull = null;
74
+ const resultNull = plugin.parseResponse(dataNull);
75
+ t.is(resultNull, dataNull);
76
+ });
77
+
78
+ test('convertMessagesToClaudeVertex text message', async (t) => {
79
+ const plugin = new Claude4VertexPlugin(pathway, model);
80
+ // Test with text message
81
+ let messages = [
82
+ { role: 'system', content: 'System message' },
83
+ { role: 'user', content: 'User message' },
84
+ { role: 'assistant', content: 'Assistant message' },
85
+ { role: 'user', content: 'User message 2' },
86
+ ];
87
+ let output = await plugin.convertMessagesToClaudeVertex(messages);
88
+ t.deepEqual(output, {
89
+ system: 'System message',
90
+ modifiedMessages: [
91
+ {
92
+ role: "user",
93
+ content: [
94
+ {
95
+ type: "text",
96
+ text: "User message",
97
+ },
98
+ ],
99
+ },
100
+ {
101
+ role: "assistant",
102
+ content: [
103
+ {
104
+ type: "text",
105
+ text: "Assistant message",
106
+ },
107
+ ],
108
+ },
109
+ {
110
+ role: "user",
111
+ content: [
112
+ {
113
+ type: "text",
114
+ text: "User message 2",
115
+ },
116
+ ],
117
+ },
118
+ ],
119
+ });
120
+ });
121
+
122
+ test('convertMessagesToClaudeVertex image_url message', async (t) => {
123
+ const plugin = new Claude4VertexPlugin(pathway, model);
124
+ // Test with image_url message
125
+ const messages = [
126
+ {
127
+ role: 'assistant',
128
+ content: {
129
+ type: 'image_url',
130
+ image_url: 'https://static.toiimg.com/thumb/msid-102827471,width-1280,height-720,resizemode-4/102827471.jpg'
131
+ }
132
+ }
133
+ ];
134
+ const output = await plugin.convertMessagesToClaudeVertex(messages);
135
+
136
+ // Define a regex for base64 validation
137
+ const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
138
+ const base64Data = output.modifiedMessages[0].content[0].source.data;
139
+
140
+ t.is(output.system, '');
141
+ t.is(output.modifiedMessages[0].role, 'assistant');
142
+ t.is(output.modifiedMessages[0].content[0].type, 'image');
143
+ t.is(output.modifiedMessages[0].content[0].source.type, 'base64');
144
+ t.is(output.modifiedMessages[0].content[0].source.media_type, 'image/jpeg');
145
+
146
+ // Check if the base64 data looks reasonable
147
+ t.true(base64Data.length > 100); // Check if the data is sufficiently long
148
+ t.true(base64Regex.test(base64Data)); // Check if the data matches the base64 regex
149
+ });
150
+
151
+ test('convertMessagesToClaudeVertex stringified JSON PDF (real-world format)', async (t) => {
152
+ const plugin = new Claude4VertexPlugin(pathway, model);
153
+
154
+ // Test with stringified JSON PDF content (how it actually comes from the system)
155
+ const base64Pdf = Buffer.from('Sample PDF content').toString('base64');
156
+ const stringifiedPdf = JSON.stringify({
157
+ type: 'image_url',
158
+ url: 'data:application/pdf;base64,' + base64Pdf,
159
+ image_url: { url: 'data:application/pdf;base64,' + base64Pdf },
160
+ originalFilename: 'Invoice.pdf'
161
+ });
162
+
163
+ const messages = [
164
+ {
165
+ role: 'user',
166
+ content: [
167
+ {
168
+ type: 'text',
169
+ text: 'Please analyze this document'
170
+ },
171
+ {
172
+ type: 'text',
173
+ text: stringifiedPdf // Content item is stringified JSON
174
+ }
175
+ ]
176
+ }
177
+ ];
178
+
179
+ const output = await plugin.convertMessagesToClaudeVertex(messages);
180
+
181
+ // Should have 2 content items: text + document
182
+ t.is(output.modifiedMessages[0].role, 'user');
183
+ t.is(output.modifiedMessages[0].content.length, 2);
184
+
185
+ // First should be text
186
+ t.is(output.modifiedMessages[0].content[0].type, 'text');
187
+ t.is(output.modifiedMessages[0].content[0].text, 'Please analyze this document');
188
+
189
+ // Second should be converted to document block (not text!)
190
+ t.is(output.modifiedMessages[0].content[1].type, 'document');
191
+ t.is(output.modifiedMessages[0].content[1].source.type, 'base64');
192
+ t.is(output.modifiedMessages[0].content[1].source.media_type, 'application/pdf');
193
+ });
194
+
195
+ test('convertMessagesToClaudeVertex document block with PDF URL', async (t) => {
196
+ const plugin = new Claude4VertexPlugin(pathway, model);
197
+
198
+ // Test with document block containing PDF URL
199
+ const messages = [
200
+ {
201
+ role: 'user',
202
+ content: [
203
+ {
204
+ type: 'document',
205
+ source: {
206
+ type: 'url',
207
+ url: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf'
208
+ }
209
+ }
210
+ ]
211
+ }
212
+ ];
213
+
214
+ const output = await plugin.convertMessagesToClaudeVertex(messages);
215
+
216
+ // Verify the document was converted
217
+ t.is(output.modifiedMessages[0].role, 'user');
218
+ t.is(output.modifiedMessages[0].content[0].type, 'document');
219
+ t.is(output.modifiedMessages[0].content[0].source.type, 'base64');
220
+ t.is(output.modifiedMessages[0].content[0].source.media_type, 'application/pdf');
221
+
222
+ // Verify base64 data exists
223
+ const base64Data = output.modifiedMessages[0].content[0].source.data;
224
+ const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
225
+ t.true(base64Data.length > 100);
226
+ t.true(base64Regex.test(base64Data));
227
+ });
228
+
229
+ test('convertMessagesToClaudeVertex document block with text file URL', async (t) => {
230
+ const plugin = new Claude4VertexPlugin(pathway, model);
231
+
232
+ // Test with document block containing text file data (base64)
233
+ const textContent = 'Sample text file content';
234
+ const base64Text = Buffer.from(textContent).toString('base64');
235
+
236
+ const messages = [
237
+ {
238
+ role: 'user',
239
+ content: [
240
+ {
241
+ type: 'document',
242
+ source: {
243
+ type: 'base64',
244
+ media_type: 'text/plain',
245
+ data: base64Text
246
+ }
247
+ }
248
+ ]
249
+ }
250
+ ];
251
+
252
+ const output = await plugin.convertMessagesToClaudeVertex(messages);
253
+
254
+ // For text files, should be converted to text content block
255
+ t.is(output.modifiedMessages[0].role, 'user');
256
+ t.is(output.modifiedMessages[0].content[0].type, 'text');
257
+ t.is(output.modifiedMessages[0].content[0].text, textContent);
258
+ });
259
+
260
+ test('convertMessagesToClaudeVertex document block with file_id', async (t) => {
261
+ const plugin = new Claude4VertexPlugin(pathway, model);
262
+
263
+ // Test with document block containing file_id
264
+ const messages = [
265
+ {
266
+ role: 'user',
267
+ content: [
268
+ {
269
+ type: 'document',
270
+ source: {
271
+ type: 'file',
272
+ file_id: 'file_abc123'
273
+ }
274
+ }
275
+ ]
276
+ }
277
+ ];
278
+
279
+ const output = await plugin.convertMessagesToClaudeVertex(messages);
280
+
281
+ // Should pass through file_id reference
282
+ t.is(output.modifiedMessages[0].role, 'user');
283
+ t.is(output.modifiedMessages[0].content[0].type, 'document');
284
+ t.is(output.modifiedMessages[0].content[0].source.type, 'file');
285
+ t.is(output.modifiedMessages[0].content[0].source.file_id, 'file_abc123');
286
+ });
287
+
288
+ test('convertMessagesToClaudeVertex document block with base64 PDF', async (t) => {
289
+ const plugin = new Claude4VertexPlugin(pathway, model);
290
+
291
+ // Create a sample base64 PDF string
292
+ const base64Pdf = Buffer.from('Sample PDF content').toString('base64');
293
+
294
+ const messages = [
295
+ {
296
+ role: 'user',
297
+ content: [
298
+ {
299
+ type: 'document',
300
+ source: {
301
+ type: 'base64',
302
+ media_type: 'application/pdf',
303
+ data: base64Pdf
304
+ }
305
+ }
306
+ ]
307
+ }
308
+ ];
309
+
310
+ const output = await plugin.convertMessagesToClaudeVertex(messages);
311
+
312
+ // Should pass through as document block
313
+ t.is(output.modifiedMessages[0].role, 'user');
314
+ t.is(output.modifiedMessages[0].content[0].type, 'document');
315
+ t.is(output.modifiedMessages[0].content[0].source.type, 'base64');
316
+ t.is(output.modifiedMessages[0].content[0].source.media_type, 'application/pdf');
317
+ t.is(output.modifiedMessages[0].content[0].source.data, base64Pdf);
318
+ });
319
+
320
+ test('convertMessagesToClaudeVertex mixed content with documents', async (t) => {
321
+ const plugin = new Claude4VertexPlugin(pathway, model);
322
+
323
+ // Test with mixed content including document and text
324
+ const base64Pdf = Buffer.from('Sample PDF content').toString('base64');
325
+
326
+ const messages = [
327
+ {
328
+ role: 'user',
329
+ content: [
330
+ {
331
+ type: 'text',
332
+ text: 'Please analyze this document'
333
+ },
334
+ {
335
+ type: 'document',
336
+ source: {
337
+ type: 'base64',
338
+ media_type: 'application/pdf',
339
+ data: base64Pdf
340
+ }
341
+ }
342
+ ]
343
+ }
344
+ ];
345
+
346
+ const output = await plugin.convertMessagesToClaudeVertex(messages);
347
+
348
+ // Should have both text and document blocks
349
+ t.is(output.modifiedMessages[0].role, 'user');
350
+ t.is(output.modifiedMessages[0].content.length, 2);
351
+ t.is(output.modifiedMessages[0].content[0].type, 'text');
352
+ t.is(output.modifiedMessages[0].content[1].type, 'document');
353
+ });
354
+
355
+ test('convertMessagesToClaudeVertex unsupported type', async (t) => {
356
+ const plugin = new Claude4VertexPlugin(pathway, model);
357
+ // Test with unsupported type
358
+ const messages = [{ role: 'user', content: { type: 'unsupported_type' } }];
359
+ const output = await plugin.convertMessagesToClaudeVertex(messages);
360
+ t.deepEqual(output, { system: '', modifiedMessages: [{role: 'user', content: [] }] });
361
+ });
362
+
363
+ test('convertMessagesToClaudeVertex empty messages', async (t) => {
364
+ const plugin = new Claude4VertexPlugin(pathway, model);
365
+ // Test with empty messages
366
+ const messages = [];
367
+ const output = await plugin.convertMessagesToClaudeVertex(messages);
368
+ t.deepEqual(output, { system: '', modifiedMessages: [] });
369
+ });
370
+
371
+ test('convertMessagesToClaudeVertex system message', async (t) => {
372
+ const plugin = new Claude4VertexPlugin(pathway, model);
373
+ // Test with system message
374
+ const messages = [{ role: 'system', content: 'System message' }];
375
+ const output = await plugin.convertMessagesToClaudeVertex(messages);
376
+ t.deepEqual(output, { system: 'System message', modifiedMessages: [] });
377
+ });
378
+
379
+ test('convertMessagesToClaudeVertex system message with user message', async (t) => {
380
+ const plugin = new Claude4VertexPlugin(pathway, model);
381
+ // Test with system message followed by user message
382
+ const messages = [
383
+ { role: 'system', content: 'System message' },
384
+ { role: 'user', content: 'User message' }
385
+ ];
386
+ const output = await plugin.convertMessagesToClaudeVertex(messages);
387
+ t.deepEqual(output, {
388
+ system: 'System message',
389
+ modifiedMessages: [{ role: 'user', content: [{ type: 'text', text: 'User message' }] }]
390
+ });
391
+ });
392
+
393
+ test('convertMessagesToClaudeVertex with multi-part content array', async (t) => {
394
+ const plugin = new Claude4VertexPlugin(pathway, model);
395
+
396
+ // Test with multi-part content array including text, image, and document
397
+ const base64Pdf = Buffer.from('Sample PDF content').toString('base64');
398
+
399
+ const multiPartContent = [
400
+ {
401
+ type: 'text',
402
+ text: 'Hello world'
403
+ },
404
+ {
405
+ type: 'text',
406
+ text: 'Hello2 world2'
407
+ },
408
+ {
409
+ type: 'image_url',
410
+ image_url: 'https://static.toiimg.com/thumb/msid-102827471,width-1280,height-720,resizemode-4/102827471.jpg'
411
+ },
412
+ {
413
+ type: 'document',
414
+ source: {
415
+ type: 'base64',
416
+ media_type: 'application/pdf',
417
+ data: base64Pdf
418
+ }
419
+ }
420
+ ];
421
+
422
+ const messages = [
423
+ { role: 'system', content: 'System message' },
424
+ { role: 'user', content: multiPartContent }
425
+ ];
426
+
427
+ const output = await plugin.convertMessagesToClaudeVertex(messages);
428
+
429
+ // Verify system message is preserved
430
+ t.is(output.system, 'System message');
431
+
432
+ // Verify the user message role is preserved
433
+ t.is(output.modifiedMessages[0].role, 'user');
434
+
435
+ // Verify the content array has the correct number of items
436
+ // We expect 4 items: 2 text items, 1 image item, and 1 document item
437
+ t.is(output.modifiedMessages[0].content.length, 4);
438
+
439
+ // Verify the text content items
440
+ t.is(output.modifiedMessages[0].content[0].type, 'text');
441
+ t.is(output.modifiedMessages[0].content[0].text, 'Hello world');
442
+
443
+ t.is(output.modifiedMessages[0].content[1].type, 'text');
444
+ t.is(output.modifiedMessages[0].content[1].text, 'Hello2 world2');
445
+
446
+ // Verify the image content item
447
+ t.is(output.modifiedMessages[0].content[2].type, 'image');
448
+ t.is(output.modifiedMessages[0].content[2].source.type, 'base64');
449
+ t.is(output.modifiedMessages[0].content[2].source.media_type, 'image/jpeg');
450
+
451
+ // Verify the document content item
452
+ t.is(output.modifiedMessages[0].content[3].type, 'document');
453
+ t.is(output.modifiedMessages[0].content[3].source.type, 'base64');
454
+ t.is(output.modifiedMessages[0].content[3].source.media_type, 'application/pdf');
455
+
456
+ // Check if the base64 data looks reasonable
457
+ const base64Data = output.modifiedMessages[0].content[2].source.data;
458
+ const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/;
459
+ t.true(base64Data.length > 100); // Check if the data is sufficiently long
460
+ t.true(base64Regex.test(base64Data)); // Check if the data matches the base64 regex
461
+ });
462
+