@aj-archipelago/cortex 1.3.23 → 1.3.24

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.3.23",
3
+ "version": "1.3.24",
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": {
@@ -33,7 +33,7 @@
33
33
  "type": "module",
34
34
  "homepage": "https://github.com/aj-archipelago/cortex#readme",
35
35
  "dependencies": {
36
- "@aj-archipelago/subvibe": "^1.0.3",
36
+ "@aj-archipelago/subvibe": "^1.0.5",
37
37
  "@apollo/server": "^4.7.3",
38
38
  "@apollo/server-plugin-response-cache": "^4.1.2",
39
39
  "@apollo/utils.keyvadapter": "^3.0.0",
@@ -18,7 +18,7 @@ export default {
18
18
  const { aiStyle, AI_STYLE_ANTHROPIC, AI_STYLE_OPENAI } = args;
19
19
  const styleModel = aiStyle === "Anthropic" ? AI_STYLE_ANTHROPIC : AI_STYLE_OPENAI;
20
20
 
21
- const memoryContext = await callPathway('sys_search_memory', { ...args, section: 'memoryAll', updateContext: true });
21
+ const memoryContext = await callPathway('sys_search_memory', { ...args, stream: false, section: 'memoryAll', updateContext: true });
22
22
  if (memoryContext) {
23
23
  const {toolCallId} = addToolCalls(args.chatHistory, "search memory for relevant information", "memory_lookup");
24
24
  addToolResults(args.chatHistory, memoryContext, toolCallId);
@@ -26,9 +26,9 @@ export default {
26
26
 
27
27
  let result;
28
28
  if (args.voiceResponse) {
29
- result = await callPathway('sys_generator_quick', { ...args, model: styleModel, stream: false });
29
+ result = await callPathway('sys_generator_quick', { ...args, model: styleModel, stream: false }, resolver);
30
30
  } else {
31
- result = await callPathway('sys_generator_quick', { ...args, model: styleModel });
31
+ result = await callPathway('sys_generator_quick', { ...args, model: styleModel }, resolver);
32
32
  }
33
33
 
34
34
  resolver.tool = JSON.stringify({ toolUsed: "memory" });
@@ -13,7 +13,7 @@ export default {
13
13
  inputParameters: {
14
14
  messages: [],
15
15
  },
16
- model: 'oai-gpturbo',
16
+ model: 'oai-gpt4o',
17
17
  useInputChunking: false,
18
- emulateOpenAIChatModel: 'gpt-3.5-turbo',
18
+ emulateOpenAIChatModel: 'gpt-4o',
19
19
  }
@@ -79,6 +79,13 @@ class PathwayResolver {
79
79
  let streamErrorOccurred = false;
80
80
  let responseData = null;
81
81
 
82
+ const publishNestedRequestProgress = (requestProgress) => {
83
+ if (requestProgress.progress === 1 && this.rootRequestId) {
84
+ delete requestProgress.progress;
85
+ }
86
+ publishRequestProgress(requestProgress);
87
+ }
88
+
82
89
  try {
83
90
  responseData = await this.executePathway(args);
84
91
  }
@@ -105,7 +112,7 @@ class PathwayResolver {
105
112
 
106
113
  // some models don't support progress updates
107
114
  if (!modelTypesExcludedFromProgressUpdates.includes(this.model.type)) {
108
- await publishRequestProgress({
115
+ await publishNestedRequestProgress({
109
116
  requestId: this.rootRequestId || this.requestId,
110
117
  progress: Math.min(completedCount,totalCount) / totalCount,
111
118
  data: JSON.stringify(responseData),
@@ -144,10 +151,7 @@ class PathwayResolver {
144
151
 
145
152
  try {
146
153
  if (!streamEnded && requestProgress.data) {
147
- if (!(this.rootRequestId && requestProgress.progress === 1)) {
148
- logger.debug(`Publishing stream message to requestId ${this.requestId}: ${requestProgress.data}`);
149
- publishRequestProgress(requestProgress);
150
- }
154
+ publishNestedRequestProgress(requestProgress);
151
155
  streamEnded = requestProgress.progress === 1;
152
156
  }
153
157
  } catch (error) {
@@ -136,7 +136,16 @@ class Claude3VertexPlugin extends OpenAIVisionPlugin {
136
136
  // Extract system messages
137
137
  const systemMessages = messagesCopy.filter(message => message.role === "system");
138
138
  if (systemMessages.length > 0) {
139
- system = systemMessages.map(message => message.content).join("\n");
139
+ system = systemMessages.map(message => {
140
+ if (Array.isArray(message.content)) {
141
+ // For content arrays, extract text content and join
142
+ return message.content
143
+ .filter(item => item.type === 'text')
144
+ .map(item => item.text)
145
+ .join("\n");
146
+ }
147
+ return message.content;
148
+ }).join("\n");
140
149
  }
141
150
 
142
151
  // Filter out system messages and empty messages
@@ -213,6 +213,10 @@ class Gemini15ChatPlugin extends ModelPlugin {
213
213
 
214
214
  // If this message also has STOP, mark it for completion but don't overwrite the content
215
215
  if (eventData.candidates[0].finishReason === "STOP") {
216
+ // Send the content first
217
+ requestProgress.data = JSON.stringify(createChunk({
218
+ content: eventData.candidates[0].content.parts[0].text
219
+ }));
216
220
  requestProgress.progress = 1;
217
221
  }
218
222
  } else if (eventData.candidates?.[0]?.finishReason === "STOP") {
@@ -48,7 +48,7 @@ test('POST /completions', async (t) => {
48
48
  test('POST /chat/completions', async (t) => {
49
49
  const response = await got.post(`${API_BASE}/chat/completions`, {
50
50
  json: {
51
- model: 'gpt-3.5-turbo',
51
+ model: 'gpt-4o',
52
52
  messages: [{ role: 'user', content: 'Hello!' }],
53
53
  stream: false,
54
54
  },
@@ -63,7 +63,7 @@ test('POST /chat/completions', async (t) => {
63
63
  test('POST /chat/completions with multimodal content', async (t) => {
64
64
  const response = await got.post(`${API_BASE}/chat/completions`, {
65
65
  json: {
66
- model: 'claude-3.5-sonnet',
66
+ model: 'gpt-4o',
67
67
  messages: [{
68
68
  role: 'user',
69
69
  content: [
@@ -153,7 +153,7 @@ test('POST SSE: /v1/completions should send a series of events and a [DONE] even
153
153
 
154
154
  test('POST SSE: /v1/chat/completions should send a series of events and a [DONE] event', async (t) => {
155
155
  const payload = {
156
- model: 'gpt-3.5-turbo',
156
+ model: 'gpt-4o',
157
157
  messages: [
158
158
  {
159
159
  role: 'user',
@@ -177,7 +177,7 @@ test('POST SSE: /v1/chat/completions should send a series of events and a [DONE]
177
177
 
178
178
  test('POST SSE: /v1/chat/completions with multimodal content should send a series of events and a [DONE] event', async (t) => {
179
179
  const payload = {
180
- model: 'claude-3.5-sonnet',
180
+ model: 'gpt-4o',
181
181
  messages: [{
182
182
  role: 'user',
183
183
  content: [
@@ -213,7 +213,7 @@ test('POST SSE: /v1/chat/completions with multimodal content should send a serie
213
213
  test('POST /chat/completions should handle multimodal content for non-multimodal model', async (t) => {
214
214
  const response = await got.post(`${API_BASE}/chat/completions`, {
215
215
  json: {
216
- model: 'gpt-3.5-turbo',
216
+ model: 'gpt-4o',
217
217
  messages: [{
218
218
  role: 'user',
219
219
  content: [
@@ -242,7 +242,7 @@ test('POST /chat/completions should handle multimodal content for non-multimodal
242
242
 
243
243
  test('POST SSE: /v1/chat/completions should handle streaming multimodal content for non-multimodal model', async (t) => {
244
244
  const payload = {
245
- model: 'gpt-3.5-turbo',
245
+ model: 'gpt-4o',
246
246
  messages: [{
247
247
  role: 'user',
248
248
  content: [
@@ -282,7 +282,7 @@ test('POST SSE: /v1/chat/completions should handle streaming multimodal content
282
282
  test('POST /chat/completions should handle malformed multimodal content', async (t) => {
283
283
  const response = await got.post(`${API_BASE}/chat/completions`, {
284
284
  json: {
285
- model: 'claude-3.5-sonnet',
285
+ model: 'gpt-4o',
286
286
  messages: [{
287
287
  role: 'user',
288
288
  content: [
@@ -310,7 +310,7 @@ test('POST /chat/completions should handle malformed multimodal content', async
310
310
  test('POST /chat/completions should handle invalid image data', async (t) => {
311
311
  const response = await got.post(`${API_BASE}/chat/completions`, {
312
312
  json: {
313
- model: 'claude-3.5-sonnet',
313
+ model: 'gpt-4o',
314
314
  messages: [{
315
315
  role: 'user',
316
316
  content: [
@@ -361,7 +361,7 @@ test('POST /completions should handle model parameters', async (t) => {
361
361
  test('POST /chat/completions should handle function calling', async (t) => {
362
362
  const response = await got.post(`${API_BASE}/chat/completions`, {
363
363
  json: {
364
- model: 'gpt-3.5-turbo',
364
+ model: 'gpt-4o',
365
365
  messages: [{ role: 'user', content: 'What is the weather in Boston?' }],
366
366
  functions: [{
367
367
  name: 'get_weather',
@@ -401,7 +401,7 @@ test('POST /chat/completions should handle function calling', async (t) => {
401
401
  test('POST /chat/completions should validate response format', async (t) => {
402
402
  const response = await got.post(`${API_BASE}/chat/completions`, {
403
403
  json: {
404
- model: 'gpt-3.5-turbo',
404
+ model: 'gpt-4o',
405
405
  messages: [{ role: 'user', content: 'Hello!' }],
406
406
  stream: false,
407
407
  },
@@ -426,7 +426,7 @@ test('POST /chat/completions should validate response format', async (t) => {
426
426
  test('POST /chat/completions should handle system messages', async (t) => {
427
427
  const response = await got.post(`${API_BASE}/chat/completions`, {
428
428
  json: {
429
- model: 'gpt-3.5-turbo',
429
+ model: 'gpt-4o',
430
430
  messages: [
431
431
  { role: 'system', content: 'You are a helpful assistant.' },
432
432
  { role: 'user', content: 'Hello!' }
@@ -443,10 +443,29 @@ test('POST /chat/completions should handle system messages', async (t) => {
443
443
  });
444
444
 
445
445
  test('POST /chat/completions should handle errors gracefully', async (t) => {
446
+ const error = await t.throwsAsync(
447
+ () => got.post(`${API_BASE}/chat/completions`, {
448
+ json: {
449
+ // Missing required model field
450
+ messages: [{ role: 'user', content: 'Hello!' }],
451
+ },
452
+ responseType: 'json',
453
+ })
454
+ );
455
+
456
+ t.is(error.response.statusCode, 404);
457
+ });
458
+
459
+ test('POST /chat/completions should handle token limits', async (t) => {
446
460
  const response = await got.post(`${API_BASE}/chat/completions`, {
447
461
  json: {
448
- // Missing required model field
449
- messages: [{ role: 'user', content: 'Hello!' }],
462
+ model: 'gpt-4o',
463
+ messages: [{
464
+ role: 'user',
465
+ content: 'Hello!'.repeat(5000) // Very long message
466
+ }],
467
+ max_tokens: 100,
468
+ stream: false,
450
469
  },
451
470
  responseType: 'json',
452
471
  });
@@ -455,17 +474,16 @@ test('POST /chat/completions should handle errors gracefully', async (t) => {
455
474
  t.is(response.body.object, 'chat.completion');
456
475
  t.true(Array.isArray(response.body.choices));
457
476
  t.truthy(response.body.choices[0].message.content);
458
- });
477
+ });
459
478
 
460
- test('POST /chat/completions should handle token limits', async (t) => {
479
+ test('POST /chat/completions should return complete responses from gpt-4o', async (t) => {
461
480
  const response = await got.post(`${API_BASE}/chat/completions`, {
462
481
  json: {
463
- model: 'gpt-3.5-turbo',
464
- messages: [{
465
- role: 'user',
466
- content: 'Hello!'.repeat(5000) // Very long message
467
- }],
468
- max_tokens: 100,
482
+ model: 'gpt-4o',
483
+ messages: [
484
+ { role: 'system', content: 'You are a helpful assistant. Always end your response with the exact string "END_MARKER_XYZ".' },
485
+ { role: 'user', content: 'Say hello and explain why complete responses matter.' }
486
+ ],
469
487
  stream: false,
470
488
  },
471
489
  responseType: 'json',
@@ -474,6 +492,8 @@ test('POST /chat/completions should handle token limits', async (t) => {
474
492
  t.is(response.statusCode, 200);
475
493
  t.is(response.body.object, 'chat.completion');
476
494
  t.true(Array.isArray(response.body.choices));
477
- t.truthy(response.body.choices[0].message.content);
478
- });
495
+ console.log('GPT-4o Response:', JSON.stringify(response.body.choices[0].message.content));
496
+ const content = response.body.choices[0].message.content;
497
+ t.regex(content, /END_MARKER_XYZ$/);
498
+ });
479
499
 
@@ -0,0 +1,197 @@
1
+ import test from 'ava';
2
+ import serverFactory from '../index.js';
3
+ import { PathwayResolver } from '../server/pathwayResolver.js';
4
+ import OpenAIChatPlugin from '../server/plugins/openAiChatPlugin.js';
5
+ import GeminiChatPlugin from '../server/plugins/geminiChatPlugin.js';
6
+ import Gemini15ChatPlugin from '../server/plugins/gemini15ChatPlugin.js';
7
+ import Claude3VertexPlugin from '../server/plugins/claude3VertexPlugin.js';
8
+ import { config } from '../config.js';
9
+
10
+ let testServer;
11
+
12
+ test.before(async () => {
13
+ process.env.CORTEX_ENABLE_REST = 'true';
14
+ const { server, startServer } = await serverFactory();
15
+ startServer && await startServer();
16
+ testServer = server;
17
+ });
18
+
19
+ test.after.always('cleanup', async () => {
20
+ if (testServer) {
21
+ await testServer.stop();
22
+ }
23
+ });
24
+
25
+ // Helper function to create a PathwayResolver with a specific plugin
26
+ function createResolverWithPlugin(pluginClass, modelName = 'test-model') {
27
+ // Map plugin classes to their corresponding model types
28
+ const pluginToModelType = {
29
+ OpenAIChatPlugin: 'OPENAI-VISION',
30
+ GeminiChatPlugin: 'GEMINI-VISION',
31
+ Gemini15ChatPlugin: 'GEMINI-1.5-VISION',
32
+ Claude3VertexPlugin: 'CLAUDE-3-VERTEX'
33
+ };
34
+
35
+ const modelType = pluginToModelType[pluginClass.name];
36
+ if (!modelType) {
37
+ throw new Error(`Unknown plugin class: ${pluginClass.name}`);
38
+ }
39
+
40
+ const pathway = {
41
+ name: 'test-pathway',
42
+ model: modelName,
43
+ prompt: 'test prompt'
44
+ };
45
+
46
+ const model = {
47
+ name: modelName,
48
+ type: modelType
49
+ };
50
+
51
+ const resolver = new PathwayResolver({
52
+ config,
53
+ pathway,
54
+ args: {},
55
+ endpoints: { [modelName]: model }
56
+ });
57
+
58
+ resolver.modelExecutor.plugin = new pluginClass(pathway, model);
59
+ return resolver;
60
+ }
61
+
62
+ // Test OpenAI Chat Plugin Streaming
63
+ test('OpenAI Chat Plugin - processStreamEvent handles content chunks correctly', async t => {
64
+ const resolver = createResolverWithPlugin(OpenAIChatPlugin);
65
+ const plugin = resolver.modelExecutor.plugin;
66
+
67
+ // Test regular content chunk
68
+ const contentEvent = {
69
+ data: JSON.stringify({
70
+ id: 'test-id',
71
+ choices: [{
72
+ delta: { content: 'test content' },
73
+ finish_reason: null
74
+ }]
75
+ })
76
+ };
77
+
78
+ let progress = plugin.processStreamEvent(contentEvent, {});
79
+ t.is(progress.data, contentEvent.data);
80
+ t.falsy(progress.progress);
81
+
82
+ // Test stream end
83
+ const endEvent = {
84
+ data: JSON.stringify({
85
+ id: 'test-id',
86
+ choices: [{
87
+ delta: {},
88
+ finish_reason: 'stop'
89
+ }]
90
+ })
91
+ };
92
+
93
+ progress = plugin.processStreamEvent(endEvent, {});
94
+ t.is(progress.progress, 1);
95
+ });
96
+
97
+ // Test Gemini Chat Plugin Streaming
98
+ test('Gemini Chat Plugin - processStreamEvent handles content chunks correctly', async t => {
99
+ const resolver = createResolverWithPlugin(GeminiChatPlugin);
100
+ const plugin = resolver.modelExecutor.plugin;
101
+
102
+ // Test regular content chunk
103
+ const contentEvent = {
104
+ data: JSON.stringify({
105
+ candidates: [{
106
+ content: {
107
+ parts: [{ text: 'test content' }]
108
+ },
109
+ finishReason: null
110
+ }]
111
+ })
112
+ };
113
+
114
+ let progress = plugin.processStreamEvent(contentEvent, {});
115
+ t.truthy(progress.data, 'Should have data');
116
+ const parsedData = JSON.parse(progress.data);
117
+ t.truthy(parsedData.candidates, 'Should have candidates array');
118
+ t.truthy(parsedData.candidates[0].content, 'Should have content object');
119
+ t.truthy(parsedData.candidates[0].content.parts, 'Should have parts array');
120
+ t.is(parsedData.candidates[0].content.parts[0].text, 'test content', 'Content should match');
121
+ t.falsy(progress.progress);
122
+
123
+ // Test stream end with STOP
124
+ const endEvent = {
125
+ data: JSON.stringify({
126
+ candidates: [{
127
+ content: {
128
+ parts: [{ text: '' }]
129
+ },
130
+ finishReason: 'STOP'
131
+ }]
132
+ })
133
+ };
134
+
135
+ progress = plugin.processStreamEvent(endEvent, {});
136
+ t.is(progress.progress, 1);
137
+ });
138
+
139
+ // Test Gemini 15 Chat Plugin Streaming
140
+ test('Gemini 15 Chat Plugin - processStreamEvent handles safety blocks', async t => {
141
+ const resolver = createResolverWithPlugin(Gemini15ChatPlugin);
142
+ const plugin = resolver.modelExecutor.plugin;
143
+
144
+ // Test safety block
145
+ const safetyEvent = {
146
+ data: JSON.stringify({
147
+ candidates: [{
148
+ safetyRatings: [{ blocked: true }]
149
+ }]
150
+ })
151
+ };
152
+
153
+ const progress = plugin.processStreamEvent(safetyEvent, {});
154
+ t.true(progress.data.includes('Response blocked'));
155
+ t.is(progress.progress, 1);
156
+ });
157
+
158
+ // Test Claude 3 Vertex Plugin Streaming
159
+ test('Claude 3 Vertex Plugin - processStreamEvent handles message types', async t => {
160
+ const resolver = createResolverWithPlugin(Claude3VertexPlugin);
161
+ const plugin = resolver.modelExecutor.plugin;
162
+
163
+ // Test message start
164
+ const startEvent = {
165
+ data: JSON.stringify({
166
+ type: 'message_start',
167
+ message: { id: 'test-id' }
168
+ })
169
+ };
170
+
171
+ let progress = plugin.processStreamEvent(startEvent, {});
172
+ t.true(JSON.parse(progress.data).choices[0].delta.role === 'assistant');
173
+
174
+ // Test content block
175
+ const contentEvent = {
176
+ data: JSON.stringify({
177
+ type: 'content_block_delta',
178
+ delta: {
179
+ type: 'text_delta',
180
+ text: 'test content'
181
+ }
182
+ })
183
+ };
184
+
185
+ progress = plugin.processStreamEvent(contentEvent, {});
186
+ t.true(JSON.parse(progress.data).choices[0].delta.content === 'test content');
187
+
188
+ // Test message stop
189
+ const stopEvent = {
190
+ data: JSON.stringify({
191
+ type: 'message_stop'
192
+ })
193
+ };
194
+
195
+ progress = plugin.processStreamEvent(stopEvent, {});
196
+ t.is(progress.progress, 1);
197
+ });